From 2b7b05de2e2e016b8800ddab774d8dce62d84f3f Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:00:24 +0100 Subject: [PATCH] `[OBJECT_MANIFEST_BREAKING_CHANGE]` Sync returns workspace migration (#17918) # Introduction In this PR we start returning a workspace migration post sync so it can committed and provided within the tarball ## Universal aggregators utils Created two utils ### deleteUniversalFlatEntityForeignKeyAggregators Used when building a universal create action, a newly created actions should not contain any aggregated foreign key so they won't be codegen in the workspace migration but also they are overriden at uninversal to flat transpilation anw ### resetUniversalFlatEntityForeignKeyAggregators Used before validating a new flat entity creation, some validator will consume the fk aggregator in order to validate integrity, but of optimstically provided it can result to errors. To avoid caller responsability we override them here ## create-field-action refactor Refactored the universal and flat field create action to be following the base actions in order to ease typing Also it was tailored to handle unlimited amount of flat field metadata in the same actions whereas in the reality we were always only sending at max 2 ( for relation fields ) Note: relation field has to be provided at the same as if not optimistic would fail to retrieve circular universal identifiers ## ObjectManifest Now always expect a `labelIdentifierFieldMetadataUniversalIdentifier` ## Integration test Created an integration test that creates an app, sync a first manifest and a second implying update workspace migration action generation --- .../src/generated-metadata/graphql.ts | 8 +- .../apps/invalid-app/src/first.object.ts | 2 + .../apps/invalid-app/src/second.object.ts | 2 + .../app-dev/expected-manifest.ts | 4 + .../rich-app/src/objects/postCard.object.ts | 2 + .../apps/rich-app/src/root.object.ts | 2 + .../app-dev/expected-manifest.ts | 2 + .../cli/__tests__/apps/root-app/my.object.ts | 2 + .../__tests__/manifest-validate.spec.ts | 1 + .../build/manifest/manifest-build.ts | 19 +- .../sdk/common/types/define-entity.type.ts | 8 +- .../__tests__/define-logic-function.spec.ts | 4 +- .../objects/__tests__/define-object.spec.ts | 30 ++- .../src/sdk/objects/define-object.ts | 21 +- .../src/sdk/objects/object-config.ts | 8 + ...orites-to-navigation-menu-items.command.ts | 2 +- .../dtos/workspace-migration.dto.ts | 12 + .../application-development.resolver.ts | 20 +- .../resolvers/application.resolver.ts | 8 +- .../application-manifest-migration.service.ts | 7 +- .../services/application-sync.service.ts | 22 +- ...-to-universal-flat-object-metadata.util.ts | 3 +- .../ai/ai-agent/agent.service.ts | 6 +- .../command-menu-item.service.ts | 6 +- .../services/field-metadata.service.ts | 6 +- ...ntity-by-metadata-engine-name.test-type.ts | 18 +- .../front-component.service.ts | 6 +- .../logic-function-metadata.service.ts | 6 +- .../navigation-menu-item.service.ts | 6 +- .../navigation-menu-item-deletion.service.ts | 2 +- .../object-metadata.service.ts | 6 +- .../services/page-layout-tab.service.ts | 6 +- .../services/page-layout-widget.service.ts | 2 +- .../page-layout-duplication.service.ts | 2 +- .../services/page-layout-update.service.ts | 2 +- .../services/page-layout.service.ts | 8 +- .../services/role-target.service.ts | 4 +- .../metadata-modules/role/role.service.ts | 6 +- .../row-level-permission-predicate.service.ts | 2 +- .../metadata-modules/skill/skill.service.ts | 10 +- .../services/view-field-group.service.ts | 8 +- .../view-field/services/view-field.service.ts | 8 +- .../services/view-filter-group.service.ts | 9 +- .../services/view-filter.service.ts | 9 +- .../view-group/services/view-group.service.ts | 8 +- .../view/services/view.service.ts | 8 +- .../webhook/webhook.service.ts | 6 +- .../twenty-standard-application.service.ts | 2 +- ...orkspace-migration-action-with-ids.util.ts | 25 +- ...ce-migration-build-orchestrator.service.ts | 3 +- ...igration-validate-build-and-run-service.ts | 36 ++- .../workspace-migration-orchestrator.type.ts | 2 +- ...ggregator-properties.constant.spec.ts.snap | 58 ++++ ...key-aggregator-properties.constant.spec.ts | 14 + ...eign-key-aggregator-properties.constant.ts | 71 +++++ ...ntity-foreign-key-aggregators.util.spec.ts | 36 +++ ...ntity-foreign-key-aggregators.util.spec.ts | 50 ++++ ...lat-entity-foreign-key-aggregators.util.ts | 29 ++ ...lat-entity-foreign-key-aggregators.util.ts | 48 ++++ ...on-fields-into-object-actions.util.spec.ts | 139 +++++----- ...ggregate-relation-field-pairs.util.spec.ts | 247 ++++++++---------- ...elation-fields-into-object-actions.util.ts | 73 ++---- .../aggregate-relation-field-pairs.util.ts | 72 +++-- ...e-field-id-by-universal-identifier.util.ts | 15 -- .../types/workspace-migration-field-action.ts | 29 +- ...migration-field-actions-builder.service.ts | 2 +- .../workspace-migration-object-action.ts | 6 +- ...kspace-entity-migration-builder.service.ts | 46 +++- ...-create-workspace-migration-action.type.ts | 6 +- ...gration.ts => workspace-migration.type.ts} | 1 - .../create-agent-action-handler.service.ts | 7 + ...ommand-menu-item-action-handler.service.ts | 7 + .../create-field-action-handler.service.ts | 46 ++-- ...ld-metadata-to-flat-field-metadata.util.ts | 25 +- ...-front-component-action-handler.service.ts | 7 + ...e-logic-function-action-handler.service.ts | 7 + ...gation-menu-item-action-handler.service.ts | 7 + ...t-metadata-to-flat-object-metadata.util.ts | 19 +- ...-page-layout-tab-action-handler.service.ts | 7 + ...ge-layout-widget-action-handler.service.ts | 7 + ...eate-page-layout-action-handler.service.ts | 7 + ...eate-role-target-action-handler.service.ts | 7 + .../create-role-action-handler.service.ts | 7 + ...-predicate-group-action-handler.service.ts | 7 + ...ission-predicate-action-handler.service.ts | 7 + .../create-skill-action-handler.service.ts | 7 + ...view-field-group-action-handler.service.ts | 10 +- ...reate-view-field-action-handler.service.ts | 7 + ...iew-filter-group-action-handler.service.ts | 7 + ...eate-view-filter-action-handler.service.ts | 7 + ...reate-view-group-action-handler.service.ts | 7 + .../create-view-action-handler.service.ts | 7 + .../create-webhook-action-handler.service.ts | 7 + .../workspace-migration-runner.service.ts | 10 +- ...metadata-events-from-create-action.util.ts | 9 +- ...ate-action-on-all-flat-entity-maps.util.ts | 9 +- ...rkspace-migration.integration-spec.ts.snap | 138 ++++++++++ ...on-workspace-migration.integration-spec.ts | 155 +++++++++++ ...eate-one-application-query-factory.util.ts | 34 +++ .../utils/create-one-application.util.ts | 57 ++++ .../sync-application-query-factory.util.ts | 20 ++ .../utils/sync-application.util.ts | 45 ++++ ...ninstall-application-query-factory.util.ts | 16 ++ .../utils/uninstall-application.util.ts | 39 +++ ...oad-application-file-query-factory.util.ts | 36 +++ .../utils/upload-application-file.util.ts | 65 +++++ .../src/application/objectManifestType.ts | 3 +- 107 files changed, 1662 insertions(+), 566 deletions(-) create mode 100644 packages/twenty-sdk/src/sdk/objects/object-config.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/dtos/workspace-migration.dto.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/delete-universal-flat-entity-foreign-key-aggregators.util.spec.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/reset-universal-flat-entity-foreign-key-aggregators.util.spec.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-foreign-key-aggregators.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/merge-field-id-by-universal-identifier.util.ts rename packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/{workspace-migration.ts => workspace-migration.type.ts} (94%) create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/successful-sync-application-workspace-migration.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/utils/create-one-application-query-factory.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/utils/create-one-application.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/utils/sync-application-query-factory.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/utils/sync-application.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/utils/uninstall-application-query-factory.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/utils/uninstall-application.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/utils/upload-application-file-query-factory.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/utils/upload-application-file.util.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index a2157b3c85..6c6c01a5fc 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -2269,7 +2269,7 @@ export type Mutation = { startChannelSync: ChannelSyncSuccess; switchBillingPlan: BillingUpdateOutput; switchSubscriptionInterval: BillingUpdateOutput; - syncApplication: Scalars['Boolean']; + syncApplication: WorkspaceMigrationDto; trackAnalytics: Analytics; uninstallApplication: Scalars['Boolean']; updateApiKey?: Maybe; @@ -5174,6 +5174,12 @@ export enum WorkspaceMigrationActionType { update = 'update' } +export type WorkspaceMigrationDto = { + __typename?: 'WorkspaceMigrationDTO'; + actions: Scalars['JSON']; + applicationUniversalIdentifier: Scalars['String']; +}; + export type WorkspaceMigrationDeleteActionInput = { metadataName: AllMetadataName; type: WorkspaceMigrationActionType; diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/first.object.ts b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/first.object.ts index 2a1ca954c0..f5e67153bc 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/first.object.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/first.object.ts @@ -10,6 +10,8 @@ export default defineObject({ labelPlural: 'First objects', description: 'First object with duplicate ID', icon: 'IconBox', + labelIdentifierFieldMetadataUniversalIdentifier: + 'first-field-0000-0000-000000000001', fields: [ { universalIdentifier: 'first-field-0000-0000-000000000001', diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/second.object.ts b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/second.object.ts index bc0e3d9d71..2e77292758 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/second.object.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/second.object.ts @@ -10,6 +10,8 @@ export default defineObject({ labelPlural: 'Second objects', description: 'Second object with duplicate ID', icon: 'IconBox', + labelIdentifierFieldMetadataUniversalIdentifier: + 'second-field-0000-0000-000000000001', fields: [ { universalIdentifier: 'second-field-0000-0000-000000000001', diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts index 69b28f10ca..2c255d0a09 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts @@ -123,6 +123,8 @@ export const EXPECTED_MANIFEST: Manifest = { }, ], icon: 'IconNote', + labelIdentifierFieldMetadataUniversalIdentifier: + 'b0b1b2b3-b4b5-4000-8000-000000000002', labelPlural: 'Root notes', labelSingular: 'Root note', namePlural: 'rootNotes', @@ -199,6 +201,8 @@ export const EXPECTED_MANIFEST: Manifest = { }, ], icon: 'IconMail', + labelIdentifierFieldMetadataUniversalIdentifier: + '58a0a314-d7ea-4865-9850-7fb84e72f30b', labelPlural: 'Post cards', labelSingular: 'Post card', namePlural: 'postCards', diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/objects/postCard.object.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/objects/postCard.object.ts index c2a7f02f3d..286ad8a781 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/objects/postCard.object.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/objects/postCard.object.ts @@ -20,6 +20,8 @@ export default defineObject({ labelPlural: 'Post cards', description: 'A post card object', icon: 'IconMail', + labelIdentifierFieldMetadataUniversalIdentifier: + CONTENT_FIELD_UNIVERSAL_IDENTIFIER, fields: [ { universalIdentifier: CONTENT_FIELD_UNIVERSAL_IDENTIFIER, diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.object.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.object.ts index 780d123fdb..5382ead3fa 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.object.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.object.ts @@ -8,6 +8,8 @@ export default defineObject({ labelPlural: 'Root notes', description: 'A simple root-level object', icon: 'IconNote', + labelIdentifierFieldMetadataUniversalIdentifier: + 'b0b1b2b3-b4b5-4000-8000-000000000002', fields: [ { universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000002', diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts index e056aeb02f..a75956ee3f 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts @@ -22,6 +22,8 @@ export const EXPECTED_MANIFEST: Manifest = { labelPlural: 'My notes', description: 'A simple root-level object', icon: 'IconNote', + labelIdentifierFieldMetadataUniversalIdentifier: + 'e1e2e3e4-e5e6-4000-8000-000000000031', fields: [ { universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000031', diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.object.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.object.ts index 21670d3bd5..8adbc4719c 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.object.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.object.ts @@ -8,6 +8,8 @@ export default defineObject({ labelPlural: 'My notes', description: 'A simple root-level object', icon: 'IconNote', + labelIdentifierFieldMetadataUniversalIdentifier: + 'e1e2e3e4-e5e6-4000-8000-000000000031', fields: [ { universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000031', diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts index 9d19629d77..f67c6b4be2 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts @@ -157,6 +157,7 @@ describe('manifestValidate', () => { namePlural: 'myObjects', labelSingular: 'My Object', labelPlural: 'My Objects', + labelIdentifierFieldMetadataUniversalIdentifier: sharedId, fields: [ { universalIdentifier: sharedId, diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts index 7730d35f00..66d216a6b4 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts @@ -10,6 +10,7 @@ import { type FrontComponentConfig, type LogicFunctionConfig, } from '@/sdk'; +import { type ObjectConfig } from '@/sdk/objects/object-config'; import { glob } from 'fast-glob'; import { readFile } from 'fs-extra'; import { basename, extname, relative } from 'path'; @@ -24,8 +25,8 @@ import { type ObjectManifest, type RoleManifest, } from 'twenty-shared/application'; -import { assertUnreachable } from 'twenty-shared/utils'; import { getInputSchemaFromSourceCode } from 'twenty-shared/logic-function'; +import { assertUnreachable } from 'twenty-shared/utils'; const loadSources = async (appPath: string): Promise => { return await glob(['**/*.ts', '**/*.tsx'], { @@ -98,11 +99,23 @@ export const buildManifest = async ( break; } case ManifestEntityKey.Objects: { - const extract = await extractManifestFromFile({ + const extract = await extractManifestFromFile({ appPath, filePath, }); - objects.push(extract.config); + + const { labelIdentifierFieldMetadataUniversalIdentifier, ...rest } = + extract.config; + + const objectManifest: ObjectManifest = { + ...rest, + labelIdentifierFieldMetadataUniversalIdentifier: + // TODO replace by system id universal identifier once we've refactored it + labelIdentifierFieldMetadataUniversalIdentifier ?? '', + }; + + objects.push(objectManifest); + errors.push(...extract.errors); objectsFilePaths.push(relativePath); break; diff --git a/packages/twenty-sdk/src/sdk/common/types/define-entity.type.ts b/packages/twenty-sdk/src/sdk/common/types/define-entity.type.ts index 7895f1ca31..d541501cc6 100644 --- a/packages/twenty-sdk/src/sdk/common/types/define-entity.type.ts +++ b/packages/twenty-sdk/src/sdk/common/types/define-entity.type.ts @@ -1,9 +1,9 @@ import { type ApplicationConfig } from '@/sdk/application/application-config'; import { type FrontComponentConfig } from '@/sdk/front-component-config'; import { type LogicFunctionConfig } from '@/sdk/logic-functions/logic-function-config'; +import { type ObjectConfig } from '@/sdk/objects/object-config'; import { type FieldManifest, - type ObjectManifest, type RoleManifest, } from 'twenty-shared/application'; @@ -15,14 +15,12 @@ export type ValidationResult = { export type DefinableEntity = | ApplicationConfig - | ObjectManifest + | ObjectConfig | FieldManifest | FrontComponentConfig | LogicFunctionConfig | RoleManifest; -export type DefineEntity = < - T extends C, ->( +export type DefineEntity = ( config: T, ) => ValidationResult; diff --git a/packages/twenty-sdk/src/sdk/logic-functions/__tests__/define-logic-function.spec.ts b/packages/twenty-sdk/src/sdk/logic-functions/__tests__/define-logic-function.spec.ts index 4bcc33555a..42fcd801dc 100644 --- a/packages/twenty-sdk/src/sdk/logic-functions/__tests__/define-logic-function.spec.ts +++ b/packages/twenty-sdk/src/sdk/logic-functions/__tests__/define-logic-function.spec.ts @@ -34,7 +34,7 @@ describe('defineLogicFunction', () => { const result = defineLogicFunction(config as any); - expect(result.config.cronTriggerSettings.pattern).toBeDefined(); + expect(result.config.cronTriggerSettings?.pattern).toBeDefined(); }); it('should accept databaseEventTriggerSettings', () => { @@ -49,7 +49,7 @@ describe('defineLogicFunction', () => { const result = defineLogicFunction(config as any); - expect(result.config.databaseEventTriggerSettings.eventName).toBeDefined(); + expect(result.config.databaseEventTriggerSettings?.eventName).toBeDefined(); }); it('should pass through optional fields', () => { diff --git a/packages/twenty-sdk/src/sdk/objects/__tests__/define-object.spec.ts b/packages/twenty-sdk/src/sdk/objects/__tests__/define-object.spec.ts index 086bf4c1e8..e728da46b1 100644 --- a/packages/twenty-sdk/src/sdk/objects/__tests__/define-object.spec.ts +++ b/packages/twenty-sdk/src/sdk/objects/__tests__/define-object.spec.ts @@ -1,6 +1,6 @@ import { defineObject } from '@/sdk'; -import { FieldMetadataType } from 'twenty-shared/types'; import { type ObjectManifest } from 'twenty-shared/application'; +import { FieldMetadataType } from 'twenty-shared/types'; describe('defineObject', () => { const validConfig: ObjectManifest = { @@ -10,6 +10,8 @@ describe('defineObject', () => { labelSingular: 'Post Card', labelPlural: 'Post Cards', icon: 'IconMail', + labelIdentifierFieldMetadataUniversalIdentifier: + '58a0a314-d7ea-4865-9850-7fb84e72f30b', fields: [ { universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b', @@ -111,17 +113,6 @@ describe('defineObject', () => { expect(result.config.fields).toEqual([]); }); - it('should accept missing fields', () => { - const config = { - ...validConfig, - fields: undefined, - }; - - const result = defineObject(config as any); - - expect(result.config.fields).toBeUndefined(); - }); - it('should return error when field is missing label', () => { const config = { ...validConfig, @@ -239,4 +230,19 @@ describe('defineObject', () => { expect(result.config.fields[0].options).toHaveLength(2); }); + + it('should return error when labelIdentifierFieldMetadataUniversalIdentifier references non-existent field', () => { + const config: ObjectManifest = { + ...validConfig, + labelIdentifierFieldMetadataUniversalIdentifier: + 'non-existent-field-uuid', + }; + + const result = defineObject(config); + + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'labelIdentifierFieldMetadataUniversalIdentifier must reference a field defined in the fields array', + ); + }); }); diff --git a/packages/twenty-sdk/src/sdk/objects/define-object.ts b/packages/twenty-sdk/src/sdk/objects/define-object.ts index c3ad960518..1a17ef69b7 100644 --- a/packages/twenty-sdk/src/sdk/objects/define-object.ts +++ b/packages/twenty-sdk/src/sdk/objects/define-object.ts @@ -1,10 +1,10 @@ -import { type ObjectManifest } from 'twenty-shared/application'; - -import { createValidationResult } from '@/sdk/common/utils/create-validation-result'; import { type DefineEntity } from '@/sdk/common/types/define-entity.type'; +import { createValidationResult } from '@/sdk/common/utils/create-validation-result'; import { validateFields } from '@/sdk/fields/validate-fields'; +import { type ObjectConfig } from '@/sdk/objects/object-config'; +import { isDefined } from 'twenty-shared/utils'; -export const defineObject: DefineEntity = (config) => { +export const defineObject: DefineEntity = (config) => { const errors = []; if (!config.universalIdentifier) { @@ -31,6 +31,19 @@ export const defineObject: DefineEntity = (config) => { errors.push(...fieldErrors); + if ( + isDefined(config.labelIdentifierFieldMetadataUniversalIdentifier) && + !config.fields.some( + (field) => + field.universalIdentifier === + config.labelIdentifierFieldMetadataUniversalIdentifier, + ) + ) { + errors.push( + 'labelIdentifierFieldMetadataUniversalIdentifier must reference a field defined in the fields array', + ); + } + return createValidationResult({ config, errors, diff --git a/packages/twenty-sdk/src/sdk/objects/object-config.ts b/packages/twenty-sdk/src/sdk/objects/object-config.ts new file mode 100644 index 0000000000..ad5c14d778 --- /dev/null +++ b/packages/twenty-sdk/src/sdk/objects/object-config.ts @@ -0,0 +1,8 @@ +import { type ObjectManifest } from 'twenty-shared/application'; + +export type ObjectConfig = Omit< + ObjectManifest, + 'labelIdentifierFieldMetadataUniversalIdentifier' +> & { + labelIdentifierFieldMetadataUniversalIdentifier?: string; +}; diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-favorites-to-navigation-menu-items.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-favorites-to-navigation-menu-items.command.ts index 2b6e7d77cd..1eb3530678 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-favorites-to-navigation-menu-items.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-favorites-to-navigation-menu-items.command.ts @@ -753,7 +753,7 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new Error( `Failed to create navigation menu items: ${JSON.stringify(validateAndBuildResult, null, 2)}`, ); diff --git a/packages/twenty-server/src/engine/core-modules/application/dtos/workspace-migration.dto.ts b/packages/twenty-server/src/engine/core-modules/application/dtos/workspace-migration.dto.ts new file mode 100644 index 0000000000..7ff2d0e501 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/dtos/workspace-migration.dto.ts @@ -0,0 +1,12 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import GraphQLJSON from 'graphql-type-json'; + +@ObjectType() +export class WorkspaceMigrationDTO { + @Field(() => String) + applicationUniversalIdentifier: string; + + @Field(() => GraphQLJSON) + actions: unknown[]; +} diff --git a/packages/twenty-server/src/engine/core-modules/application/resolvers/application-development.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/resolvers/application-development.resolver.ts index 623e069850..96904c554a 100644 --- a/packages/twenty-server/src/engine/core-modules/application/resolvers/application-development.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/resolvers/application-development.resolver.ts @@ -23,6 +23,7 @@ import { ApplicationInput } from 'src/engine/core-modules/application/dtos/appli import { CreateApplicationInput } from 'src/engine/core-modules/application/dtos/create-application.input'; import { GenerateApplicationTokenInput } from 'src/engine/core-modules/application/dtos/generate-application-token.input'; import { UploadApplicationFileInput } from 'src/engine/core-modules/application/dtos/uploadApplicationFileInput'; +import { WorkspaceMigrationDTO } from 'src/engine/core-modules/application/dtos/workspace-migration.dto'; import { ApplicationSyncService } from 'src/engine/core-modules/application/services/application-sync.service'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; import { AuthToken } from 'src/engine/core-modules/auth/dto/auth-token.dto'; @@ -69,18 +70,23 @@ export class ApplicationDevelopmentResolver { }); } - @Mutation(() => Boolean) + @Mutation(() => WorkspaceMigrationDTO) @RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED) async syncApplication( @Args() { manifest }: ApplicationInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { - await this.applicationSyncService.synchronizeFromManifest({ - workspaceId, - manifest, - }); + ): Promise { + const workspaceMigration = + await this.applicationSyncService.synchronizeFromManifest({ + workspaceId, + manifest, + }); - return true; + return { + applicationUniversalIdentifier: + workspaceMigration.applicationUniversalIdentifier, + actions: workspaceMigration.actions, + }; } @Mutation(() => ApplicationDTO) diff --git a/packages/twenty-server/src/engine/core-modules/application/resolvers/application.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/resolvers/application.resolver.ts index fe2b83d9e2..13902ca6cd 100644 --- a/packages/twenty-server/src/engine/core-modules/application/resolvers/application.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/resolvers/application.resolver.ts @@ -144,10 +144,12 @@ export class ApplicationResolver { ); await this.workspaceMigrationRunnerService.run({ - actions, + workspaceMigration: { + actions, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, + }, workspaceId, - applicationUniversalIdentifier: - workspaceCustomFlatApplication.universalIdentifier, }); return true; diff --git a/packages/twenty-server/src/engine/core-modules/application/services/application-manifest-migration.service.ts b/packages/twenty-server/src/engine/core-modules/application/services/application-manifest-migration.service.ts index a6ce72286a..c3fd22d0dd 100644 --- a/packages/twenty-server/src/engine/core-modules/application/services/application-manifest-migration.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/services/application-manifest-migration.service.ts @@ -20,6 +20,7 @@ import { PermissionFlagService } from 'src/engine/metadata-modules/permission-fl import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; +import { WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.type'; @Injectable() export class ApplicationManifestMigrationService { @@ -44,7 +45,7 @@ export class ApplicationManifestMigrationService { manifest: Manifest; workspaceId: string; ownerFlatApplication: FlatApplication; - }) { + }): Promise { const now = new Date().toISOString(); const toAllUniversalFlatEntityMaps = @@ -88,7 +89,7 @@ export class ApplicationManifestMigrationService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Validation errors occurred while syncing application manifest metadata', @@ -104,6 +105,8 @@ export class ApplicationManifestMigrationService { workspaceId, ownerFlatApplication, }); + + return validateAndBuildResult.workspaceMigration; } /** diff --git a/packages/twenty-server/src/engine/core-modules/application/services/application-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/services/application-sync.service.ts index 371070c04b..de20034a5f 100644 --- a/packages/twenty-server/src/engine/core-modules/application/services/application-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/services/application-sync.service.ts @@ -23,6 +23,7 @@ import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-e import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; +import { WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.type'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; @Injectable() @@ -43,7 +44,7 @@ export class ApplicationSyncService { manifest, }: ApplicationInput & { workspaceId: string; - }) { + }): Promise { const application = await this.syncApplication({ workspaceId, manifest, @@ -51,13 +52,16 @@ export class ApplicationSyncService { const ownerFlatApplication: FlatApplication = application; - await this.applicationManifestMigrationService.syncMetadataFromManifest({ - manifest, - workspaceId, - ownerFlatApplication, - }); + const workspaceMigration = + await this.applicationManifestMigrationService.syncMetadataFromManifest({ + manifest, + workspaceId, + ownerFlatApplication, + }); this.logger.log('✅ Application sync from manifest completed'); + + return workspaceMigration; } private async syncApplication({ @@ -129,7 +133,7 @@ export class ApplicationSyncService { }: { workspaceId: string; applicationUniversalIdentifier: string; - }) { + }): Promise { const application = await this.applicationService.findByUniversalIdentifier( { universalIdentifier: applicationUniversalIdentifier, workspaceId }, ); @@ -180,7 +184,7 @@ export class ApplicationSyncService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Validation errors occurred while uninstalling application', @@ -191,5 +195,7 @@ export class ApplicationSyncService { applicationUniversalIdentifier, workspaceId, ); + + return validateAndBuildResult.workspaceMigration; } } diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/from-object-manifest-to-universal-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/from-object-manifest-to-universal-flat-object-metadata.util.ts index ee1dde3f4d..8b88c57970 100644 --- a/packages/twenty-server/src/engine/core-modules/application/utils/from-object-manifest-to-universal-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/utils/from-object-manifest-to-universal-flat-object-metadata.util.ts @@ -35,7 +35,8 @@ export const fromObjectManifestToUniversalFlatObjectMetadata = ({ fieldUniversalIdentifiers: [], indexMetadataUniversalIdentifiers: [], viewUniversalIdentifiers: [], - labelIdentifierFieldMetadataUniversalIdentifier: null, + labelIdentifierFieldMetadataUniversalIdentifier: + objectManifest.labelIdentifierFieldMetadataUniversalIdentifier, imageIdentifierFieldMetadataUniversalIdentifier: null, createdAt: now, updatedAt: now, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/agent.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/agent.service.ts index 7ae3ea566c..a2b7325faa 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/agent.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/agent.service.ts @@ -158,7 +158,7 @@ export class AgentService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while creating agent', @@ -242,7 +242,7 @@ export class AgentService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while updating agent', @@ -357,7 +357,7 @@ export class AgentService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, `Multiple validation errors occurred while deleting agent${ids.length > 1 ? 's' : ''}`, diff --git a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.service.ts b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.service.ts index f3ba8329c3..5b433ac4ab 100644 --- a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.service.ts @@ -127,7 +127,7 @@ export class CommandMenuItemService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while creating command menu item', @@ -194,7 +194,7 @@ export class CommandMenuItemService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while updating command menu item', @@ -254,7 +254,7 @@ export class CommandMenuItemService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while deleting command menu item', diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts index 6e3194cd66..501a6985d2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts @@ -141,7 +141,7 @@ export class FieldMetadataService extends TypeOrmQueryService, - Expect< - Equal< - BaseUniversalCreateWorkspaceMigrationAction<'objectMetadata'>['flatEntity'], - UniversalFlatObjectMetadata - > - >, - Expect< - Equal< - BaseFlatCreateWorkspaceMigrationAction<'view'>['flatEntity'], - FlatView - > - >, ]; diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.service.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.service.ts index 27759d8e4b..3fa242c49b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.service.ts @@ -114,7 +114,7 @@ export class FrontComponentService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while creating front component', @@ -185,7 +185,7 @@ export class FrontComponentService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while updating front component', @@ -262,7 +262,7 @@ export class FrontComponentService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while destroying front component', diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function/services/logic-function-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function/services/logic-function-metadata.service.ts index 90c21c5e69..d395dbbee0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/logic-function/services/logic-function-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function/services/logic-function-metadata.service.ts @@ -118,7 +118,7 @@ export class LogicFunctionMetadataService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while creating logic function', @@ -178,7 +178,7 @@ export class LogicFunctionMetadataService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while updating logic function', @@ -267,7 +267,7 @@ export class LogicFunctionMetadataService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while destroying logic function', diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts index 5e3b015ad7..5ece8beffa 100644 --- a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts @@ -198,7 +198,7 @@ export class NavigationMenuItemService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while creating navigation menu item', @@ -287,7 +287,7 @@ export class NavigationMenuItemService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while updating navigation menu item', @@ -367,7 +367,7 @@ export class NavigationMenuItemService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while deleting navigation menu item', diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-deletion.service.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-deletion.service.ts index 8064f7acdd..a78bd1ffcf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-deletion.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-deletion.service.ts @@ -74,7 +74,7 @@ export class NavigationMenuItemDeletionService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while deleting navigation menu items for deleted records', diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 13d5ea969a..7dc53ac2b4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -158,7 +158,7 @@ export class ObjectMetadataService extends TypeOrmQueryService 1 ? 's' : ''}`, @@ -560,7 +560,7 @@ export class ObjectMetadataService extends TypeOrmQueryService 1 ? 's' : ''}`, diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service.ts index c1edb80eb5..bbcfca7379 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service.ts @@ -540,7 +540,7 @@ export class RowLevelPermissionPredicateService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Validation errors occurred while upserting row level permission predicates', diff --git a/packages/twenty-server/src/engine/metadata-modules/skill/skill.service.ts b/packages/twenty-server/src/engine/metadata-modules/skill/skill.service.ts index f7acff2257..3e405af384 100644 --- a/packages/twenty-server/src/engine/metadata-modules/skill/skill.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/skill/skill.service.ts @@ -97,7 +97,7 @@ export class SkillService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while creating skill', @@ -159,7 +159,7 @@ export class SkillService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while updating skill', @@ -218,7 +218,7 @@ export class SkillService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while deleting skill', @@ -308,7 +308,7 @@ export class SkillService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while activating skill', @@ -373,7 +373,7 @@ export class SkillService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while deactivating skill', diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field-group/services/view-field-group.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-field-group/services/view-field-group.service.ts index 11f2a13490..67077f3e91 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field-group/services/view-field-group.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field-group/services/view-field-group.service.ts @@ -110,7 +110,7 @@ export class ViewFieldGroupService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while creating view field groups', @@ -176,7 +176,7 @@ export class ViewFieldGroupService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while updating view field group', @@ -247,7 +247,7 @@ export class ViewFieldGroupService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while deleting view field group', @@ -322,7 +322,7 @@ export class ViewFieldGroupService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while destroying view field group', diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field.service.ts index fa0dd8103d..f69d829cd1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field.service.ts @@ -116,7 +116,7 @@ export class ViewFieldService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while creating view fields', @@ -186,7 +186,7 @@ export class ViewFieldService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while updating view field', @@ -254,7 +254,7 @@ export class ViewFieldService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while deleting view field', @@ -328,7 +328,7 @@ export class ViewFieldService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while deleting view field', diff --git a/packages/twenty-server/src/engine/metadata-modules/view-filter-group/services/view-filter-group.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-filter-group/services/view-filter-group.service.ts index 386b7162ee..bb14303093 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-filter-group/services/view-filter-group.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-filter-group/services/view-filter-group.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { isDefined } from 'twenty-shared/utils'; import { IsNull, type Repository } from 'typeorm'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; @@ -82,7 +81,7 @@ export class ViewFilterGroupService { }, ); - if (isDefined(buildAndRunResult)) { + if (buildAndRunResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( buildAndRunResult, 'Multiple validation errors occurred while creating view filter group', @@ -154,7 +153,7 @@ export class ViewFilterGroupService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while updating view filter group', @@ -227,7 +226,7 @@ export class ViewFilterGroupService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while deleting view filter group', @@ -305,7 +304,7 @@ export class ViewFilterGroupService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while destroying view filter group', diff --git a/packages/twenty-server/src/engine/metadata-modules/view-filter/services/view-filter.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-filter/services/view-filter.service.ts index 143183f0ec..cd60e64d9a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-filter/services/view-filter.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-filter/services/view-filter.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { isDefined } from 'twenty-shared/utils'; import { IsNull, Repository } from 'typeorm'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; @@ -84,7 +83,7 @@ export class ViewFilterService { }, ); - if (isDefined(buildAndRunResult)) { + if (buildAndRunResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( buildAndRunResult, 'Multiple validation errors occurred while creating view filter', @@ -161,7 +160,7 @@ export class ViewFilterService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while updating view filter', @@ -232,7 +231,7 @@ export class ViewFilterService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while deleting view filter', @@ -306,7 +305,7 @@ export class ViewFilterService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while destroying view filter', diff --git a/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group.service.ts index 111580740a..55d2174af3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group.service.ts @@ -124,7 +124,7 @@ export class ViewGroupService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while creating view groups', @@ -190,7 +190,7 @@ export class ViewGroupService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while updating view group', @@ -261,7 +261,7 @@ export class ViewGroupService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while deleting view group', @@ -335,7 +335,7 @@ export class ViewGroupService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while destroying view group', diff --git a/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts b/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts index a0b8852a2b..db37c5e374 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts @@ -96,7 +96,7 @@ export class ViewService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while creating view', @@ -181,7 +181,7 @@ export class ViewService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while updating view', @@ -249,7 +249,7 @@ export class ViewService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while deleting view', @@ -321,7 +321,7 @@ export class ViewService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while destroying view', diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.service.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.service.ts index 6ae2019f43..de09af4aee 100644 --- a/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.service.ts @@ -130,7 +130,7 @@ export class WebhookService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while creating webhook', @@ -202,7 +202,7 @@ export class WebhookService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while updating webhook', @@ -260,7 +260,7 @@ export class WebhookService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while deleting webhook', diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service.ts index 82aa9987cd..b1cdad517d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service.ts @@ -125,7 +125,7 @@ export class TwentyStandardApplicationService { }, ); - if (isDefined(validateAndBuildResult)) { + if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while synchronizing twenty-standard application', diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts index 656c3063cc..3c8da25881 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts @@ -1,20 +1,19 @@ import { type AllMetadataName } from 'twenty-shared/metadata'; import { assertUnreachable, isDefined } from 'twenty-shared/utils'; -import { type UniversalCreateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action'; import { type UniversalCreateObjectAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action'; import { type UniversalCreatePageLayoutAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/types/workspace-migration-page-layout-action.type'; -import { type WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration'; +import { type WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.type'; export type IdByUniversalIdentifierByMetadataName = { [P in AllMetadataName]?: Record; }; -const buildFieldIdByUniversalIdentifier = ({ +const buildFieldIdByUniversalIdentifierForObjectAction = ({ action, fieldMetadataIdByUniversalIdentifier, }: { - action: UniversalCreateObjectAction | UniversalCreateFieldAction; + action: UniversalCreateObjectAction; fieldMetadataIdByUniversalIdentifier: Record; }): Record | undefined => { const fieldIdByUniversalIdentifier = { @@ -96,7 +95,7 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({ const fieldIdByUniversalIdentifier = isDefined( fieldMetadataIdByUniversalIdentifier, ) - ? buildFieldIdByUniversalIdentifier({ + ? buildFieldIdByUniversalIdentifierForObjectAction({ action, fieldMetadataIdByUniversalIdentifier, }) @@ -113,12 +112,20 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({ return action; } + const relatedFieldId = isDefined( + action.relatedUniversalFlatFieldMetadata, + ) + ? fieldMetadataIdByUniversalIdentifier[ + action.relatedUniversalFlatFieldMetadata.universalIdentifier + ] + : undefined; + return { ...action, - fieldIdByUniversalIdentifier: buildFieldIdByUniversalIdentifier({ - action, - fieldMetadataIdByUniversalIdentifier, - }), + id: fieldMetadataIdByUniversalIdentifier[ + action.flatEntity.universalIdentifier + ], + relatedFieldId, }; } case 'pageLayout': { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts index a0abc73c11..aa3f953e79 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts @@ -30,8 +30,8 @@ import { WorkspaceMigrationRoleActionsBuilderService } from 'src/engine/workspac import { WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate-group/workspace-migration-row-level-permission-predicate-group-actions-builder.service'; import { WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate/workspace-migration-row-level-permission-predicate-actions-builder.service'; import { WorkspaceMigrationSkillActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/skill/workspace-migration-skill-actions-builder.service'; -import { WorkspaceMigrationViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/workspace-migration-view-field-actions-builder.service'; import { WorkspaceMigrationViewFieldGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/workspace-migration-view-field-group-actions-builder.service'; +import { WorkspaceMigrationViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/workspace-migration-view-field-actions-builder.service'; import { WorkspaceMigrationViewFilterGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter-group/workspace-migration-view-filter-group-actions-builder.service'; import { WorkspaceMigrationViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter/workspace-migration-view-filter-actions-builder.service'; import { WorkspaceMigrationViewGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-group/workspace-migration-view-group-actions-builder.service'; @@ -1138,7 +1138,6 @@ export class WorkspaceMigrationBuildOrchestratorService { ...aggregatedOrchestratorActionsReport.webhook.update, /// ], - workspaceId, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts index fe5a298dd8..85c8c94369 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts @@ -27,6 +27,7 @@ import { FromToAllUniversalFlatEntityMaps, WorkspaceMigrationOrchestratorBuildArgs, WorkspaceMigrationOrchestratorFailedResult, + WorkspaceMigrationOrchestratorSuccessfulResult, } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type'; import { computeUniversalFlatEntityMapsFromTo } from 'src/engine/workspace-manager/workspace-migration/utils/compute-universal-flat-entity-maps-from-to.util'; import { InferDeletionFromMissingEntities } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/infer-deletion-from-missing-entities.type'; @@ -217,7 +218,10 @@ export class WorkspaceMigrationValidateBuildAndRunService { args: WorkspaceMigrationOrchestratorBuildArgs & { idByUniversalIdentifierByMetadataName?: IdByUniversalIdentifierByMetadataName; }, - ) { + ): Promise< + | WorkspaceMigrationOrchestratorFailedResult + | WorkspaceMigrationOrchestratorSuccessfulResult + > { const { idByUniversalIdentifierByMetadataName, ...buildArgs } = args; const validateAndBuildResult = @@ -239,10 +243,6 @@ export class WorkspaceMigrationValidateBuildAndRunService { return validateAndBuildResult; } - if (validateAndBuildResult.workspaceMigration.actions.length === 0) { - return undefined; - } - const workspaceMigration = isDefined(idByUniversalIdentifierByMetadataName) ? enrichCreateWorkspaceMigrationActionsWithIds({ idByUniversalIdentifierByMetadataName, @@ -250,13 +250,24 @@ export class WorkspaceMigrationValidateBuildAndRunService { }) : validateAndBuildResult.workspaceMigration; - const { metadataEvents } = - await this.workspaceMigrationRunnerService.run(workspaceMigration); + if (workspaceMigration.actions.length > 0) { + const { metadataEvents } = await this.workspaceMigrationRunnerService.run( + { + workspaceId: args.workspaceId, + workspaceMigration, + }, + ); - this.metadataEventEmitter.emitMetadataEvents({ - metadataEvents, - workspaceId: args.workspaceId, - }); + this.metadataEventEmitter.emitMetadataEvents({ + metadataEvents, + workspaceId: args.workspaceId, + }); + } + + return { + status: 'success', + workspaceMigration, + }; } public async validateBuildAndRunWorkspaceMigration({ @@ -265,7 +276,8 @@ export class WorkspaceMigrationValidateBuildAndRunService { isSystemBuild = false, applicationUniversalIdentifier, }: ValidateBuildAndRunWorkspaceMigrationFromMatriceArgs): Promise< - WorkspaceMigrationOrchestratorFailedResult | undefined + | WorkspaceMigrationOrchestratorFailedResult + | WorkspaceMigrationOrchestratorSuccessfulResult > { const { fromToAllFlatEntityMaps, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type.ts index 8aacc5c4d3..6118c68295 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type.ts @@ -8,8 +8,8 @@ import { import { type WorkspaceMigrationBuilderAdditionalCacheDataMaps } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-builder-additional-cache-data-maps.type'; import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type'; import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type'; -import { type WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration'; import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type'; +import { type WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.type'; export type FromToAllUniversalFlatEntityMaps = { [P in keyof AllUniversalFlatEntityMaps]?: FromTo< diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap new file mode 100644 index 0000000000..a3c7a792dd --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should match snapshot 1`] = ` +{ + "agent": [], + "commandMenuItem": [], + "fieldMetadata": [ + "calendarViewUniversalIdentifiers", + "kanbanAggregateOperationViewUniversalIdentifiers", + "mainGroupByFieldMetadataViewUniversalIdentifiers", + "viewFieldUniversalIdentifiers", + "viewFilterUniversalIdentifiers", + ], + "frontComponent": [], + "index": [], + "logicFunction": [], + "navigationMenuItem": [], + "objectMetadata": [ + "fieldUniversalIdentifiers", + "viewUniversalIdentifiers", + "indexMetadataUniversalIdentifiers", + ], + "pageLayout": [ + "tabUniversalIdentifiers", + ], + "pageLayoutTab": [ + "widgetUniversalIdentifiers", + ], + "pageLayoutWidget": [], + "role": [ + "roleTargetUniversalIdentifiers", + ], + "roleTarget": [], + "rowLevelPermissionPredicate": [], + "rowLevelPermissionPredicateGroup": [ + "childRowLevelPermissionPredicateGroupUniversalIdentifiers", + ], + "skill": [], + "view": [ + "viewFieldUniversalIdentifiers", + "viewFieldGroupUniversalIdentifiers", + "viewFilterUniversalIdentifiers", + "viewGroupUniversalIdentifiers", + "viewFilterGroupUniversalIdentifiers", + ], + "viewField": [], + "viewFieldGroup": [ + "viewFieldUniversalIdentifiers", + ], + "viewFilter": [], + "viewFilterGroup": [ + "viewFilterUniversalIdentifiers", + "childViewFilterGroupUniversalIdentifiers", + ], + "viewGroup": [], + "webhook": [], +} +`; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts new file mode 100644 index 0000000000..9d284c0a27 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts @@ -0,0 +1,14 @@ +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; + +import { ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant'; + +describe('ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES', () => { + it('should match snapshot', () => { + expect( + Object.keys(ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES), + ).toMatchObject(Object.values(ALL_METADATA_NAME)); + expect( + ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES, + ).toMatchSnapshot(); + }); +}); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant.ts new file mode 100644 index 0000000000..0bd9ccce7b --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant.ts @@ -0,0 +1,71 @@ +import { + ALL_METADATA_NAME, + type AllMetadataName, +} from 'twenty-shared/metadata'; +import { isDefined } from 'twenty-shared/utils'; + +import { ALL_UNIVERSAL_METADATA_RELATIONS } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-metadata-relations.constant'; + +type ExtractForeignKeyAggregatorFromManyToOneRelations< + ManyToOneRelations, + TargetMetadataName extends AllMetadataName, +> = { + [K in keyof ManyToOneRelations]: ManyToOneRelations[K] extends { + metadataName: TargetMetadataName; + universalFlatEntityForeignKeyAggregator: infer Agg; + } + ? Agg extends string + ? Agg + : never + : never; +}[keyof ManyToOneRelations]; + +export type ExtractUniversalForeignKeyAggregatorForMetadataName< + T extends AllMetadataName, +> = { + [M in AllMetadataName]: ExtractForeignKeyAggregatorFromManyToOneRelations< + (typeof ALL_UNIVERSAL_METADATA_RELATIONS)[M]['manyToOne'], + T + >; +}[AllMetadataName]; + +type UniversalFlatEntityForeignKeyAggregatorProperties = { + [P in AllMetadataName]: ExtractUniversalForeignKeyAggregatorForMetadataName

[]; +}; + +const computeForeignKeyAggregatorProperties = ( + metadataName: T, +): ExtractUniversalForeignKeyAggregatorForMetadataName[] => { + const aggregatorProperties: ExtractUniversalForeignKeyAggregatorForMetadataName[] = + []; + + for (const relationsEntry of Object.values( + ALL_UNIVERSAL_METADATA_RELATIONS, + )) { + for (const relation of Object.values(relationsEntry.manyToOne)) { + if (!isDefined(relation)) { + continue; + } + + if ( + relation.metadataName === metadataName && + isDefined(relation.universalFlatEntityForeignKeyAggregator) + ) { + aggregatorProperties.push( + relation.universalFlatEntityForeignKeyAggregator as ExtractUniversalForeignKeyAggregatorForMetadataName, + ); + } + } + } + + return aggregatorProperties; +}; + +export const ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES = + Object.values(ALL_METADATA_NAME).reduce( + (acc, metadataName) => ({ + ...acc, + [metadataName]: computeForeignKeyAggregatorProperties(metadataName), + }), + {} as UniversalFlatEntityForeignKeyAggregatorProperties, + ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/delete-universal-flat-entity-foreign-key-aggregators.util.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/delete-universal-flat-entity-foreign-key-aggregators.util.spec.ts new file mode 100644 index 0000000000..17a6610e15 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/delete-universal-flat-entity-foreign-key-aggregators.util.spec.ts @@ -0,0 +1,36 @@ +import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock'; +import { deleteUniversalFlatEntityForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-foreign-key-aggregators.util'; + +describe('deleteUniversalFlatEntityForeignKeyAggregators', () => { + it('should delete all aggregator keys from the entity', () => { + const objectMetadata = getFlatObjectMetadataMock({ + universalIdentifier: 'object-1', + fieldUniversalIdentifiers: ['field-1', 'field-2'], + viewUniversalIdentifiers: ['view-1'], + indexMetadataUniversalIdentifiers: ['index-1'], + }); + + const result = deleteUniversalFlatEntityForeignKeyAggregators({ + universalFlatEntity: objectMetadata, + metadataName: 'objectMetadata', + }); + + expect(result).not.toHaveProperty('fieldUniversalIdentifiers'); + expect(result).not.toHaveProperty('viewUniversalIdentifiers'); + expect(result).not.toHaveProperty('indexMetadataUniversalIdentifiers'); + expect(result.universalIdentifier).toBe('object-1'); + }); + + it('should return the same reference for metadata with no aggregators', () => { + const entity = getFlatObjectMetadataMock({ + universalIdentifier: 'logic-1', + }); + + const result = deleteUniversalFlatEntityForeignKeyAggregators({ + universalFlatEntity: entity, + metadataName: 'logicFunction' as any, + }); + + expect(result).toBe(entity); + }); +}); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/reset-universal-flat-entity-foreign-key-aggregators.util.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/reset-universal-flat-entity-foreign-key-aggregators.util.spec.ts new file mode 100644 index 0000000000..add5d9f760 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/reset-universal-flat-entity-foreign-key-aggregators.util.spec.ts @@ -0,0 +1,50 @@ +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 { resetUniversalFlatEntityForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; + +describe('resetUniversalFlatEntityForeignKeyAggregators', () => { + it('should reset aggregator properties to empty arrays for objectMetadata', () => { + const objectMetadata = getFlatObjectMetadataMock({ + universalIdentifier: 'object-1', + fieldUniversalIdentifiers: ['field-1', 'field-2'], + viewUniversalIdentifiers: ['view-1'], + indexMetadataUniversalIdentifiers: ['index-1'], + }); + + const result = resetUniversalFlatEntityForeignKeyAggregators({ + universalFlatEntity: objectMetadata, + metadataName: 'objectMetadata', + }); + + expect(result).toMatchObject({ + fieldUniversalIdentifiers: [], + viewUniversalIdentifiers: [], + indexMetadataUniversalIdentifiers: [], + }); + }); + + it('should reset aggregator properties to empty arrays for fieldMetadata', () => { + const fieldMetadata = getFlatFieldMetadataMock({ + universalIdentifier: 'field-1', + objectMetadataId: 'object-1', + type: FieldMetadataType.TEXT, + viewFieldUniversalIdentifiers: ['vf-1'], + viewFilterUniversalIdentifiers: ['filter-1'], + }); + + const result = resetUniversalFlatEntityForeignKeyAggregators({ + universalFlatEntity: fieldMetadata, + metadataName: 'fieldMetadata', + }); + + expect(result).toMatchObject({ + viewFieldUniversalIdentifiers: [], + viewFilterUniversalIdentifiers: [], + calendarViewUniversalIdentifiers: [], + kanbanAggregateOperationViewUniversalIdentifiers: [], + mainGroupByFieldMetadataViewUniversalIdentifiers: [], + }); + }); +}); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-foreign-key-aggregators.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-foreign-key-aggregators.util.ts new file mode 100644 index 0000000000..7f4b3ff4eb --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-foreign-key-aggregators.util.ts @@ -0,0 +1,29 @@ +import { type AllMetadataName } from 'twenty-shared/metadata'; + +import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type'; +import { ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant'; + +export const deleteUniversalFlatEntityForeignKeyAggregators = < + T extends AllMetadataName, +>({ + universalFlatEntity, + metadataName, +}: { + universalFlatEntity: MetadataUniversalFlatEntity; + metadataName: T; +}): MetadataUniversalFlatEntity => { + const aggregatorProperties = + ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES[metadataName]; + + if (aggregatorProperties.length === 0) { + return universalFlatEntity; + } + + const result = { ...universalFlatEntity }; + + for (const aggregatorProperty of aggregatorProperties) { + delete (result as Record)[aggregatorProperty]; + } + + return result; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util.ts new file mode 100644 index 0000000000..6424de80f9 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util.ts @@ -0,0 +1,48 @@ +import { type AllMetadataName } from 'twenty-shared/metadata'; + +import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type'; +import { + ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES, + type ExtractUniversalForeignKeyAggregatorForMetadataName, +} from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant'; + +export const getUniversalFlatEntityEmptyForeignKeyAggregators = < + T extends AllMetadataName, +>({ + metadataName, +}: { + metadataName: T; +}) => { + const aggregatorProperties = + ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES[metadataName]; + + const emptyAggregatorsRecord = {} as Record< + ExtractUniversalForeignKeyAggregatorForMetadataName, + string[] + >; + + for (const aggregatorProperty of aggregatorProperties) { + emptyAggregatorsRecord[aggregatorProperty] = []; + } + + return emptyAggregatorsRecord; +}; + +export const resetUniversalFlatEntityForeignKeyAggregators = < + T extends AllMetadataName, +>({ + universalFlatEntity, + metadataName, +}: { + universalFlatEntity: MetadataUniversalFlatEntity; + metadataName: T; +}): MetadataUniversalFlatEntity => { + const overrides = getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName, + }); + + return { + ...universalFlatEntity, + ...overrides, + }; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/__tests__/aggregate-non-relation-fields-into-object-actions.util.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/__tests__/aggregate-non-relation-fields-into-object-actions.util.spec.ts index a8af6529f3..a2e296d494 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/__tests__/aggregate-non-relation-fields-into-object-actions.util.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/__tests__/aggregate-non-relation-fields-into-object-actions.util.spec.ts @@ -9,10 +9,18 @@ import { type UniversalCreateFieldAction } from 'src/engine/workspace-manager/wo import { type UniversalCreateObjectAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action'; describe('aggregateNonRelationFieldsIntoObjectActions', () => { - it('should merge non-relation fields into matching create-object actions', () => { + it('should merge non-relation field into matching create-object action', () => { const objectUniversalId = 'object-1'; const fieldUniversalId = 'field-1'; + const flatFieldMetadata = getFlatFieldMetadataMock({ + universalIdentifier: fieldUniversalId, + objectMetadataId: 'object-metadata-id', + objectMetadataUniversalIdentifier: objectUniversalId, + type: FieldMetadataType.TEXT, + name: 'testField', + }); + const input: OrchestratorActionsReport = { ...createEmptyOrchestratorActionsReport(), objectMetadata: { @@ -36,18 +44,8 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => { { type: 'create', metadataName: 'fieldMetadata', - universalFlatFieldMetadatas: [ - getFlatFieldMetadataMock({ - universalIdentifier: fieldUniversalId, - objectMetadataId: 'object-metadata-id', - objectMetadataUniversalIdentifier: objectUniversalId, - type: FieldMetadataType.TEXT, - name: 'testField', - }), - ], - fieldIdByUniversalIdentifier: { - [fieldUniversalId]: 'generated-field-id', - }, + flatEntity: flatFieldMetadata, + id: 'generated-field-id', } satisfies UniversalCreateFieldAction, ], update: [], @@ -75,11 +73,21 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => { expect(result.fieldMetadata.create).toHaveLength(0); }); - it('should keep relation fields in separate create-field actions', () => { + it('should keep relation field in separate create-field action', () => { const objectUniversalId = 'object-1'; const relationFieldUniversalId = 'relation-field-1'; const targetFieldUniversalId = 'target-field-1'; + const flatRelationFieldMetadata = getFlatFieldMetadataMock({ + universalIdentifier: relationFieldUniversalId, + objectMetadataId: 'object-metadata-id', + objectMetadataUniversalIdentifier: objectUniversalId, + type: FieldMetadataType.RELATION, + name: 'relationField', + relationTargetFieldMetadataUniversalIdentifier: targetFieldUniversalId, + relationTargetObjectMetadataUniversalIdentifier: 'other-object', + }); + const input: OrchestratorActionsReport = { ...createEmptyOrchestratorActionsReport(), objectMetadata: { @@ -103,21 +111,8 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => { { type: 'create', metadataName: 'fieldMetadata', - universalFlatFieldMetadatas: [ - getFlatFieldMetadataMock({ - universalIdentifier: relationFieldUniversalId, - objectMetadataId: 'object-metadata-id', - objectMetadataUniversalIdentifier: objectUniversalId, - type: FieldMetadataType.RELATION, - name: 'relationField', - relationTargetFieldMetadataUniversalIdentifier: - targetFieldUniversalId, - relationTargetObjectMetadataUniversalIdentifier: 'other-object', - }), - ], - fieldIdByUniversalIdentifier: { - [relationFieldUniversalId]: 'generated-relation-field-id', - }, + flatEntity: flatRelationFieldMetadata, + id: 'generated-relation-field-id', } satisfies UniversalCreateFieldAction, ], update: [], @@ -137,17 +132,23 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => { // Relation field should remain in field actions expect(result.fieldMetadata.create).toMatchObject([ { - universalFlatFieldMetadatas: [ - { universalIdentifier: relationFieldUniversalId }, - ], + flatEntity: { universalIdentifier: relationFieldUniversalId }, }, ]); }); - it('should keep fields for existing objects in separate create-field actions', () => { + it('should keep field for existing object in separate create-field action', () => { const existingObjectUniversalId = 'existing-object'; const fieldUniversalId = 'field-for-existing-object'; + const flatFieldMetadata = getFlatFieldMetadataMock({ + universalIdentifier: fieldUniversalId, + objectMetadataId: 'existing-object-metadata-id', + objectMetadataUniversalIdentifier: existingObjectUniversalId, + type: FieldMetadataType.TEXT, + name: 'fieldForExistingObject', + }); + const input: OrchestratorActionsReport = { ...createEmptyOrchestratorActionsReport(), objectMetadata: { @@ -160,18 +161,8 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => { { type: 'create', metadataName: 'fieldMetadata', - universalFlatFieldMetadatas: [ - getFlatFieldMetadataMock({ - universalIdentifier: fieldUniversalId, - objectMetadataId: 'existing-object-metadata-id', - objectMetadataUniversalIdentifier: existingObjectUniversalId, - type: FieldMetadataType.TEXT, - name: 'fieldForExistingObject', - }), - ], - fieldIdByUniversalIdentifier: { - [fieldUniversalId]: 'generated-field-id', - }, + flatEntity: flatFieldMetadata, + id: 'generated-field-id', } satisfies UniversalCreateFieldAction, ], update: [], @@ -189,18 +180,34 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => { // Field should remain in field actions (no matching object to merge into) expect(result.fieldMetadata.create).toMatchObject([ { - universalFlatFieldMetadatas: [ - { universalIdentifier: fieldUniversalId }, - ], + flatEntity: { universalIdentifier: fieldUniversalId }, }, ]); }); - it('should handle mixed relation and non-relation fields', () => { + it('should handle mixed relation and non-relation fields as separate actions', () => { const objectUniversalId = 'object-1'; const textFieldUniversalId = 'text-field'; const relationFieldUniversalId = 'relation-field'; + const flatTextFieldMetadata = getFlatFieldMetadataMock({ + universalIdentifier: textFieldUniversalId, + objectMetadataId: 'object-metadata-id', + objectMetadataUniversalIdentifier: objectUniversalId, + type: FieldMetadataType.TEXT, + name: 'textField', + }); + + const flatRelationFieldMetadata = getFlatFieldMetadataMock({ + universalIdentifier: relationFieldUniversalId, + objectMetadataId: 'object-metadata-id', + objectMetadataUniversalIdentifier: objectUniversalId, + type: FieldMetadataType.RELATION, + name: 'relationField', + relationTargetFieldMetadataUniversalIdentifier: 'target-field', + relationTargetObjectMetadataUniversalIdentifier: 'other-object', + }); + const input: OrchestratorActionsReport = { ...createEmptyOrchestratorActionsReport(), objectMetadata: { @@ -224,28 +231,14 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => { { type: 'create', metadataName: 'fieldMetadata', - universalFlatFieldMetadatas: [ - getFlatFieldMetadataMock({ - universalIdentifier: textFieldUniversalId, - objectMetadataId: 'object-metadata-id', - objectMetadataUniversalIdentifier: objectUniversalId, - type: FieldMetadataType.TEXT, - name: 'textField', - }), - getFlatFieldMetadataMock({ - universalIdentifier: relationFieldUniversalId, - objectMetadataId: 'object-metadata-id', - objectMetadataUniversalIdentifier: objectUniversalId, - type: FieldMetadataType.RELATION, - name: 'relationField', - relationTargetFieldMetadataUniversalIdentifier: 'target-field', - relationTargetObjectMetadataUniversalIdentifier: 'other-object', - }), - ], - fieldIdByUniversalIdentifier: { - [textFieldUniversalId]: 'text-field-id', - [relationFieldUniversalId]: 'relation-field-id', - }, + flatEntity: flatTextFieldMetadata, + id: 'text-field-id', + } satisfies UniversalCreateFieldAction, + { + type: 'create', + metadataName: 'fieldMetadata', + flatEntity: flatRelationFieldMetadata, + id: 'relation-field-id', } satisfies UniversalCreateFieldAction, ], update: [], @@ -269,9 +262,7 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => { // Relation field should remain in field actions expect(result.fieldMetadata.create).toMatchObject([ { - universalFlatFieldMetadatas: [ - { universalIdentifier: relationFieldUniversalId }, - ], + flatEntity: { universalIdentifier: relationFieldUniversalId }, }, ]); }); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/__tests__/aggregate-relation-field-pairs.util.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/__tests__/aggregate-relation-field-pairs.util.spec.ts index a7cd724585..c3b10ea5c6 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/__tests__/aggregate-relation-field-pairs.util.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/__tests__/aggregate-relation-field-pairs.util.spec.ts @@ -7,7 +7,7 @@ import { aggregateRelationFieldPairs } from 'src/engine/workspace-manager/worksp import { type UniversalCreateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action'; describe('aggregateRelationFieldPairs', () => { - it('should bundle relation field pairs into single action with merged fieldIdByUniversalIdentifier', () => { + it('should bundle relation field pairs into single action with relatedFieldId', () => { const attachmentObjectId = 'attachment-object'; const taskObjectId = 'task-object'; const targetTaskFieldId = 'target-task-field'; @@ -20,41 +20,32 @@ describe('aggregateRelationFieldPairs', () => { { type: 'create', metadataName: 'fieldMetadata', - universalFlatFieldMetadatas: [ - getFlatFieldMetadataMock({ - universalIdentifier: targetTaskFieldId, - objectMetadataId: 'attachment-object-metadata-id', - objectMetadataUniversalIdentifier: attachmentObjectId, - type: FieldMetadataType.RELATION, - name: 'targetTask', - relationTargetFieldMetadataUniversalIdentifier: - attachmentsFieldId, - relationTargetObjectMetadataUniversalIdentifier: taskObjectId, - }), - ], - fieldIdByUniversalIdentifier: { - [targetTaskFieldId]: 'target-task-generated-id', - }, + flatEntity: getFlatFieldMetadataMock({ + universalIdentifier: targetTaskFieldId, + objectMetadataId: 'attachment-object-metadata-id', + objectMetadataUniversalIdentifier: attachmentObjectId, + type: FieldMetadataType.RELATION, + name: 'targetTask', + relationTargetFieldMetadataUniversalIdentifier: + attachmentsFieldId, + relationTargetObjectMetadataUniversalIdentifier: taskObjectId, + }), + id: 'target-task-generated-id', } satisfies UniversalCreateFieldAction, { type: 'create', metadataName: 'fieldMetadata', - universalFlatFieldMetadatas: [ - getFlatFieldMetadataMock({ - universalIdentifier: attachmentsFieldId, - objectMetadataId: 'task-object-metadata-id', - objectMetadataUniversalIdentifier: taskObjectId, - type: FieldMetadataType.RELATION, - name: 'attachments', - relationTargetFieldMetadataUniversalIdentifier: - targetTaskFieldId, - relationTargetObjectMetadataUniversalIdentifier: - attachmentObjectId, - }), - ], - fieldIdByUniversalIdentifier: { - [attachmentsFieldId]: 'attachments-generated-id', - }, + flatEntity: getFlatFieldMetadataMock({ + universalIdentifier: attachmentsFieldId, + objectMetadataId: 'task-object-metadata-id', + objectMetadataUniversalIdentifier: taskObjectId, + type: FieldMetadataType.RELATION, + name: 'attachments', + relationTargetFieldMetadataUniversalIdentifier: targetTaskFieldId, + relationTargetObjectMetadataUniversalIdentifier: + attachmentObjectId, + }), + id: 'attachments-generated-id', } satisfies UniversalCreateFieldAction, ], update: [], @@ -66,17 +57,15 @@ describe('aggregateRelationFieldPairs', () => { orchestratorActionsReport: input, }); - // Should be bundled into a single action with both fields and merged ID map + // Should be bundled into a single action with flatEntity and relatedUniversalFlatFieldMetadata expect(result.fieldMetadata.create).toMatchObject([ { - universalFlatFieldMetadatas: [ - { universalIdentifier: targetTaskFieldId }, - { universalIdentifier: attachmentsFieldId }, - ], - fieldIdByUniversalIdentifier: { - [targetTaskFieldId]: 'target-task-generated-id', - [attachmentsFieldId]: 'attachments-generated-id', + flatEntity: { universalIdentifier: targetTaskFieldId }, + id: 'target-task-generated-id', + relatedUniversalFlatFieldMetadata: { + universalIdentifier: attachmentsFieldId, }, + relatedFieldId: 'attachments-generated-id', }, ]); }); @@ -92,18 +81,14 @@ describe('aggregateRelationFieldPairs', () => { { type: 'create', metadataName: 'fieldMetadata', - universalFlatFieldMetadatas: [ - getFlatFieldMetadataMock({ - universalIdentifier: fieldUniversalId, - objectMetadataId: 'some-object-metadata-id', - objectMetadataUniversalIdentifier: objectUniversalId, - type: FieldMetadataType.TEXT, - name: 'standaloneField', - }), - ], - fieldIdByUniversalIdentifier: { - [fieldUniversalId]: 'standalone-field-id', - }, + flatEntity: getFlatFieldMetadataMock({ + universalIdentifier: fieldUniversalId, + objectMetadataId: 'some-object-metadata-id', + objectMetadataUniversalIdentifier: objectUniversalId, + type: FieldMetadataType.TEXT, + name: 'standaloneField', + }), + id: 'standalone-field-id', } satisfies UniversalCreateFieldAction, ], update: [], @@ -115,14 +100,16 @@ describe('aggregateRelationFieldPairs', () => { orchestratorActionsReport: input, }); - // Should remain as single action + // Should remain as single action without relatedUniversalFlatFieldMetadata expect(result.fieldMetadata.create).toMatchObject([ { - universalFlatFieldMetadatas: [ - { universalIdentifier: fieldUniversalId }, - ], + flatEntity: { universalIdentifier: fieldUniversalId }, }, ]); + expect( + result.fieldMetadata.create[0].relatedUniversalFlatFieldMetadata, + ).toBeUndefined(); + expect(result.fieldMetadata.create[0].relatedFieldId).toBeUndefined(); }); it('should handle relation field with target not being created (existing field)', () => { @@ -137,21 +124,17 @@ describe('aggregateRelationFieldPairs', () => { { type: 'create', metadataName: 'fieldMetadata', - universalFlatFieldMetadatas: [ - getFlatFieldMetadataMock({ - universalIdentifier: relationFieldId, - objectMetadataId: 'some-object-metadata-id', - objectMetadataUniversalIdentifier: objectUniversalId, - type: FieldMetadataType.RELATION, - name: 'relationToExisting', - relationTargetFieldMetadataUniversalIdentifier: - existingTargetFieldId, - relationTargetObjectMetadataUniversalIdentifier: 'other-object', - }), - ], - fieldIdByUniversalIdentifier: { - [relationFieldId]: 'new-relation-field-id', - }, + flatEntity: getFlatFieldMetadataMock({ + universalIdentifier: relationFieldId, + objectMetadataId: 'some-object-metadata-id', + objectMetadataUniversalIdentifier: objectUniversalId, + type: FieldMetadataType.RELATION, + name: 'relationToExisting', + relationTargetFieldMetadataUniversalIdentifier: + existingTargetFieldId, + relationTargetObjectMetadataUniversalIdentifier: 'other-object', + }), + id: 'new-relation-field-id', } satisfies UniversalCreateFieldAction, ], update: [], @@ -163,12 +146,16 @@ describe('aggregateRelationFieldPairs', () => { orchestratorActionsReport: input, }); - // Should remain as single action (target not being created) + // Should remain as single action without relatedUniversalFlatFieldMetadata (target not being created) expect(result.fieldMetadata.create).toMatchObject([ { - universalFlatFieldMetadatas: [{ universalIdentifier: relationFieldId }], + flatEntity: { universalIdentifier: relationFieldId }, }, ]); + expect( + result.fieldMetadata.create[0].relatedUniversalFlatFieldMetadata, + ).toBeUndefined(); + expect(result.fieldMetadata.create[0].relatedFieldId).toBeUndefined(); }); it('should handle multiple independent relation pairs', () => { @@ -184,66 +171,58 @@ describe('aggregateRelationFieldPairs', () => { { type: 'create', metadataName: 'fieldMetadata', - universalFlatFieldMetadatas: [ - getFlatFieldMetadataMock({ - universalIdentifier: pair1FieldA, - objectMetadataId: 'object-1-metadata-id', - objectMetadataUniversalIdentifier: 'object-1', - type: FieldMetadataType.RELATION, - name: 'pair1FieldA', - relationTargetFieldMetadataUniversalIdentifier: pair1FieldB, - relationTargetObjectMetadataUniversalIdentifier: 'object-2', - }), - ], - fieldIdByUniversalIdentifier: { [pair1FieldA]: 'id-1a' }, + flatEntity: getFlatFieldMetadataMock({ + universalIdentifier: pair1FieldA, + objectMetadataId: 'object-1-metadata-id', + objectMetadataUniversalIdentifier: 'object-1', + type: FieldMetadataType.RELATION, + name: 'pair1FieldA', + relationTargetFieldMetadataUniversalIdentifier: pair1FieldB, + relationTargetObjectMetadataUniversalIdentifier: 'object-2', + }), + id: 'id-1a', } satisfies UniversalCreateFieldAction, { type: 'create', metadataName: 'fieldMetadata', - universalFlatFieldMetadatas: [ - getFlatFieldMetadataMock({ - universalIdentifier: pair1FieldB, - objectMetadataId: 'object-2-metadata-id', - objectMetadataUniversalIdentifier: 'object-2', - type: FieldMetadataType.RELATION, - name: 'pair1FieldB', - relationTargetFieldMetadataUniversalIdentifier: pair1FieldA, - relationTargetObjectMetadataUniversalIdentifier: 'object-1', - }), - ], - fieldIdByUniversalIdentifier: { [pair1FieldB]: 'id-1b' }, + flatEntity: getFlatFieldMetadataMock({ + universalIdentifier: pair1FieldB, + objectMetadataId: 'object-2-metadata-id', + objectMetadataUniversalIdentifier: 'object-2', + type: FieldMetadataType.RELATION, + name: 'pair1FieldB', + relationTargetFieldMetadataUniversalIdentifier: pair1FieldA, + relationTargetObjectMetadataUniversalIdentifier: 'object-1', + }), + id: 'id-1b', } satisfies UniversalCreateFieldAction, { type: 'create', metadataName: 'fieldMetadata', - universalFlatFieldMetadatas: [ - getFlatFieldMetadataMock({ - universalIdentifier: pair2FieldA, - objectMetadataId: 'object-3-metadata-id', - objectMetadataUniversalIdentifier: 'object-3', - type: FieldMetadataType.RELATION, - name: 'pair2FieldA', - relationTargetFieldMetadataUniversalIdentifier: pair2FieldB, - relationTargetObjectMetadataUniversalIdentifier: 'object-4', - }), - ], - fieldIdByUniversalIdentifier: { [pair2FieldA]: 'id-2a' }, + flatEntity: getFlatFieldMetadataMock({ + universalIdentifier: pair2FieldA, + objectMetadataId: 'object-3-metadata-id', + objectMetadataUniversalIdentifier: 'object-3', + type: FieldMetadataType.RELATION, + name: 'pair2FieldA', + relationTargetFieldMetadataUniversalIdentifier: pair2FieldB, + relationTargetObjectMetadataUniversalIdentifier: 'object-4', + }), + id: 'id-2a', } satisfies UniversalCreateFieldAction, { type: 'create', metadataName: 'fieldMetadata', - universalFlatFieldMetadatas: [ - getFlatFieldMetadataMock({ - universalIdentifier: pair2FieldB, - objectMetadataId: 'object-4-metadata-id', - objectMetadataUniversalIdentifier: 'object-4', - type: FieldMetadataType.RELATION, - name: 'pair2FieldB', - relationTargetFieldMetadataUniversalIdentifier: pair2FieldA, - relationTargetObjectMetadataUniversalIdentifier: 'object-3', - }), - ], - fieldIdByUniversalIdentifier: { [pair2FieldB]: 'id-2b' }, + flatEntity: getFlatFieldMetadataMock({ + universalIdentifier: pair2FieldB, + objectMetadataId: 'object-4-metadata-id', + objectMetadataUniversalIdentifier: 'object-4', + type: FieldMetadataType.RELATION, + name: 'pair2FieldB', + relationTargetFieldMetadataUniversalIdentifier: pair2FieldA, + relationTargetObjectMetadataUniversalIdentifier: 'object-3', + }), + id: 'id-2b', } satisfies UniversalCreateFieldAction, ], update: [], @@ -255,27 +234,23 @@ describe('aggregateRelationFieldPairs', () => { orchestratorActionsReport: input, }); - // Should result in 2 bundled actions (one per pair), each with merged fieldIdByUniversalIdentifier + // Should result in 2 bundled actions (one per pair) expect(result.fieldMetadata.create).toMatchObject([ { - universalFlatFieldMetadatas: [ - { universalIdentifier: pair1FieldA }, - { universalIdentifier: pair1FieldB }, - ], - fieldIdByUniversalIdentifier: { - [pair1FieldA]: 'id-1a', - [pair1FieldB]: 'id-1b', + flatEntity: { universalIdentifier: pair1FieldA }, + id: 'id-1a', + relatedUniversalFlatFieldMetadata: { + universalIdentifier: pair1FieldB, }, + relatedFieldId: 'id-1b', }, { - universalFlatFieldMetadatas: [ - { universalIdentifier: pair2FieldA }, - { universalIdentifier: pair2FieldB }, - ], - fieldIdByUniversalIdentifier: { - [pair2FieldA]: 'id-2a', - [pair2FieldB]: 'id-2b', + flatEntity: { universalIdentifier: pair2FieldA }, + id: 'id-2a', + relatedUniversalFlatFieldMetadata: { + universalIdentifier: pair2FieldB, }, + relatedFieldId: 'id-2b', }, ]); }); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/aggregate-non-relation-fields-into-object-actions.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/aggregate-non-relation-fields-into-object-actions.util.ts index 1f28af8f42..0604c1a258 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/aggregate-non-relation-fields-into-object-actions.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/aggregate-non-relation-fields-into-object-actions.util.ts @@ -1,10 +1,8 @@ import { isDefined } from 'twenty-shared/utils'; -import { isMorphOrRelationUniversalFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; import { type AggregateOrchestratorActionsReportArgs } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-aggregate-orchestrator-actions-report-args.type'; import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type'; -import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; -import { mergeFieldIdByUniversalIdentifier } from 'src/engine/workspace-manager/workspace-migration/utils/merge-field-id-by-universal-identifier.util'; import { type UniversalCreateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action'; import { type UniversalCreateObjectAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action'; @@ -29,64 +27,29 @@ export const aggregateNonRelationFieldsIntoObjectActions = ({ const remainingCreateFieldActions: UniversalCreateFieldAction[] = []; for (const createFieldAction of createFieldActions) { - const nonRelationFields: UniversalFlatFieldMetadata[] = []; - const relationFields: UniversalFlatFieldMetadata[] = []; + const flatFieldMetadata = createFieldAction.flatEntity; - for (const field of createFieldAction.universalFlatFieldMetadatas) { - if (isMorphOrRelationUniversalFlatFieldMetadata(field)) { - relationFields.push(field); - } else { - nonRelationFields.push(field); - } + if (isMorphOrRelationFieldMetadataType(flatFieldMetadata.type)) { + remainingCreateFieldActions.push(createFieldAction); + continue; } - const nonRelationFieldsByObjectUniversalIdentifier = new Map< - string, - UniversalFlatFieldMetadata[] - >(); + const objectUniversalId = + flatFieldMetadata.objectMetadataUniversalIdentifier; + const matchingObjectAction = + createObjectActionByObjectUniversalIdentifier.get(objectUniversalId); - for (const field of nonRelationFields) { - const objectUniversalId = field.objectMetadataUniversalIdentifier; - const existing = - nonRelationFieldsByObjectUniversalIdentifier.get(objectUniversalId); + if (isDefined(matchingObjectAction)) { + matchingObjectAction.universalFlatFieldMetadatas.push(flatFieldMetadata); - if (isDefined(existing)) { - existing.push(field); - } else { - nonRelationFieldsByObjectUniversalIdentifier.set(objectUniversalId, [ - field, - ]); + if (isDefined(createFieldAction.id)) { + matchingObjectAction.fieldIdByUniversalIdentifier = { + ...matchingObjectAction.fieldIdByUniversalIdentifier, + [flatFieldMetadata.universalIdentifier]: createFieldAction.id, + }; } - } - - const unmergedNonRelationFields: UniversalFlatFieldMetadata[] = []; - - for (const [ - objectUniversalId, - fields, - ] of nonRelationFieldsByObjectUniversalIdentifier) { - const matchingObjectAction = - createObjectActionByObjectUniversalIdentifier.get(objectUniversalId); - - if (isDefined(matchingObjectAction)) { - matchingObjectAction.universalFlatFieldMetadatas.push(...fields); - matchingObjectAction.fieldIdByUniversalIdentifier = - mergeFieldIdByUniversalIdentifier( - matchingObjectAction.fieldIdByUniversalIdentifier, - createFieldAction.fieldIdByUniversalIdentifier, - ); - } else { - unmergedNonRelationFields.push(...fields); - } - } - - const remainingFields = [...unmergedNonRelationFields, ...relationFields]; - - if (remainingFields.length > 0) { - remainingCreateFieldActions.push({ - ...createFieldAction, - universalFlatFieldMetadatas: remainingFields, - }); + } else { + remainingCreateFieldActions.push(createFieldAction); } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/aggregate-relation-field-pairs.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/aggregate-relation-field-pairs.util.ts index 1829bcbfc0..f4f73e5b6d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/aggregate-relation-field-pairs.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/aggregate-relation-field-pairs.util.ts @@ -2,29 +2,23 @@ import { isDefined } from 'twenty-shared/utils'; import { type AggregateOrchestratorActionsReportArgs } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-aggregate-orchestrator-actions-report-args.type'; import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type'; -import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; -import { mergeFieldIdByUniversalIdentifier } from 'src/engine/workspace-manager/workspace-migration/utils/merge-field-id-by-universal-identifier.util'; import { type UniversalCreateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action'; -type FieldWithActionContext = { - field: UniversalFlatFieldMetadata; - fieldIdByUniversalIdentifier: Record | undefined; -}; - export const aggregateRelationFieldPairs = ({ orchestratorActionsReport, }: AggregateOrchestratorActionsReportArgs): OrchestratorActionsReport => { const createFieldActions = orchestratorActionsReport.fieldMetadata.create; - const fieldByUniversalIdentifier = new Map(); + const actionByFieldUniversalIdentifier = new Map< + string, + UniversalCreateFieldAction + >(); for (const action of createFieldActions) { - for (const field of action.universalFlatFieldMetadatas) { - fieldByUniversalIdentifier.set(field.universalIdentifier, { - field, - fieldIdByUniversalIdentifier: action.fieldIdByUniversalIdentifier, - }); - } + actionByFieldUniversalIdentifier.set( + action.flatEntity.universalIdentifier, + action, + ); } const processedFieldUniversalIdentifiers = new Set(); @@ -32,45 +26,43 @@ export const aggregateRelationFieldPairs = ({ for (const [ universalIdentifier, - fieldContext, - ] of fieldByUniversalIdentifier) { + action, + ] of actionByFieldUniversalIdentifier) { if (processedFieldUniversalIdentifiers.has(universalIdentifier)) { continue; } - const { field, fieldIdByUniversalIdentifier } = fieldContext; - processedFieldUniversalIdentifiers.add(universalIdentifier); - const fieldsToBundle: UniversalFlatFieldMetadata[] = [field]; - let mergedFieldIdMap = fieldIdByUniversalIdentifier; - const targetUniversalIdentifier = - field.relationTargetFieldMetadataUniversalIdentifier; + action.flatEntity.relationTargetFieldMetadataUniversalIdentifier; - if (isDefined(targetUniversalIdentifier)) { - const targetFieldContext = fieldByUniversalIdentifier.get( - targetUniversalIdentifier, - ); - - if ( - isDefined(targetFieldContext) && - !processedFieldUniversalIdentifiers.has(targetUniversalIdentifier) - ) { - fieldsToBundle.push(targetFieldContext.field); - processedFieldUniversalIdentifiers.add(targetUniversalIdentifier); - mergedFieldIdMap = mergeFieldIdByUniversalIdentifier( - mergedFieldIdMap, - targetFieldContext.fieldIdByUniversalIdentifier, - ); - } + if (!isDefined(targetUniversalIdentifier)) { + aggregatedCreateFieldActions.push(action); + continue; } + const targetAction = actionByFieldUniversalIdentifier.get( + targetUniversalIdentifier, + ); + + if ( + !isDefined(targetAction) || + processedFieldUniversalIdentifiers.has(targetUniversalIdentifier) + ) { + aggregatedCreateFieldActions.push(action); + continue; + } + + processedFieldUniversalIdentifiers.add(targetUniversalIdentifier); + aggregatedCreateFieldActions.push({ type: 'create', metadataName: 'fieldMetadata', - universalFlatFieldMetadatas: fieldsToBundle, - fieldIdByUniversalIdentifier: mergedFieldIdMap, + flatEntity: action.flatEntity, + id: action.id, + relatedUniversalFlatFieldMetadata: targetAction.flatEntity, + relatedFieldId: targetAction.id, }); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/merge-field-id-by-universal-identifier.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/merge-field-id-by-universal-identifier.util.ts deleted file mode 100644 index 68fc37f16d..0000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/merge-field-id-by-universal-identifier.util.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { isDefined } from 'twenty-shared/utils'; - -export const mergeFieldIdByUniversalIdentifier = ( - existing: Record | undefined, - incoming: Record | undefined, -): Record | undefined => { - if (!isDefined(existing) && !isDefined(incoming)) { - return undefined; - } - - return { - ...existing, - ...incoming, - }; -}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action.ts index 4d6710092c..d42dcae8a4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action.ts @@ -1,4 +1,5 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type ExtractUniversalForeignKeyAggregatorForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant'; import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; import { type BaseFlatCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-create-workspace-migration-action.type'; import { type BaseFlatDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-delete-workspace-migration-action.type'; @@ -8,14 +9,16 @@ import { type BaseUniversalDeleteWorkspaceMigrationAction } from 'src/engine/wor import { type BaseUniversalUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-update-workspace-migration-action.type'; // Universal action types (always use universal identifiers) -export type UniversalCreateFieldAction = Omit< - BaseUniversalCreateWorkspaceMigrationAction<'fieldMetadata'>, - 'flatEntity' -> & { - universalFlatFieldMetadatas: UniversalFlatFieldMetadata[]; - // Optional map to provide specific IDs for field creation (for API metadata). - fieldIdByUniversalIdentifier?: Record; -}; +export type UniversalCreateFieldAction = + BaseUniversalCreateWorkspaceMigrationAction<'fieldMetadata'> & { + // For relation fields, the related field on the other side of the relation. + relatedUniversalFlatFieldMetadata?: Omit< + UniversalFlatFieldMetadata, + ExtractUniversalForeignKeyAggregatorForMetadataName<'fieldMetadata'> + >; + // Optional ID for the related field (for API metadata). + relatedFieldId?: string; + }; export type UniversalUpdateFieldAction = BaseUniversalUpdateWorkspaceMigrationAction<'fieldMetadata'>; @@ -24,12 +27,10 @@ export type UniversalDeleteFieldAction = BaseUniversalDeleteWorkspaceMigrationAction<'fieldMetadata'>; // Flat action types (always use entity IDs) -export type FlatCreateFieldAction = Omit< - BaseFlatCreateWorkspaceMigrationAction<'fieldMetadata'>, - 'flatEntity' -> & { - flatFieldMetadatas: FlatFieldMetadata[]; -}; +export type FlatCreateFieldAction = + BaseFlatCreateWorkspaceMigrationAction<'fieldMetadata'> & { + relatedFlatFieldMetadata?: FlatFieldMetadata; + }; export type FlatUpdateFieldAction = BaseFlatUpdateWorkspaceMigrationAction<'fieldMetadata'>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service.ts index f3a5b5a2af..c946637541 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service.ts @@ -44,7 +44,7 @@ export class WorkspaceMigrationFieldActionsBuilderService extends WorkspaceEntit action: { type: 'create', metadataName: 'fieldMetadata', - universalFlatFieldMetadatas: [flatFieldMetadataToValidate], + flatEntity: flatFieldMetadataToValidate, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action.ts index e1014c375d..01e0f28cbc 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action.ts @@ -1,4 +1,5 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type ExtractUniversalForeignKeyAggregatorForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant'; import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; import { type BaseFlatCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-create-workspace-migration-action.type'; import { type BaseFlatDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-delete-workspace-migration-action.type'; @@ -10,7 +11,10 @@ import { type BaseUniversalUpdateWorkspaceMigrationAction } from 'src/engine/wor // Universal action types (always use universal identifiers) export type UniversalCreateObjectAction = BaseUniversalCreateWorkspaceMigrationAction<'objectMetadata'> & { - universalFlatFieldMetadatas: UniversalFlatFieldMetadata[]; + universalFlatFieldMetadatas: Omit< + UniversalFlatFieldMetadata, + ExtractUniversalForeignKeyAggregatorForMetadataName<'fieldMetadata'> + >[]; // Optional map to provide specific IDs for field creation (for API metadata). fieldIdByUniversalIdentifier?: Record; }; 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 7248f35c3b..ec611b9454 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 @@ -10,6 +10,7 @@ import { FlatEntityMapsExceptionCode, } from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; import { MetadataUniversalFlatEntityAndRelatedFlatEntityMapsForValidation } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-and-related-flat-entity-maps-for-validation.type'; +import { MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; import { MetadataValidationRelatedUniversalFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-types.type'; import { MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type'; import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; @@ -18,9 +19,11 @@ import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-e import { WorkspaceMigrationBuilderAdditionalCacheDataMaps } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-builder-additional-cache-data-maps.type'; import { MetadataUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/metadata-universal-flat-entity-maps.type'; 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 { 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'; import { flatEntityDeletedCreatedUpdatedMatrixDispatcher } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/universal-flat-entity-deleted-created-updated-matrix-dispatcher.util'; import { getMetadataEmptyWorkspaceMigrationActionRecord } from 'src/engine/workspace-manager/workspace-migration/utils/get-metadata-empty-workspace-migration-action-record.util'; import { shouldInferDeletionFromMissingEntities } from 'src/engine/workspace-manager/workspace-migration/utils/should-infer-deletion-from-missing-entities.util'; @@ -29,6 +32,7 @@ import { SuccessfulFlatEntityValidateAndBuild } from 'src/engine/workspace-manag import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type'; import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type'; import { UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type'; +import { AllUniversalWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type'; export type ValidateAndBuildArgs = { @@ -111,23 +115,31 @@ export abstract class WorkspaceEntityMigrationBuilderService< 'creation validation', ); for (const flatEntityToCreateUniversalIdentifier in createdFlatEntityMaps.byUniversalIdentifier) { - const flatEntityToCreate = findFlatEntityByUniversalIdentifierOrThrow({ - universalIdentifier: flatEntityToCreateUniversalIdentifier, - flatEntityMaps: createdFlatEntityMaps, - }); + const rawUniversalflatEntityToCreate = + findFlatEntityByUniversalIdentifierOrThrow({ + universalIdentifier: flatEntityToCreateUniversalIdentifier, + flatEntityMaps: createdFlatEntityMaps, + }); - const flatEntityToCreateId = flatEntityToCreate.universalIdentifier; + const universalFlatEntityToCreate = + resetUniversalFlatEntityForeignKeyAggregators({ + metadataName: this.metadataName, + universalFlatEntity: rawUniversalflatEntityToCreate, + }); + + const universalIdentifierToDelete = + universalFlatEntityToCreate.universalIdentifier; deleteUniversalFlatEntityFromUniversalFlatEntityMapsThroughMutationOrThrow( { - universalIdentifierToDelete: flatEntityToCreateId, + universalIdentifierToDelete, universalFlatEntityMapsToMutate: remainingFlatEntityMapsToCreate, }, ); const validationResult = await this.validateFlatEntityCreation({ additionalCacheDataMaps, - flatEntityToValidate: flatEntityToCreate, + flatEntityToValidate: universalFlatEntityToCreate, workspaceId, optimisticFlatEntityMapsAndRelatedFlatEntityMaps, remainingFlatEntityMapsToValidate: remainingFlatEntityMapsToCreate, @@ -141,18 +153,26 @@ export abstract class WorkspaceEntityMigrationBuilderService< addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( { - universalFlatEntity: flatEntityToCreate, + universalFlatEntity: universalFlatEntityToCreate, universalFlatEntityAndRelatedMapsToMutate: optimisticFlatEntityMapsAndRelatedFlatEntityMaps, metadataName: this.metadataName, }, ); - actionsResult.create.push( - ...(Array.isArray(validationResult.action) - ? validationResult.action - : [validationResult.action]), - ); + const formattedNewCreateAction: AllUniversalWorkspaceMigrationAction< + 'create', + typeof this.metadataName + > = { + ...validationResult.action, + flatEntity: deleteUniversalFlatEntityForeignKeyAggregators({ + metadataName: this.metadataName, + universalFlatEntity: validationResult.action + .flatEntity as MetadataFlatEntity, + }), + }; + + actionsResult.create.push(formattedNewCreateAction); } this.logger.timeEnd( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-create-workspace-migration-action.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-create-workspace-migration-action.type.ts index b093d6d019..05f0f3a5fb 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-create-workspace-migration-action.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-create-workspace-migration-action.type.ts @@ -1,12 +1,16 @@ import { type AllMetadataName } from 'twenty-shared/metadata'; import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type'; +import { type ExtractUniversalForeignKeyAggregatorForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant'; import { type WORKSPACE_MIGRATION_ACTION_TYPE } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant'; export type BaseUniversalCreateWorkspaceMigrationAction< T extends AllMetadataName, > = { - flatEntity: MetadataUniversalFlatEntity; + flatEntity: Omit< + MetadataUniversalFlatEntity, + ExtractUniversalForeignKeyAggregatorForMetadataName + >; type: typeof WORKSPACE_MIGRATION_ACTION_TYPE.create; metadataName: T; // Optional ID to use when creating the entity (for API metadata). diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.type.ts similarity index 94% rename from packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.ts rename to packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.type.ts index 3231fc9b5f..4286309c95 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.type.ts @@ -5,6 +5,5 @@ export type WorkspaceMigration< AllUniversalWorkspaceMigrationAction = AllUniversalWorkspaceMigrationAction, > = { actions: TActions[]; - workspaceId: string; applicationUniversalIdentifier: string; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/agent/services/create-agent-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/agent/services/create-agent-action-handler.service.ts index 0bceff7913..eab4059b42 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/agent/services/create-agent-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/agent/services/create-agent-action-handler.service.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { FlatCreateAgentAction, UniversalCreateAgentAction, @@ -27,6 +28,11 @@ export class CreateAgentActionHandlerService extends WorkspaceMigrationRunnerAct flatApplication, workspaceId, }: WorkspaceMigrationActionRunnerArgs): Promise { + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'agent', + }); + return { ...action, flatEntity: { @@ -34,6 +40,7 @@ export class CreateAgentActionHandlerService extends WorkspaceMigrationRunnerAct applicationId: flatApplication.id, id: action.id ?? v4(), workspaceId, + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/create-command-menu-item-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/create-command-menu-item-action-handler.service.ts index 04ce06508d..b4a1d0f9a2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/create-command-menu-item-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/create-command-menu-item-action-handler.service.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; import { FlatCreateCommandMenuItemAction, @@ -36,6 +37,11 @@ export class CreateCommandMenuItemActionHandlerService extends WorkspaceMigratio universalForeignKeyValues: action.flatEntity, }); + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'commandMenuItem', + }); + return { ...action, flatEntity: { @@ -45,6 +51,7 @@ export class CreateCommandMenuItemActionHandlerService extends WorkspaceMigratio applicationId: flatApplication.id, id: action.id ?? v4(), workspaceId, + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/create-field-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/create-field-action-handler.service.ts index bf0a4c992c..31b8455185 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/create-field-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/create-field-action-handler.service.ts @@ -47,40 +47,44 @@ export class CreateFieldActionHandlerService extends WorkspaceMigrationRunnerAct context: WorkspaceMigrationActionRunnerArgs, ): Promise { const { action, allFlatEntityMaps } = context; - const { universalFlatFieldMetadatas, fieldIdByUniversalIdentifier } = - action; const allFieldIdToBeCreatedInActionByUniversalIdentifierMap = new Map< string, string >(); - for (const universalFlatFieldMetadata of universalFlatFieldMetadatas) { - const providedId = - fieldIdByUniversalIdentifier?.[ - universalFlatFieldMetadata.universalIdentifier - ]; + allFieldIdToBeCreatedInActionByUniversalIdentifierMap.set( + action.flatEntity.universalIdentifier, + action.id ?? v4(), + ); + if (isDefined(action.relatedUniversalFlatFieldMetadata)) { allFieldIdToBeCreatedInActionByUniversalIdentifierMap.set( - universalFlatFieldMetadata.universalIdentifier, - providedId ?? v4(), + action.relatedUniversalFlatFieldMetadata.universalIdentifier, + action.relatedFieldId ?? v4(), ); } + const universalFlatFieldMetadatas = isDefined( + action.relatedUniversalFlatFieldMetadata, + ) + ? [action.flatEntity, action.relatedUniversalFlatFieldMetadata] + : [action.flatEntity]; - const flatFieldMetadatas = universalFlatFieldMetadatas.map( - (universalFlatFieldMetadata) => + const [flatFieldMetadata, relatedFlatFieldMetadata] = + universalFlatFieldMetadatas.map((universalFlatFieldMetadata) => fromUniversalFlatFieldMetadataToFlatFieldMetadata({ universalFlatFieldMetadata, allFieldIdToBeCreatedInActionByUniversalIdentifierMap, allFlatEntityMaps, context, }), - ); + ); return { type: action.type, metadataName: action.metadataName, - flatFieldMetadatas, + flatEntity: flatFieldMetadata, + relatedFlatFieldMetadata, }; } @@ -88,11 +92,11 @@ export class CreateFieldActionHandlerService extends WorkspaceMigrationRunnerAct context: WorkspaceMigrationActionRunnerContext, ): Promise { const { queryRunner, flatAction } = context; - const { flatFieldMetadatas } = flatAction; + const { flatEntity, relatedFlatFieldMetadata } = flatAction; await this.insertFlatEntitiesInRepository({ queryRunner, - flatEntities: flatFieldMetadatas, + flatEntities: [flatEntity, relatedFlatFieldMetadata].filter(isDefined), }); } @@ -105,14 +109,14 @@ export class CreateFieldActionHandlerService extends WorkspaceMigrationRunnerAct allFlatEntityMaps: { flatObjectMetadataMaps }, workspaceId, } = context; - const { flatFieldMetadatas } = flatAction; + const { flatEntity, relatedFlatFieldMetadata } = flatAction; - const fieldsByObjectMetadataId = new Map< - string, - typeof flatFieldMetadatas - >(); + const fieldsByObjectMetadataId = new Map(); - for (const flatFieldMetadata of flatFieldMetadatas) { + for (const flatFieldMetadata of [ + flatEntity, + relatedFlatFieldMetadata, + ].filter(isDefined)) { const existingFields = fieldsByObjectMetadataId.get( flatFieldMetadata.objectMetadataId, ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/utils/from-universal-flat-field-metadata-to-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/utils/from-universal-flat-field-metadata-to-flat-field-metadata.util.ts index 3dd8e55278..7be53bb9b0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/utils/from-universal-flat-field-metadata-to-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/utils/from-universal-flat-field-metadata-to-flat-field-metadata.util.ts @@ -2,7 +2,9 @@ import { isDefined } from 'twenty-shared/utils'; import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type ExtractUniversalForeignKeyAggregatorForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant'; import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { type AllUniversalWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; import { findFieldMetadataIdInCreateFieldContext } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/utils/find-field-metadata-id-in-create-field-context.util'; import { fromUniversalSettingsToFlatFieldMetadataSettings } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/utils/from-universal-settings-to-flat-field-metadata-settings.util'; @@ -10,7 +12,10 @@ import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-ma export type FromUniversalFlatFieldMetadataToFlatFieldMetadataArgs = { allFieldIdToBeCreatedInActionByUniversalIdentifierMap: Map; - universalFlatFieldMetadata: UniversalFlatFieldMetadata; + universalFlatFieldMetadata: Omit< + UniversalFlatFieldMetadata, + ExtractUniversalForeignKeyAggregatorForMetadataName<'fieldMetadata'> + >; allFlatEntityMaps: AllFlatEntityMaps; context: Pick< WorkspaceMigrationActionRunnerArgs, @@ -35,13 +40,6 @@ export const fromUniversalFlatFieldMetadataToFlatFieldMetadata = ({ objectMetadataUniversalIdentifier, relationTargetFieldMetadataUniversalIdentifier, relationTargetObjectMetadataUniversalIdentifier, - viewFilterUniversalIdentifiers: _viewFilterUniversalIdentifiers, - viewFieldUniversalIdentifiers: _viewFieldUniversalIdentifiers, - kanbanAggregateOperationViewUniversalIdentifiers: - _kanbanAggregateOperationViewUniversalIdentifiers, - calendarViewUniversalIdentifiers: _calendarViewUniversalIdentifiers, - mainGroupByFieldMetadataViewUniversalIdentifiers: - _mainGroupByFieldMetadataViewUniversalIdentifiers, universalSettings, ...restProperties } = universalFlatFieldMetadata; @@ -107,6 +105,11 @@ export const fromUniversalFlatFieldMetadataToFlatFieldMetadata = ({ flatFieldMetadataMaps: allFlatEntityMaps.flatFieldMetadataMaps, }); + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'fieldMetadata', + }); + return { ...restProperties, settings, @@ -130,10 +133,6 @@ export const fromUniversalFlatFieldMetadataToFlatFieldMetadata = ({ calendarViewIds: [], mainGroupByFieldMetadataViewIds: [], kanbanAggregateOperationViewIds: [], - viewFieldUniversalIdentifiers: [], - viewFilterUniversalIdentifiers: [], - calendarViewUniversalIdentifiers: [], - mainGroupByFieldMetadataViewUniversalIdentifiers: [], - kanbanAggregateOperationViewUniversalIdentifiers: [], + ...emptyUniversalForeignKeyAggregators, }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/create-front-component-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/create-front-component-action-handler.service.ts index 1edf083b82..00d4a649ca 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/create-front-component-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/create-front-component-action-handler.service.ts @@ -11,6 +11,7 @@ import { FrontComponentException, FrontComponentExceptionCode, } from 'src/engine/metadata-modules/front-component/front-component.exception'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { FlatCreateFrontComponentAction, UniversalCreateFrontComponentAction, @@ -34,6 +35,11 @@ export class CreateFrontComponentActionHandlerService extends WorkspaceMigration flatApplication, workspaceId, }: WorkspaceMigrationActionRunnerArgs): Promise { + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'frontComponent', + }); + return { ...action, flatEntity: { @@ -41,6 +47,7 @@ export class CreateFrontComponentActionHandlerService extends WorkspaceMigration applicationId: flatApplication.id, id: action.id ?? v4(), workspaceId, + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/create-logic-function-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/create-logic-function-action-handler.service.ts index e4899fddfb..d41a28ecd4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/create-logic-function-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/create-logic-function-action-handler.service.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { FlatCreateLogicFunctionAction, UniversalCreateLogicFunctionAction, @@ -23,6 +24,11 @@ export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationR flatApplication, workspaceId, }: WorkspaceMigrationActionRunnerArgs): Promise { + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'logicFunction', + }); + return { ...action, flatEntity: { @@ -30,6 +36,7 @@ export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationR applicationId: flatApplication.id, id: action.id ?? v4(), workspaceId, + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/create-navigation-menu-item-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/create-navigation-menu-item-action-handler.service.ts index fc62659f12..cc2563c7e7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/create-navigation-menu-item-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/create-navigation-menu-item-action-handler.service.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; import { FlatCreateNavigationMenuItemAction, @@ -36,6 +37,11 @@ export class CreateNavigationMenuItemActionHandlerService extends WorkspaceMigra universalForeignKeyValues: action.flatEntity, }); + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'navigationMenuItem', + }); + return { ...action, flatEntity: { @@ -46,6 +52,7 @@ export class CreateNavigationMenuItemActionHandlerService extends WorkspaceMigra applicationId: flatApplication.id, id: action.id ?? v4(), workspaceId, + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/utils/from-universal-flat-object-metadata-to-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/utils/from-universal-flat-object-metadata-to-flat-object-metadata.util.ts index 8b3145ef90..3222f9fc59 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/utils/from-universal-flat-object-metadata-to-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/utils/from-universal-flat-object-metadata-to-flat-object-metadata.util.ts @@ -2,12 +2,17 @@ import { isDefined } from 'twenty-shared/utils'; import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { type ExtractUniversalForeignKeyAggregatorForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant'; import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { type AllUniversalWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; export type FromUniversalFlatObjectMetadataToFlatObjectMetadataArgs = { - universalFlatObjectMetadata: UniversalFlatObjectMetadata; + universalFlatObjectMetadata: Omit< + UniversalFlatObjectMetadata, + ExtractUniversalForeignKeyAggregatorForMetadataName<'objectMetadata'> + >; generatedId: string; allFlatEntityMaps: AllFlatEntityMaps; allFieldIdToBeCreatedInActionByUniversalIdentifierMap: Map; @@ -60,9 +65,6 @@ export const fromUniversalFlatObjectMetadataToFlatObjectMetadata = ({ applicationUniversalIdentifier, labelIdentifierFieldMetadataUniversalIdentifier, imageIdentifierFieldMetadataUniversalIdentifier, - viewUniversalIdentifiers: _viewUniversalIdentifiers, - indexMetadataUniversalIdentifiers: _indexMetadataUniversalIdentifiers, - fieldUniversalIdentifiers: _fieldUniversalIdentifiers, ...restProperties } = universalFlatObjectMetadata; @@ -98,6 +100,11 @@ export const fromUniversalFlatObjectMetadataToFlatObjectMetadata = ({ } } + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'objectMetadata', + }); + return { ...restProperties, dataSourceId, @@ -115,8 +122,6 @@ export const fromUniversalFlatObjectMetadataToFlatObjectMetadata = ({ fieldIds: [], viewIds: [], indexMetadataIds: [], - fieldUniversalIdentifiers: [], - viewUniversalIdentifiers: [], - indexMetadataUniversalIdentifiers: [], + ...emptyUniversalForeignKeyAggregators, }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-tab/services/create-page-layout-tab-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-tab/services/create-page-layout-tab-action-handler.service.ts index eef30805ae..091548dc78 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-tab/services/create-page-layout-tab-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-tab/services/create-page-layout-tab-action-handler.service.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; import { FlatCreatePageLayoutTabAction, @@ -35,6 +36,11 @@ export class CreatePageLayoutTabActionHandlerService extends WorkspaceMigrationR universalForeignKeyValues: action.flatEntity, }); + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'pageLayoutTab', + }); + return { ...action, flatEntity: { @@ -44,6 +50,7 @@ export class CreatePageLayoutTabActionHandlerService extends WorkspaceMigrationR id: action.id ?? v4(), workspaceId, widgetIds: [], + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/create-page-layout-widget-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/create-page-layout-widget-action-handler.service.ts index f5ccde47d8..8f992eb2d1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/create-page-layout-widget-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/create-page-layout-widget-action-handler.service.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; import { FlatCreatePageLayoutWidgetAction, @@ -43,6 +44,11 @@ export class CreatePageLayoutWidgetActionHandlerService extends WorkspaceMigrati flatFieldMetadataMaps: allFlatEntityMaps.flatFieldMetadataMaps, }); + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'pageLayoutWidget', + }); + return { ...action, flatEntity: { @@ -53,6 +59,7 @@ export class CreatePageLayoutWidgetActionHandlerService extends WorkspaceMigrati applicationId: flatApplication.id, id: action.id ?? v4(), workspaceId, + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/create-page-layout-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/create-page-layout-action-handler.service.ts index c000d61b5b..5554ae35d2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/create-page-layout-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/create-page-layout-action-handler.service.ts @@ -9,6 +9,7 @@ import { FlatEntityMapsException, FlatEntityMapsExceptionCode, } from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; import { FlatCreatePageLayoutAction, @@ -70,6 +71,11 @@ export class CreatePageLayoutActionHandlerService extends WorkspaceMigrationRunn } } + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'pageLayout', + }); + return { ...action, flatEntity: { @@ -80,6 +86,7 @@ export class CreatePageLayoutActionHandlerService extends WorkspaceMigrationRunn id: action.id ?? v4(), workspaceId, tabIds: [], + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role-target/services/create-role-target-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role-target/services/create-role-target-action-handler.service.ts index 0d876da3c7..a33a81d099 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role-target/services/create-role-target-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role-target/services/create-role-target-action-handler.service.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; import { FlatCreateRoleTargetAction, @@ -31,6 +32,11 @@ export class CreateRoleTargetActionHandlerService extends WorkspaceMigrationRunn universalForeignKeyValues: action.flatEntity, }); + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'roleTarget', + }); + return { ...action, flatEntity: { @@ -39,6 +45,7 @@ export class CreateRoleTargetActionHandlerService extends WorkspaceMigrationRunn applicationId: flatApplication.id, id: action.id ?? v4(), workspaceId, + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role/services/create-role-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role/services/create-role-action-handler.service.ts index 78fbe8ac9c..963780d797 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role/services/create-role-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role/services/create-role-action-handler.service.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { FlatCreateRoleAction, UniversalCreateRoleAction, @@ -27,6 +28,11 @@ export class CreateRoleActionHandlerService extends WorkspaceMigrationRunnerActi flatApplication, workspaceId, }: WorkspaceMigrationActionRunnerArgs): Promise { + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'role', + }); + return { ...action, flatEntity: { @@ -40,6 +46,7 @@ export class CreateRoleActionHandlerService extends WorkspaceMigrationRunnerActi objectPermissionIds: [], permissionFlagIds: [], fieldPermissionIds: [], + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/row-level-permission-predicate-group/services/create-row-level-permission-predicate-group-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/row-level-permission-predicate-group/services/create-row-level-permission-predicate-group-action-handler.service.ts index cff6391fc8..397be233ce 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/row-level-permission-predicate-group/services/create-row-level-permission-predicate-group-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/row-level-permission-predicate-group/services/create-row-level-permission-predicate-group-action-handler.service.ts @@ -6,6 +6,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; import { FlatCreateRowLevelPermissionPredicateGroupAction, @@ -37,6 +38,11 @@ export class CreateRowLevelPermissionPredicateGroupActionHandlerService extends universalForeignKeyValues: action.flatEntity, }); + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'rowLevelPermissionPredicateGroup', + }); + return { ...action, flatEntity: { @@ -49,6 +55,7 @@ export class CreateRowLevelPermissionPredicateGroupActionHandlerService extends workspaceId, rowLevelPermissionPredicateIds: [], childRowLevelPermissionPredicateGroupIds: [], + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/row-level-permission-predicate/services/create-row-level-permission-predicate-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/row-level-permission-predicate/services/create-row-level-permission-predicate-action-handler.service.ts index a6e09a94a5..daf76a568b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/row-level-permission-predicate/services/create-row-level-permission-predicate-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/row-level-permission-predicate/services/create-row-level-permission-predicate-action-handler.service.ts @@ -6,6 +6,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; import { FlatCreateRowLevelPermissionPredicateAction, @@ -39,6 +40,11 @@ export class CreateRowLevelPermissionPredicateActionHandlerService extends Works universalForeignKeyValues: action.flatEntity, }); + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'rowLevelPermissionPredicate', + }); + return { ...action, flatEntity: { @@ -51,6 +57,7 @@ export class CreateRowLevelPermissionPredicateActionHandlerService extends Works applicationId: flatApplication.id, id: action.id ?? v4(), workspaceId, + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/skill/services/create-skill-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/skill/services/create-skill-action-handler.service.ts index 656d530ecc..8a5b563204 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/skill/services/create-skill-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/skill/services/create-skill-action-handler.service.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { FlatCreateSkillAction, UniversalCreateSkillAction, @@ -27,6 +28,11 @@ export class CreateSkillActionHandlerService extends WorkspaceMigrationRunnerAct flatApplication, workspaceId, }: WorkspaceMigrationActionRunnerArgs): Promise { + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'skill', + }); + return { ...action, flatEntity: { @@ -34,6 +40,7 @@ export class CreateSkillActionHandlerService extends WorkspaceMigrationRunnerAct applicationId: flatApplication.id, id: action.id ?? v4(), workspaceId, + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/create-view-field-group-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/create-view-field-group-action-handler.service.ts index f5997416bc..26b5108358 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/create-view-field-group-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/create-view-field-group-action-handler.service.ts @@ -5,6 +5,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; import { FlatCreateViewFieldGroupAction, @@ -32,16 +33,21 @@ export class CreateViewFieldGroupActionHandlerService extends WorkspaceMigration universalForeignKeyValues: action.flatEntity, }); + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'viewFieldGroup', + }); + return { ...action, flatEntity: { ...action.flatEntity, viewId, - viewFieldIds: [], - viewFieldUniversalIdentifiers: [], id: action.id ?? v4(), applicationId: flatApplication.id, workspaceId, + viewFieldIds: [], + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/create-view-field-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/create-view-field-action-handler.service.ts index 8e9f13c6b3..f077f6dd4a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/create-view-field-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/create-view-field-action-handler.service.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; import { FlatCreateViewFieldAction, @@ -36,6 +37,11 @@ export class CreateViewFieldActionHandlerService extends WorkspaceMigrationRunne universalForeignKeyValues: action.flatEntity, }); + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'viewField', + }); + return { ...action, flatEntity: { @@ -46,6 +52,7 @@ export class CreateViewFieldActionHandlerService extends WorkspaceMigrationRunne id: action.id ?? v4(), applicationId: flatApplication.id, workspaceId, + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-filter-group/services/create-view-filter-group-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-filter-group/services/create-view-filter-group-action-handler.service.ts index 9ea3d9be31..785537d8f2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-filter-group/services/create-view-filter-group-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-filter-group/services/create-view-filter-group-action-handler.service.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; import { type FlatCreateViewFilterGroupAction, @@ -32,6 +33,11 @@ export class CreateViewFilterGroupActionHandlerService extends WorkspaceMigratio universalForeignKeyValues: action.flatEntity, }); + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'viewFilterGroup', + }); + return { ...action, flatEntity: { @@ -43,6 +49,7 @@ export class CreateViewFilterGroupActionHandlerService extends WorkspaceMigratio workspaceId, childViewFilterGroupIds: [], viewFilterIds: [], + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-filter/services/create-view-filter-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-filter/services/create-view-filter-action-handler.service.ts index 4fd5f2ded0..bcd5739e47 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-filter/services/create-view-filter-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-filter/services/create-view-filter-action-handler.service.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; import { FlatCreateViewFilterAction, @@ -32,6 +33,11 @@ export class CreateViewFilterActionHandlerService extends WorkspaceMigrationRunn universalForeignKeyValues: action.flatEntity, }); + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'viewFilter', + }); + return { ...action, flatEntity: { @@ -42,6 +48,7 @@ export class CreateViewFilterActionHandlerService extends WorkspaceMigrationRunn id: action.id ?? v4(), applicationId: flatApplication.id, workspaceId, + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-group/services/create-view-group-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-group/services/create-view-group-action-handler.service.ts index f48518e713..dcdc1406cd 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-group/services/create-view-group-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-group/services/create-view-group-action-handler.service.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; import { FlatCreateViewGroupAction, @@ -31,6 +32,11 @@ export class CreateViewGroupActionHandlerService extends WorkspaceMigrationRunne universalForeignKeyValues: action.flatEntity, }); + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'viewGroup', + }); + return { ...action, flatEntity: { @@ -39,6 +45,7 @@ export class CreateViewGroupActionHandlerService extends WorkspaceMigrationRunne id: action.id ?? v4(), applicationId: flatApplication.id, workspaceId, + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service.ts index 048b2572e6..c1ec8dd36a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; import { FlatCreateViewAction, @@ -40,6 +41,11 @@ export class CreateViewActionHandlerService extends WorkspaceMigrationRunnerActi universalForeignKeyValues: action.flatEntity, }); + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'view', + }); + return { ...action, flatEntity: { @@ -56,6 +62,7 @@ export class CreateViewActionHandlerService extends WorkspaceMigrationRunnerActi viewGroupIds: [], viewFilterIds: [], viewFilterGroupIds: [], + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/create-webhook-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/create-webhook-action-handler.service.ts index 28003716cc..1ab12fd520 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/create-webhook-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/create-webhook-action-handler.service.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; import { FlatCreateWebhookAction, UniversalCreateWebhookAction, @@ -27,6 +28,11 @@ export class CreateWebhookActionHandlerService extends WorkspaceMigrationRunnerA flatApplication, workspaceId, }: WorkspaceMigrationActionRunnerArgs): Promise { + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'webhook', + }); + return { ...action, flatEntity: { @@ -34,6 +40,7 @@ export class CreateWebhookActionHandlerService extends WorkspaceMigrationRunnerA applicationId: flatApplication.id, id: action.id ?? v4(), workspaceId, + ...emptyUniversalForeignKeyAggregators, }, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts index 73471e0298..775af783b9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts @@ -15,7 +15,7 @@ import { FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION } from 'src/engine/metadata-modul import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration'; +import { WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.type'; import { WorkspaceMigrationRunnerException, WorkspaceMigrationRunnerExceptionCode, @@ -150,10 +150,12 @@ export class WorkspaceMigrationRunnerService { } run = async ({ - actions, - applicationUniversalIdentifier, + workspaceMigration: { actions, applicationUniversalIdentifier }, workspaceId, - }: WorkspaceMigration): Promise<{ + }: { + workspaceMigration: WorkspaceMigration; + workspaceId: string; + }): Promise<{ allFlatEntityMaps: AllFlatEntityMaps; metadataEvents: MetadataEvent[]; }> => { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts index 02f7e6e2ea..708ea02d18 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts @@ -1,4 +1,4 @@ -import { assertUnreachable } from 'twenty-shared/utils'; +import { assertUnreachable, isDefined } from 'twenty-shared/utils'; import { type AllFlatWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; import { @@ -12,7 +12,12 @@ export const deriveMetadataEventsFromCreateAction = ( ): MetadataEvent[] => { switch (flatAction.metadataName) { case 'fieldMetadata': { - return flatAction.flatFieldMetadatas.map( + const flatFieldMetadatas = [ + flatAction.flatEntity, + flatAction.relatedFlatFieldMetadata, + ].filter(isDefined); + + return flatFieldMetadatas.map( (flatFieldMetadata): CreateMetadataEvent<'fieldMetadata'> => ({ type: 'created', recordId: flatFieldMetadata.id, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts index ebcb4ac021..69d3fbedc2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts @@ -1,4 +1,4 @@ -import { assertUnreachable } from 'twenty-shared/utils'; +import { assertUnreachable, isDefined } from 'twenty-shared/utils'; import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; import { addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-and-related-entity-maps-through-mutation-or-throw.util'; @@ -15,7 +15,12 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = ({ }: OptimisticallyApplyCreateActionOnAllFlatEntityMapsArgs): AllFlatEntityMaps => { switch (flatAction.metadataName) { case 'fieldMetadata': { - flatAction.flatFieldMetadatas.forEach((flatEntity) => + const flatFieldMetadatas = [ + flatAction.flatEntity, + flatAction.relatedFlatFieldMetadata, + ].filter(isDefined); + + flatFieldMetadatas.forEach((flatEntity) => addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({ flatEntity, flatEntityAndRelatedMapsToMutate: allFlatEntityMaps, 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 new file mode 100644 index 0000000000..7a88499af8 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap @@ -0,0 +1,138 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`syncApplication should return workspace migration actions on initial sync then on second sync with field rename and new role 1`] = ` +{ + "syncApplication": { + "actions": [ + { + "flatEntity": { + "applicationUniversalIdentifier": Any, + "createdAt": Any, + "description": "A support ticket", + "duplicateCriteria": null, + "icon": "IconTicket", + "imageIdentifierFieldMetadataUniversalIdentifier": null, + "isActive": true, + "isAuditLogged": true, + "isCustom": true, + "isLabelSyncedWithName": true, + "isRemote": false, + "isSearchable": false, + "isSystem": false, + "isUIReadOnly": false, + "labelIdentifierFieldMetadataUniversalIdentifier": Any, + "labelPlural": "Tickets", + "labelSingular": "Ticket", + "namePlural": "tickets", + "nameSingular": "ticket", + "shortcut": null, + "standardOverrides": null, + "targetTableName": "DEPRECATED", + "universalIdentifier": Any, + "updatedAt": Any, + }, + "metadataName": "objectMetadata", + "type": "create", + "universalFlatFieldMetadatas": [ + { + "applicationUniversalIdentifier": Any, + "createdAt": Any, + "defaultValue": null, + "description": "Ticket description", + "icon": "IconFileDescription", + "isActive": true, + "isCustom": true, + "isLabelSyncedWithName": false, + "isNullable": true, + "isSystem": false, + "isUIReadOnly": false, + "isUnique": false, + "label": "Description", + "morphId": null, + "name": "description", + "objectMetadataUniversalIdentifier": Any, + "options": null, + "relationTargetFieldMetadataUniversalIdentifier": null, + "relationTargetObjectMetadataUniversalIdentifier": null, + "standardOverrides": null, + "type": "TEXT", + "universalIdentifier": Any, + "universalSettings": null, + "updatedAt": Any, + }, + ], + }, + { + "flatEntity": { + "applicationUniversalIdentifier": Any, + "canAccessAllTools": false, + "canBeAssignedToAgents": true, + "canBeAssignedToApiKeys": true, + "canBeAssignedToUsers": true, + "canDestroyAllObjectRecords": false, + "canReadAllObjectRecords": false, + "canSoftDeleteAllObjectRecords": false, + "canUpdateAllObjectRecords": false, + "canUpdateAllSettings": false, + "createdAt": Any, + "description": "A test role", + "icon": null, + "isEditable": true, + "label": "Test Role", + "rowLevelPermissionPredicateGroupUniversalIdentifiers": [], + "rowLevelPermissionPredicateUniversalIdentifiers": [], + "universalIdentifier": Any, + "updatedAt": Any, + }, + "metadataName": "role", + "type": "create", + }, + ], + "applicationUniversalIdentifier": Any, + }, +} +`; + +exports[`syncApplication should return workspace migration actions on initial sync then on second sync with field rename and new role 2`] = ` +{ + "syncApplication": { + "actions": [ + { + "metadataName": "fieldMetadata", + "type": "update", + "universalIdentifier": Any, + "update": { + "label": "Body", + "name": "body", + }, + }, + { + "flatEntity": { + "applicationUniversalIdentifier": Any, + "canAccessAllTools": false, + "canBeAssignedToAgents": true, + "canBeAssignedToApiKeys": true, + "canBeAssignedToUsers": true, + "canDestroyAllObjectRecords": false, + "canReadAllObjectRecords": false, + "canSoftDeleteAllObjectRecords": false, + "canUpdateAllObjectRecords": false, + "canUpdateAllSettings": false, + "createdAt": Any, + "description": "A read-only role", + "icon": null, + "isEditable": true, + "label": "Viewer Role", + "rowLevelPermissionPredicateGroupUniversalIdentifiers": [], + "rowLevelPermissionPredicateUniversalIdentifiers": [], + "universalIdentifier": Any, + "updatedAt": Any, + }, + "metadataName": "role", + "type": "create", + }, + ], + "applicationUniversalIdentifier": Any, + }, +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/application/successful-sync-application-workspace-migration.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/successful-sync-application-workspace-migration.integration-spec.ts new file mode 100644 index 0000000000..9a40691d7b --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/successful-sync-application-workspace-migration.integration-spec.ts @@ -0,0 +1,155 @@ +import { type Manifest } from 'twenty-shared/application'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { v4 as uuidv4 } from 'uuid'; +import { createOneApplication } from 'test/integration/metadata/suites/application/utils/create-one-application.util'; +import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util'; +import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util'; +import { uploadApplicationFile } from 'test/integration/metadata/suites/application/utils/upload-application-file.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; + +const TEST_APP_ID = uuidv4(); +const TEST_ROLE_ID = uuidv4(); +const TEST_SECOND_ROLE_ID = uuidv4(); +const TEST_OBJECT_ID = uuidv4(); +const TEST_FIELD_ID = uuidv4(); + +describe('syncApplication', () => { + let appCreated = false; + + beforeAll(async () => { + await createOneApplication({ + universalIdentifier: TEST_APP_ID, + name: 'Test Application', + description: 'A test application', + version: '1.0.0', + sourcePath: 'test-sync', + expectToFail: false, + }); + + appCreated = true; + + // File upload uses multipart which requires real timers + jest.useRealTimers(); + + const packageJson = JSON.stringify({ + name: 'test-application', + version: '1.0.0', + }); + + await uploadApplicationFile({ + applicationUniversalIdentifier: TEST_APP_ID, + fileFolder: 'Dependencies', + filePath: 'package.json', + fileBuffer: Buffer.from(packageJson), + filename: 'package.json', + expectToFail: false, + }); + + jest.useFakeTimers(); + }, 60000); + + afterAll(async () => { + if (!appCreated) { + return; + } + + await uninstallApplication({ + universalIdentifier: TEST_APP_ID, + expectToFail: false, + }); + }); + + it('should return workspace migration actions on initial sync then on second sync with field rename and new role', async () => { + const initialManifest: Manifest = { + application: { + universalIdentifier: TEST_APP_ID, + defaultRoleUniversalIdentifier: TEST_ROLE_ID, + displayName: 'Test Application', + description: 'A test application for workspace migration', + icon: 'IconTestPipe', + applicationVariables: {}, + packageJsonChecksum: null, + yarnLockChecksum: null, + }, + roles: [ + { + universalIdentifier: TEST_ROLE_ID, + label: 'Test Role', + description: 'A test role', + }, + ], + objects: [ + { + labelIdentifierFieldMetadataUniversalIdentifier: TEST_FIELD_ID, + universalIdentifier: TEST_OBJECT_ID, + nameSingular: 'ticket', + namePlural: 'tickets', + labelSingular: 'Ticket', + labelPlural: 'Tickets', + description: 'A support ticket', + icon: 'IconTicket', + fields: [], + }, + ], + fields: [ + { + universalIdentifier: TEST_FIELD_ID, + type: FieldMetadataType.TEXT, + name: 'description', + label: 'Description', + description: 'Ticket description', + icon: 'IconFileDescription', + objectUniversalIdentifier: TEST_OBJECT_ID, + }, + ], + logicFunctions: [], + frontComponents: [], + publicAssets: [], + }; + + const { data: firstSyncData } = await syncApplication({ + manifest: initialManifest, + expectToFail: false, + }); + + expect(firstSyncData).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny(firstSyncData), + ); + + const updatedManifest: Manifest = { + ...initialManifest, + roles: [ + { + universalIdentifier: TEST_ROLE_ID, + label: 'Test Role', + description: 'A test role', + }, + { + universalIdentifier: TEST_SECOND_ROLE_ID, + label: 'Viewer Role', + description: 'A read-only role', + }, + ], + fields: [ + { + universalIdentifier: TEST_FIELD_ID, + type: FieldMetadataType.TEXT, + name: 'body', + label: 'Body', + description: 'Ticket description', + icon: 'IconFileDescription', + objectUniversalIdentifier: TEST_OBJECT_ID, + }, + ], + }; + + const { data: secondSyncData } = await syncApplication({ + manifest: updatedManifest, + expectToFail: false, + }); + + expect(secondSyncData).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny(secondSyncData), + ); + }, 60000); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/create-one-application-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/create-one-application-query-factory.util.ts new file mode 100644 index 0000000000..c9577e9165 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/create-one-application-query-factory.util.ts @@ -0,0 +1,34 @@ +import gql from 'graphql-tag'; + +export const createOneApplicationQueryFactory = ({ + universalIdentifier, + name, + description, + version, + sourcePath, +}: { + universalIdentifier: string; + name: string; + description?: string; + version: string; + sourcePath: string; +}) => ({ + query: gql` + mutation CreateOneApplication($input: CreateApplicationInput!) { + createOneApplication(input: $input) { + id + universalIdentifier + name + } + } + `, + variables: { + input: { + universalIdentifier, + name, + description, + version, + sourcePath, + }, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/create-one-application.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/create-one-application.util.ts new file mode 100644 index 0000000000..557af6987a --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/create-one-application.util.ts @@ -0,0 +1,57 @@ +import { createOneApplicationQueryFactory } from 'test/integration/metadata/suites/application/utils/create-one-application-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +type CreatedApplication = { + id: string; + universalIdentifier: string; + name: string; +}; + +export const createOneApplication = async ({ + universalIdentifier, + name, + description, + version, + sourcePath, + expectToFail = false, + token, +}: { + universalIdentifier: string; + name: string; + description?: string; + version: string; + sourcePath: string; + expectToFail?: boolean; + token?: string; +}): CommonResponseBody<{ + createOneApplication: CreatedApplication; +}> => { + const graphqlOperation = createOneApplicationQueryFactory({ + universalIdentifier, + name, + description, + version, + sourcePath, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Create one application should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Create one application has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/sync-application-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/sync-application-query-factory.util.ts new file mode 100644 index 0000000000..ffb9c4f51b --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/sync-application-query-factory.util.ts @@ -0,0 +1,20 @@ +import gql from 'graphql-tag'; +import { type Manifest } from 'twenty-shared/application'; + +export const syncApplicationQueryFactory = ({ + manifest, +}: { + manifest: Manifest; +}) => ({ + query: gql` + mutation SyncApplication($manifest: JSON!) { + syncApplication(manifest: $manifest) { + applicationUniversalIdentifier + actions + } + } + `, + variables: { + manifest, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/sync-application.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/sync-application.util.ts new file mode 100644 index 0000000000..7a053e1d50 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/sync-application.util.ts @@ -0,0 +1,45 @@ +import { type Manifest } from 'twenty-shared/application'; +import { syncApplicationQueryFactory } from 'test/integration/metadata/suites/application/utils/sync-application-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +type WorkspaceMigration = { + applicationUniversalIdentifier: string; + actions: unknown[]; +}; + +export const syncApplication = async ({ + manifest, + expectToFail = false, + token, +}: { + manifest: Manifest; + expectToFail?: boolean; + token?: string; +}): CommonResponseBody<{ + syncApplication: WorkspaceMigration; +}> => { + const graphqlOperation = syncApplicationQueryFactory({ + manifest, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Sync application should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Sync application has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/uninstall-application-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/uninstall-application-query-factory.util.ts new file mode 100644 index 0000000000..0b6f76f156 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/uninstall-application-query-factory.util.ts @@ -0,0 +1,16 @@ +import gql from 'graphql-tag'; + +export const uninstallApplicationQueryFactory = ({ + universalIdentifier, +}: { + universalIdentifier: string; +}) => ({ + query: gql` + mutation UninstallApplication($universalIdentifier: String!) { + uninstallApplication(universalIdentifier: $universalIdentifier) + } + `, + variables: { + universalIdentifier, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/uninstall-application.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/uninstall-application.util.ts new file mode 100644 index 0000000000..7edda6fbb1 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/uninstall-application.util.ts @@ -0,0 +1,39 @@ +import { uninstallApplicationQueryFactory } from 'test/integration/metadata/suites/application/utils/uninstall-application-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +export const uninstallApplication = async ({ + universalIdentifier, + expectToFail = false, + token, +}: { + universalIdentifier: string; + expectToFail?: boolean; + token?: string; +}): CommonResponseBody<{ + uninstallApplication: boolean; +}> => { + const graphqlOperation = uninstallApplicationQueryFactory({ + universalIdentifier, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Uninstall application should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Uninstall application has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/upload-application-file-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/upload-application-file-query-factory.util.ts new file mode 100644 index 0000000000..d13d18fadb --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/upload-application-file-query-factory.util.ts @@ -0,0 +1,36 @@ +import gql from 'graphql-tag'; + +export const uploadApplicationFileQueryFactory = ({ + applicationUniversalIdentifier, + fileFolder, + filePath, +}: { + applicationUniversalIdentifier: string; + fileFolder: string; + filePath: string; +}) => ({ + query: gql` + mutation UploadApplicationFile( + $file: Upload! + $applicationUniversalIdentifier: String! + $fileFolder: FileFolder! + $filePath: String! + ) { + uploadApplicationFile( + file: $file + applicationUniversalIdentifier: $applicationUniversalIdentifier + fileFolder: $fileFolder + filePath: $filePath + ) { + id + path + } + } + `, + variables: { + file: null, + applicationUniversalIdentifier, + fileFolder, + filePath, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/upload-application-file.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/upload-application-file.util.ts new file mode 100644 index 0000000000..07f554f332 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/upload-application-file.util.ts @@ -0,0 +1,65 @@ +import { uploadApplicationFileQueryFactory } from 'test/integration/metadata/suites/application/utils/upload-application-file-query-factory.util'; +import { makeMetadataAPIRequestWithFileUpload } from 'test/integration/metadata/suites/utils/make-metadata-api-request-with-file-upload.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +type UploadedFile = { + id: string; + path: string; +}; + +export const uploadApplicationFile = async ({ + applicationUniversalIdentifier, + fileFolder, + filePath, + fileBuffer, + filename, + contentType = 'application/json', + expectToFail = false, + token, +}: { + applicationUniversalIdentifier: string; + fileFolder: string; + filePath: string; + fileBuffer: Buffer; + filename: string; + contentType?: string; + expectToFail?: boolean; + token?: string; +}): CommonResponseBody<{ + uploadApplicationFile: UploadedFile; +}> => { + const graphqlOperation = uploadApplicationFileQueryFactory({ + applicationUniversalIdentifier, + fileFolder, + filePath, + }); + + const response = await makeMetadataAPIRequestWithFileUpload( + graphqlOperation, + { + field: 'file', + buffer: fileBuffer, + filename, + contentType, + }, + token, + ); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Upload application file should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Upload application file has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-shared/src/application/objectManifestType.ts b/packages/twenty-shared/src/application/objectManifestType.ts index b870b79630..380517ca3e 100644 --- a/packages/twenty-shared/src/application/objectManifestType.ts +++ b/packages/twenty-shared/src/application/objectManifestType.ts @@ -1,5 +1,5 @@ -import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType'; import { type ObjectFieldManifest } from '@/application/objectFieldManifest.type'; +import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType'; export type ObjectManifest = SyncableEntityOptions & { nameSingular: string; @@ -9,4 +9,5 @@ export type ObjectManifest = SyncableEntityOptions & { description?: string; icon?: string; fields: ObjectFieldManifest[]; + labelIdentifierFieldMetadataUniversalIdentifier: string; };