diff --git a/backend/src/object/object.service.ts b/backend/src/object/object.service.ts index eddd8b6..1b66bca 100644 --- a/backend/src/object/object.service.ts +++ b/backend/src/object/object.service.ts @@ -271,6 +271,10 @@ export class ObjectService { relationObject?: string; relationDisplayField?: string; defaultValue?: string; + length?: number; + precision?: number; + scale?: number; + uiMetadata?: any; }, ) { const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); @@ -283,8 +287,11 @@ export class ObjectService { // Use relationObject if provided (alias for referenceObject) const referenceObject = data.referenceObject || data.relationObject; + // Generate UUID in Node.js instead of using MySQL UUID() function + const fieldId = require('crypto').randomUUID(); + const fieldData: any = { - id: knex.raw('(UUID())'), + id: fieldId, objectDefinitionId: obj.id, apiName: data.apiName, label: data.label, @@ -294,20 +301,41 @@ export class ObjectService { isUnique: data.isUnique ?? false, referenceObject: referenceObject, defaultValue: data.defaultValue, + length: data.length, + precision: data.precision, + scale: data.scale, created_at: knex.fn.now(), updated_at: knex.fn.now(), }; - // Store relationDisplayField in UI metadata if provided + // Merge UI metadata + const uiMetadata: any = {}; if (data.relationDisplayField) { - fieldData.ui_metadata = JSON.stringify({ - relationDisplayField: data.relationDisplayField, - }); + uiMetadata.relationDisplayField = data.relationDisplayField; + } + if (data.uiMetadata) { + Object.assign(uiMetadata, data.uiMetadata); + } + if (Object.keys(uiMetadata).length > 0) { + fieldData.ui_metadata = JSON.stringify(uiMetadata); } - const [id] = await knex('field_definitions').insert(fieldData); + await knex('field_definitions').insert(fieldData); + const createdField = await knex('field_definitions').where({ id: fieldId }).first(); + + // Add the column to the physical table + const schemaManagementService = new SchemaManagementService(); + try { + await schemaManagementService.addFieldToTable(knex, objectApiName, createdField); + this.logger.log(`Added column ${data.apiName} to table for object ${objectApiName}`); + } catch (error) { + // If column creation fails, delete the field definition to maintain consistency + this.logger.error(`Failed to add column ${data.apiName}: ${error.message}`); + await knex('field_definitions').where({ id: fieldId }).delete(); + throw new Error(`Failed to create field column: ${error.message}`); + } - return knex('field_definitions').where({ id }).first(); + return createdField; } // Helper to get table name from object definition @@ -874,26 +902,39 @@ export class ObjectService { } // Clean up page layouts - remove field references from layoutConfig - const pageLayouts = await knex('page_layouts') - .where({ objectDefinitionId: objectDef.id }); - - for (const layout of pageLayouts) { - const layoutConfig = layout.layout_config ? JSON.parse(layout.layout_config) : { fields: [] }; + try { + const pageLayouts = await knex('page_layouts') + .where({ object_id: objectDef.id }); - // Filter out any field references for this field - if (layoutConfig.fields) { - layoutConfig.fields = layoutConfig.fields.filter( - (f: any) => f.fieldId !== field.id, - ); + for (const layout of pageLayouts) { + // Handle JSON column that might already be parsed + let layoutConfig; + if (layout.layout_config) { + layoutConfig = typeof layout.layout_config === 'string' + ? JSON.parse(layout.layout_config) + : layout.layout_config; + } else { + layoutConfig = { fields: [] }; + } + + // Filter out any field references for this field + if (layoutConfig.fields) { + layoutConfig.fields = layoutConfig.fields.filter( + (f: any) => f.fieldId !== field.id, + ); + } + + // Update the page layout + await knex('page_layouts') + .where({ id: layout.id }) + .update({ + layout_config: JSON.stringify(layoutConfig), + updated_at: knex.fn.now(), + }); } - - // Update the page layout - await knex('page_layouts') - .where({ id: layout.id }) - .update({ - layout_config: JSON.stringify(layoutConfig), - updated_at: knex.fn.now(), - }); + } catch (error) { + // If page layouts table doesn't exist or query fails, log but continue + this.logger.warn(`Could not update page layouts for field deletion: ${error.message}`); } // Clean up dependsOn references in other fields @@ -902,7 +943,15 @@ export class ObjectService { .whereNot({ id: field.id }); for (const otherField of otherFields) { - const metadata = otherField.ui_metadata ? JSON.parse(otherField.ui_metadata) : {}; + // Handle JSON column that might already be parsed + let metadata; + if (otherField.ui_metadata) { + metadata = typeof otherField.ui_metadata === 'string' + ? JSON.parse(otherField.ui_metadata) + : otherField.ui_metadata; + } else { + metadata = {}; + } if (metadata.dependsOn && Array.isArray(metadata.dependsOn)) { metadata.dependsOn = metadata.dependsOn.filter( diff --git a/backend/src/object/schema-management.service.ts b/backend/src/object/schema-management.service.ts index 4b73305..2130c00 100644 --- a/backend/src/object/schema-management.service.ts +++ b/backend/src/object/schema-management.service.ts @@ -125,15 +125,30 @@ export class SchemaManagementService { let column: Knex.ColumnBuilder; switch (field.type) { + // Text types case 'String': + case 'TEXT': + case 'EMAIL': + case 'PHONE': + case 'URL': column = table.string(columnName, field.length || 255); break; case 'Text': + case 'LONG_TEXT': column = table.text(columnName); break; + case 'PICKLIST': + case 'MULTI_PICKLIST': + column = table.string(columnName, 255); + break; + + // Numeric types case 'Number': + case 'NUMBER': + case 'CURRENCY': + case 'PERCENT': if (field.scale && field.scale > 0) { column = table.decimal( columnName, @@ -146,18 +161,28 @@ export class SchemaManagementService { break; case 'Boolean': + case 'BOOLEAN': column = table.boolean(columnName).defaultTo(false); break; + // Date types case 'Date': + case 'DATE': column = table.date(columnName); break; case 'DateTime': + case 'DATE_TIME': column = table.datetime(columnName); break; + case 'TIME': + column = table.time(columnName); + break; + + // Relationship types case 'Reference': + case 'LOOKUP': column = table.uuid(columnName); if (field.referenceObject) { const refTableName = this.getTableName(field.referenceObject); @@ -165,19 +190,30 @@ export class SchemaManagementService { } break; + // Email (legacy) case 'Email': column = table.string(columnName, 255); break; + // Phone (legacy) case 'Phone': column = table.string(columnName, 50); break; + // Url (legacy) case 'Url': column = table.string(columnName, 255); break; + // File types + case 'FILE': + case 'IMAGE': + column = table.text(columnName); // Store file path or URL + break; + + // JSON case 'Json': + case 'JSON': column = table.json(columnName); break; diff --git a/frontend/pages/setup/objects/[apiName].vue b/frontend/pages/setup/objects/[apiName].vue index 2908a29..b4998be 100644 --- a/frontend/pages/setup/objects/[apiName].vue +++ b/frontend/pages/setup/objects/[apiName].vue @@ -560,13 +560,29 @@ const saveField = async () => { defaultValue: fieldForm.value.defaultValue, } + // Extract type-specific database fields + const typeAttrs = fieldForm.value.typeAttributes || {} + + // For text fields + if (fieldForm.value.type === 'text' && typeAttrs.maxLength) { + payload.length = typeAttrs.maxLength + } + + // For number and currency fields + if ((fieldForm.value.type === 'number' || fieldForm.value.type === 'currency') && typeAttrs.scale !== undefined) { + payload.scale = typeAttrs.scale + if (typeAttrs.scale > 0) { + payload.precision = 10 // Default precision for decimals + } + } + // Merge UI metadata const uiMetadata: any = { placeholder: fieldForm.value.placeholder, helpText: fieldForm.value.helpText, } - // Add type-specific attributes + // Add type-specific attributes to UI metadata if (fieldForm.value.typeAttributes) { Object.assign(uiMetadata, fieldForm.value.typeAttributes) }