import { Injectable } from '@nestjs/common'; import { FieldDefinition } from '../models/field-definition.model'; export interface FieldConfigDTO { id: string; apiName: string; label: string; type: string; placeholder?: string; helpText?: string; defaultValue?: any; isRequired?: boolean; isReadOnly?: boolean; showOnList?: boolean; showOnDetail?: boolean; showOnEdit?: boolean; sortable?: boolean; options?: Array<{ label: string; value: any }>; rows?: number; min?: number; max?: number; step?: number; accept?: string; relationObject?: string; relationObjects?: string[]; relationDisplayField?: string; relationTypeField?: string; format?: string; prefix?: string; suffix?: string; validationRules?: Array<{ type: string; value?: any; message?: string; }>; dependsOn?: string[]; computedValue?: string; } export interface ObjectDefinitionDTO { id: string; apiName: string; label: string; pluralLabel?: string; description?: string; isSystem: boolean; fields: FieldConfigDTO[]; relatedLists?: Array<{ title: string; relationName: string; objectApiName: string; fields: FieldConfigDTO[]; canCreate?: boolean; createRoute?: string; }>; } @Injectable() export class FieldMapperService { /** * Convert a field definition from the database to a frontend-friendly FieldConfig */ mapFieldToDTO(field: any): FieldConfigDTO { // Parse ui_metadata if it's a JSON string or object let uiMetadata: any = {}; const metadataField = field.ui_metadata || field.uiMetadata; if (metadataField) { if (typeof metadataField === 'string') { try { uiMetadata = JSON.parse(metadataField); } catch (e) { uiMetadata = {}; } } else { uiMetadata = metadataField; } } const frontendType = this.mapFieldType(field.type); const isLookupField = frontendType === 'belongsTo' || field.type.toLowerCase().includes('lookup'); return { id: field.id, apiName: field.apiName, label: field.label, type: frontendType, // Display properties placeholder: uiMetadata.placeholder || field.description, helpText: uiMetadata.helpText || field.description, defaultValue: field.defaultValue, // Validation isRequired: field.isRequired || false, isReadOnly: field.isSystem || uiMetadata.isReadOnly || false, // View visibility showOnList: uiMetadata.showOnList !== false, showOnDetail: uiMetadata.showOnDetail !== false, showOnEdit: uiMetadata.showOnEdit !== false && !field.isSystem, sortable: uiMetadata.sortable !== false, // Field type specific options options: uiMetadata.options, rows: uiMetadata.rows, min: uiMetadata.min, max: uiMetadata.max, step: uiMetadata.step, accept: uiMetadata.accept, relationObject: field.referenceObject, relationObjects: uiMetadata.relationObjects, // For lookup fields, provide default display field if not specified relationDisplayField: isLookupField ? (uiMetadata.relationDisplayField || 'name') : uiMetadata.relationDisplayField, relationTypeField: uiMetadata.relationTypeField, // Formatting format: uiMetadata.format, prefix: uiMetadata.prefix, suffix: uiMetadata.suffix, // Validation rules validationRules: this.buildValidationRules(field, uiMetadata), // Advanced dependsOn: uiMetadata.dependsOn, computedValue: uiMetadata.computedValue, }; } /** * Map database field type to frontend FieldType enum */ private mapFieldType(dbType: string): string { const typeMap: Record = { 'string': 'text', 'text': 'textarea', 'integer': 'number', 'decimal': 'number', 'boolean': 'boolean', 'date': 'date', 'datetime': 'datetime', 'time': 'time', 'email': 'email', 'url': 'url', 'phone': 'text', 'picklist': 'select', 'multipicklist': 'multiSelect', 'lookup': 'belongsTo', 'master-detail': 'belongsTo', 'currency': 'currency', 'percent': 'number', 'textarea': 'textarea', 'richtext': 'markdown', 'file': 'file', 'image': 'image', 'json': 'json', }; return typeMap[dbType.toLowerCase()] || 'text'; } /** * Build validation rules array */ private buildValidationRules(field: any, uiMetadata: any): Array { const rules = uiMetadata.validationRules || []; // Add required rule if field is required and not already in rules if (field.isRequired && !rules.some(r => r.type === 'required')) { rules.unshift({ type: 'required', message: `${field.label} is required`, }); } // Add length validation for string fields if (field.length && field.type === 'string') { rules.push({ type: 'max', value: field.length, message: `${field.label} must not exceed ${field.length} characters`, }); } // Add email validation if (field.type === 'email' && !rules.some(r => r.type === 'email')) { rules.push({ type: 'email', message: `${field.label} must be a valid email address`, }); } // Add URL validation if (field.type === 'url' && !rules.some(r => r.type === 'url')) { rules.push({ type: 'url', message: `${field.label} must be a valid URL`, }); } return rules; } /** * Convert object definition with fields to DTO */ mapObjectDefinitionToDTO(objectDef: any): ObjectDefinitionDTO { return { id: objectDef.id, apiName: objectDef.apiName, label: objectDef.label, pluralLabel: objectDef.pluralLabel, description: objectDef.description, isSystem: objectDef.isSystem || false, fields: (objectDef.fields || []) .filter((f: any) => f.isActive !== false) .sort((a: any, b: any) => (a.displayOrder || 0) - (b.displayOrder || 0)) .map((f: any) => this.mapFieldToDTO(f)), relatedLists: (objectDef.relatedLists || []).map((list: any) => ({ title: list.title, relationName: list.relationName, objectApiName: list.objectApiName, fields: (list.fields || []) .filter((f: any) => f.isActive !== false) .map((f: any) => this.mapFieldToDTO(f)) .filter((f: any) => f.showOnList !== false), canCreate: list.canCreate, createRoute: list.createRoute, })), }; } /** * Generate default UI metadata for a field type */ generateDefaultUIMetadata(fieldType: string): any { const defaults: Record = { text: { showOnList: true, showOnDetail: true, showOnEdit: true, sortable: true, }, textarea: { showOnList: false, showOnDetail: true, showOnEdit: true, sortable: false, rows: 4, }, number: { showOnList: true, showOnDetail: true, showOnEdit: true, sortable: true, }, currency: { showOnList: true, showOnDetail: true, showOnEdit: true, sortable: true, prefix: '$', step: 0.01, }, boolean: { showOnList: true, showOnDetail: true, showOnEdit: true, sortable: true, }, date: { showOnList: true, showOnDetail: true, showOnEdit: true, sortable: true, format: 'yyyy-MM-dd', }, datetime: { showOnList: false, showOnDetail: true, showOnEdit: true, sortable: true, format: 'yyyy-MM-dd HH:mm:ss', }, email: { showOnList: true, showOnDetail: true, showOnEdit: true, sortable: true, validationRules: [{ type: 'email' }], }, url: { showOnList: false, showOnDetail: true, showOnEdit: true, sortable: false, validationRules: [{ type: 'url' }], }, select: { showOnList: true, showOnDetail: true, showOnEdit: true, sortable: true, options: [], }, multiSelect: { showOnList: false, showOnDetail: true, showOnEdit: true, sortable: false, options: [], }, image: { showOnList: false, showOnDetail: true, showOnEdit: true, sortable: false, accept: 'image/*', }, file: { showOnList: false, showOnDetail: true, showOnEdit: true, sortable: false, }, }; return defaults[fieldType] || { showOnList: true, showOnDetail: true, showOnEdit: true, sortable: true, }; } }