From 414d73daefc9cd5944fe4170d9832051a579f7bf Mon Sep 17 00:00:00 2001 From: Francisco Gaona Date: Tue, 13 Jan 2026 19:22:41 +0100 Subject: [PATCH] WIP - update records fixes --- backend/src/models/base.model.ts | 35 ++++++++++++- .../object/models/dynamic-model.factory.ts | 51 ++++++++++++------- backend/src/object/object.service.ts | 3 +- 3 files changed, 67 insertions(+), 22 deletions(-) diff --git a/backend/src/models/base.model.ts b/backend/src/models/base.model.ts index 259e992..6584e0c 100644 --- a/backend/src/models/base.model.ts +++ b/backend/src/models/base.model.ts @@ -1,7 +1,38 @@ -import { Model, ModelOptions, QueryContext, snakeCaseMappers } from 'objection'; +import { Model, ModelOptions, QueryContext } from 'objection'; export class BaseModel extends Model { - static columnNameMappers = snakeCaseMappers(); + /** + * Use a minimal column mapper: keep property names as-is, but handle + * timestamp fields that are stored as created_at/updated_at in the DB. + */ + static columnNameMappers = { + parse(dbRow: Record) { + const mapped: Record = {}; + for (const [key, value] of Object.entries(dbRow || {})) { + if (key === 'created_at') { + mapped.createdAt = value; + } else if (key === 'updated_at') { + mapped.updatedAt = value; + } else { + mapped[key] = value; + } + } + return mapped; + }, + format(model: Record) { + const mapped: Record = {}; + for (const [key, value] of Object.entries(model || {})) { + if (key === 'createdAt') { + mapped.created_at = value; + } else if (key === 'updatedAt') { + mapped.updated_at = value; + } else { + mapped[key] = value; + } + } + return mapped; + }, + }; id: string; createdAt: Date; diff --git a/backend/src/object/models/dynamic-model.factory.ts b/backend/src/object/models/dynamic-model.factory.ts index 3ce2c0c..78e4848 100644 --- a/backend/src/object/models/dynamic-model.factory.ts +++ b/backend/src/object/models/dynamic-model.factory.ts @@ -179,7 +179,8 @@ export class DynamicModelFactory { * Convert a field definition to JSON schema property */ private static fieldToJsonSchema(field: FieldDefinition): Record { - switch (field.type.toUpperCase()) { + const baseSchema = () => { + switch (field.type.toUpperCase()) { case 'TEXT': case 'STRING': case 'EMAIL': @@ -187,45 +188,57 @@ export class DynamicModelFactory { case 'PHONE': case 'PICKLIST': case 'MULTI_PICKLIST': - return { - type: 'string', - ...(field.isUnique && { uniqueItems: true }), - }; + return { + type: 'string', + ...(field.isUnique && { uniqueItems: true }), + }; case 'LONG_TEXT': - return { type: 'string' }; + return { type: 'string' }; case 'NUMBER': case 'DECIMAL': case 'CURRENCY': case 'PERCENT': - return { - type: 'number', - ...(field.isUnique && { uniqueItems: true }), - }; + return { + type: 'number', + ...(field.isUnique && { uniqueItems: true }), + }; case 'INTEGER': - return { - type: 'integer', - ...(field.isUnique && { uniqueItems: true }), - }; + return { + type: 'integer', + ...(field.isUnique && { uniqueItems: true }), + }; case 'BOOLEAN': - return { type: 'boolean', default: false }; + return { type: 'boolean', default: false }; case 'DATE': - return { type: 'string', format: 'date' }; + return { type: 'string', format: 'date' }; case 'DATE_TIME': - return { type: 'string', format: 'date-time' }; + return { type: 'string', format: 'date-time' }; case 'LOOKUP': case 'BELONGS_TO': - return { type: 'string' }; + return { type: 'string' }; default: - return { type: 'string' }; + return { type: 'string' }; + } + }; + + const schema = baseSchema(); + + // Allow null for non-required fields so optional strings/numbers don't fail validation + if (!field.isRequired) { + return { + anyOf: [schema, { type: 'null' }], + }; } + + return schema; } /** diff --git a/backend/src/object/object.service.ts b/backend/src/object/object.service.ts index 9e96032..6ead21f 100644 --- a/backend/src/object/object.service.ts +++ b/backend/src/object/object.service.ts @@ -1179,7 +1179,8 @@ export class ObjectService { objectApiName, editableData, ); - await boundModel.query().where({ id: recordId }).update(normalizedEditableData); + // Use patch to avoid validating or overwriting fields that aren't present in the edit view + await boundModel.query().patch(normalizedEditableData).where({ id: recordId }); const record = await boundModel.query().where({ id: recordId }).first(); await this.indexRecord(resolvedTenantId, objectApiName, objectDefModel.fields, record); return record;