diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/from-entity-to-scalar-entity.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/from-entity-to-scalar-entity.util.ts index f50b223269..f5ac0c5f67 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/from-entity-to-scalar-entity.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/from-entity-to-scalar-entity.util.ts @@ -13,12 +13,12 @@ const serializeScalarValue = (value: unknown): unknown => { return value ?? null; }; -const BASE_SCALAR_PROPERTY_NAMES = [ - 'id', - 'workspaceId', - 'applicationId', - 'universalIdentifier', -] as const; +export const BASE_SCALAR_PROPERTY_NAME = { + id: 'id', + workspaceId: 'workspaceId', + applicationId: 'applicationId', + universalIdentifier: 'universalIdentifier', +} as const; // Allow-list counterpart to removePropertiesFromRecord: rather than forwarding // every entity property except the known relations (deny-list), this forwards only @@ -42,7 +42,7 @@ export const fromEntityToScalarEntity = ({ const scalarProperties: Record = {}; for (const propertyName of [ - ...BASE_SCALAR_PROPERTY_NAMES, + ...Object.values(BASE_SCALAR_PROPERTY_NAME), ...Object.keys(propertiesConfiguration), ]) { scalarProperties[propertyName] = serializeScalarValue( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/__snapshots__/delete-flat-entity-foreign-key-aggregators.util.spec.ts.snap b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/__snapshots__/delete-flat-entity-foreign-key-aggregators.util.spec.ts.snap new file mode 100644 index 0000000000..f546158450 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/__snapshots__/delete-flat-entity-foreign-key-aggregators.util.spec.ts.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`deleteFlatEntityForeignKeyAggregators should strip raw workspace-scoped properties from a flat field metadata 1`] = ` +{ + "applicationUniversalIdentifier": "application-universal-identifier", + "calendarEndViewUniversalIdentifiers": [], + "calendarViewUniversalIdentifiers": [], + "createdAt": "2024-01-01T00:00:00.000Z", + "defaultValue": null, + "description": "default flat field metadata description", + "fieldPermissionUniversalIdentifiers": [], + "icon": "icon", + "isActive": true, + "isLabelSyncedWithName": false, + "isNullable": true, + "isSystem": false, + "isSystemSideEffect": false, + "isUIEditable": true, + "isUnique": false, + "kanbanAggregateOperationViewUniversalIdentifiers": [], + "label": "flat field metadata label", + "mainGroupByFieldMetadataViewUniversalIdentifiers": [], + "morphId": null, + "name": "flatFieldMetadataName", + "objectMetadataUniversalIdentifier": "object-metadata-universal-identifier", + "options": null, + "overrides": null, + "relationTargetFieldMetadataUniversalIdentifier": null, + "relationTargetObjectMetadataUniversalIdentifier": null, + "searchFieldMetadataUniversalIdentifiers": [], + "type": "TEXT", + "universalIdentifier": "field-universal-identifier", + "universalSettings": null, + "updatedAt": "2024-01-01T00:00:00.000Z", + "viewFieldUniversalIdentifiers": [ + "view-field-universal-identifier", + ], + "viewFilterUniversalIdentifiers": [], + "viewSortUniversalIdentifiers": [], +} +`; + +exports[`deleteFlatEntityForeignKeyAggregators should strip raw workspace-scoped properties from a flat object metadata 1`] = ` +{ + "applicationUniversalIdentifier": "application-universal-identifier", + "color": null, + "createdAt": "2024-01-01T00:00:00.000Z", + "description": "default flat object metadata description", + "duplicateCriteria": null, + "fieldPermissionUniversalIdentifiers": [], + "fieldUniversalIdentifiers": [ + "field-universal-identifier", + ], + "icon": "icon", + "imageIdentifierFieldMetadataUniversalIdentifier": "image-identifier-field-metadata-universal-identifier", + "indexMetadataUniversalIdentifiers": [], + "isActive": true, + "isAuditLogged": true, + "isLabelSyncedWithName": false, + "isRemote": false, + "isSearchable": true, + "isSystem": false, + "isUICreatable": true, + "isUIEditable": true, + "labelIdentifierFieldMetadataUniversalIdentifier": "label-identifier-field-metadata-universal-identifier", + "labelPlural": "default flat object metadata label plural", + "labelSingular": "default flat object metadata label singular", + "namePlural": "defaultflatObjectMetadataNamePlural", + "nameSingular": "defaultflatObjectMetadataNameSingular", + "objectPermissionUniversalIdentifiers": [], + "overrides": null, + "searchFieldMetadataUniversalIdentifiers": [], + "shortcut": "shortcut", + "targetTableName": "", + "universalIdentifier": "object-universal-identifier", + "updatedAt": "2024-01-01T00:00:00.000Z", + "viewUniversalIdentifiers": [ + "view-universal-identifier", + ], +} +`; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/delete-flat-entity-foreign-key-aggregators.util.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/delete-flat-entity-foreign-key-aggregators.util.spec.ts new file mode 100644 index 0000000000..42183ede9d --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/delete-flat-entity-foreign-key-aggregators.util.spec.ts @@ -0,0 +1,66 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock'; +import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock'; +import { deleteFlatEntityForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-flat-entity-foreign-key-aggregators.util'; + +describe('deleteFlatEntityForeignKeyAggregators', () => { + it('should strip raw workspace-scoped properties from a flat object metadata', () => { + const flatObjectMetadata = getFlatObjectMetadataMock({ + universalIdentifier: 'object-universal-identifier', + applicationId: 'application-id', + applicationUniversalIdentifier: 'application-universal-identifier', + labelIdentifierFieldMetadataId: 'label-identifier-field-metadata-id', + labelIdentifierFieldMetadataUniversalIdentifier: + 'label-identifier-field-metadata-universal-identifier', + imageIdentifierFieldMetadataId: 'image-identifier-field-metadata-id', + imageIdentifierFieldMetadataUniversalIdentifier: + 'image-identifier-field-metadata-universal-identifier', + fieldIds: ['field-id'], + fieldUniversalIdentifiers: ['field-universal-identifier'], + viewIds: ['view-id'], + viewUniversalIdentifiers: ['view-universal-identifier'], + }); + + const result = deleteFlatEntityForeignKeyAggregators({ + universalFlatEntity: flatObjectMetadata, + metadataName: 'objectMetadata', + }); + + expect(result).toMatchSnapshot(); + }); + + it('should strip raw workspace-scoped properties from a flat field metadata', () => { + const flatFieldMetadata = getFlatFieldMetadataMock({ + universalIdentifier: 'field-universal-identifier', + type: FieldMetadataType.TEXT, + applicationUniversalIdentifier: 'application-universal-identifier', + objectMetadataId: 'object-metadata-id', + objectMetadataUniversalIdentifier: 'object-metadata-universal-identifier', + viewFieldIds: ['view-field-id'], + viewFieldUniversalIdentifiers: ['view-field-universal-identifier'], + }); + + const result = deleteFlatEntityForeignKeyAggregators({ + universalFlatEntity: flatFieldMetadata, + metadataName: 'fieldMetadata', + }); + + expect(result).toMatchSnapshot(); + }); + + it('should not mutate the input entity', () => { + const flatObjectMetadata = getFlatObjectMetadataMock({ + universalIdentifier: 'object-universal-identifier', + }); + + deleteFlatEntityForeignKeyAggregators({ + universalFlatEntity: flatObjectMetadata, + metadataName: 'objectMetadata', + }); + + expect(flatObjectMetadata).toHaveProperty('id'); + expect(flatObjectMetadata).toHaveProperty('workspaceId'); + expect(flatObjectMetadata).toHaveProperty('fieldIds'); + }); +}); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-flat-entity-foreign-key-aggregators.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-flat-entity-foreign-key-aggregators.util.ts new file mode 100644 index 0000000000..9029555108 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-flat-entity-foreign-key-aggregators.util.ts @@ -0,0 +1,54 @@ +import { type AllMetadataName } from 'twenty-shared/metadata'; +import { isDefined } from 'twenty-shared/utils'; + +import { ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant'; +import { ALL_ONE_TO_MANY_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant'; +import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type'; +import { BASE_SCALAR_PROPERTY_NAME } from 'src/engine/metadata-modules/flat-entity/utils/from-entity-to-scalar-entity.util'; + +const BASE_WORKSPACE_SCOPED_PROPERTIES = [ + BASE_SCALAR_PROPERTY_NAME.id, + BASE_SCALAR_PROPERTY_NAME.workspaceId, + BASE_SCALAR_PROPERTY_NAME.applicationId, +] as const; + +export const deleteFlatEntityForeignKeyAggregators = < + T extends AllMetadataName, +>({ + universalFlatEntity, + metadataName, +}: { + universalFlatEntity: MetadataUniversalFlatEntity; + metadataName: T; +}): MetadataUniversalFlatEntity => { + const result = { ...universalFlatEntity } as Record; + + for (const baseProperty of BASE_WORKSPACE_SCOPED_PROPERTIES) { + delete result[baseProperty]; + } + + const propertiesConfiguration = + ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME[metadataName]; + + for (const [propertyName, propertyConfiguration] of Object.entries( + propertiesConfiguration, + ) as [string, { universalProperty?: string }][]) { + if (isDefined(propertyConfiguration.universalProperty)) { + delete result[propertyName]; + } + } + + const oneToManyRelations = ALL_ONE_TO_MANY_METADATA_RELATIONS[metadataName]; + + for (const relation of Object.values(oneToManyRelations) as ({ + flatEntityForeignKeyAggregator: string; + } | null)[]) { + if (!isDefined(relation)) { + continue; + } + + delete result[relation.flatEntityForeignKeyAggregator]; + } + + return result as MetadataUniversalFlatEntity; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service.ts index d82a344238..59c98990d6 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service.ts @@ -23,6 +23,7 @@ import { UniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace- import { addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/add-universal-flat-entity-to-universal-flat-entity-and-related-entity-maps-through-mutation-or-throw.util'; import { deleteUniversalFlatEntityForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-foreign-key-aggregators.util'; import { deleteUniversalFlatEntityFromUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-from-universal-flat-entity-and-related-entity-maps-through-mutation-or-throw.util'; +import { deleteFlatEntityForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-flat-entity-foreign-key-aggregators.util'; import { deleteUniversalFlatEntityFromUniversalFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-from-universal-flat-entity-maps-through-mutation-or-throw.util'; import { replaceUniversalFlatEntityInUniversalFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/replace-universal-flat-entity-in-universal-flat-entity-maps-through-mutation-or-throw.util'; import { resetUniversalFlatEntityForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; @@ -168,13 +169,19 @@ export abstract class WorkspaceEntityMigrationBuilderService< }, ); + const universalFlatEntityToDeletePayload = + deleteFlatEntityForeignKeyAggregators({ + metadataName: this.metadataName, + universalFlatEntity: universalFlatEntityToDelete, + }); + actionsResult.delete.push( ...(Array.isArray(validationResult.action) ? validationResult.action : [validationResult.action] ).map((action) => ({ ...action, - flatEntity: universalFlatEntityToDelete, + flatEntity: universalFlatEntityToDeletePayload, })), ); } diff --git a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap index c38a9ea0fc..0c5d8f4d09 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap @@ -773,19 +773,14 @@ exports[`syncApplication should delete old field and create equivalent one when "actions": [ { "flatEntity": { - "applicationId": Any, "applicationUniversalIdentifier": Any, - "calendarEndViewIds": [], "calendarEndViewUniversalIdentifiers": [], - "calendarViewIds": [], "calendarViewUniversalIdentifiers": [], "createdAt": Any, "defaultValue": null, "description": "Ticket description", - "fieldPermissionIds": [], "fieldPermissionUniversalIdentifiers": [], "icon": "IconFileDescription", - "id": Any, "isActive": true, "isLabelSyncedWithName": false, "isNullable": true, @@ -793,35 +788,24 @@ exports[`syncApplication should delete old field and create equivalent one when "isSystemSideEffect": false, "isUIEditable": true, "isUnique": false, - "kanbanAggregateOperationViewIds": [], "kanbanAggregateOperationViewUniversalIdentifiers": [], "label": "Description", - "mainGroupByFieldMetadataViewIds": [], "mainGroupByFieldMetadataViewUniversalIdentifiers": [], "morphId": null, "name": "description", - "objectMetadataId": Any, "objectMetadataUniversalIdentifier": Any, "options": null, "overrides": null, - "relationTargetFieldMetadataId": null, "relationTargetFieldMetadataUniversalIdentifier": null, - "relationTargetObjectMetadataId": null, "relationTargetObjectMetadataUniversalIdentifier": null, - "searchFieldMetadataIds": [], "searchFieldMetadataUniversalIdentifiers": [], - "settings": null, "type": "TEXT", "universalIdentifier": Any, "universalSettings": null, "updatedAt": Any, - "viewFieldIds": [], "viewFieldUniversalIdentifiers": [], - "viewFilterIds": [], "viewFilterUniversalIdentifiers": [], - "viewSortIds": [], "viewSortUniversalIdentifiers": [], - "workspaceId": Any, }, "metadataName": "fieldMetadata", "type": "delete",