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 a2157b3c85b..6c6c01a5fce 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 2a1ca954c00..f5e67153bc6 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 bc0e3d9d716..2e77292758c 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 69b28f10ca4..2c255d0a09f 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 c2a7f02f3de..286ad8a781b 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 780d123fdb2..5382ead3fac 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 e056aeb02fe..a75956ee3f3 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 21670d3bd55..8adbc4719ce 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 9d19629d779..f67c6b4be23 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 7730d35f003..66d216a6b43 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 7895f1ca317..d541501cc63 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 4bcc33555a5..42fcd801dc7 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 086bf4c1e81..e728da46b1c 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 c3ad960518d..1a17ef69b77 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 00000000000..ad5c14d7789 --- /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 2b6e7d77cd3..1eb35306789 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 00000000000..7ff2d0e5016 --- /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 623e0698509..96904c554ad 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 fe2b83d9e29..13902ca6cde 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 a6ce72286a3..c3fd22d0dda 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 371070c04bd..de20034a5fd 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 ee1dde3f4d3..8b88c579709 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 7ae3ea566c8..a2b7325faae 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 f3ba8329c36..5b433ac4ab5 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 6e3194cd66a..501a6985d22 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 27759d8e4b1..3fa242c49b2 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 90c21c5e69f..d395dbbee0a 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 5e3b015ad7f..5ece8beffa3 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 8064f7acdd9..a78bd1ffcf6 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 13d5ea969ac..7dc53ac2b47 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 c1edb80eb51..bbcfca7379c 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 f7acff2257e..3e405af3849 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 11f2a134909..67077f3e910 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 fa0dd8103de..f69d829cd1c 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 386b7162ee9..bb143030930 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 143183f0ec7..cd60e64d9ad 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 111580740ad..55d2174af33 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 a0b8852a2b7..db37c5e3742 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 6ae2019f431..de09af4aeee 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 82aa9987cd4..b1cdad517dd 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 656c3063cc9..3c8da258811 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 a0abc73c115..aa3f953e791 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 fe5a298dd8c..85c8c943697 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 8aacc5c4d39..6118c682952 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 00000000000..a3c7a792ddc --- /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 00000000000..9d284c0a27b --- /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 00000000000..0bd9ccce7b9 --- /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 00000000000..17a6610e157 --- /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 00000000000..add5d9f7608 --- /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 00000000000..7f4b3ff4eb6 --- /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 00000000000..6424de80f96 --- /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 a8af6529f36..a2e296d4944 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 a7cd724585d..c3b10ea5c60 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 1f28af8f427..0604c1a258f 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 1829bcbfc03..f4f73e5b6db 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 68fc37f16de..00000000000 --- 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 4d6710092c2..d42dcae8a46 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 f3a5b5a2af2..c946637541f 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 e1014c375da..01e0f28cbc2 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 7248f35c3bb..ec611b94543 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 b093d6d0195..05f0f3a5fb0 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 3231fc9b5ff..4286309c95c 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 0bceff79131..eab4059b427 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 04ce06508d9..b4a1d0f9a22 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 bf0a4c992c1..31b84551852 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 3dd8e55278e..7be53bb9b04 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 1edf083b823..00d4a649ca1 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 e4899fddfbe..d41a28ecd42 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 fc62659f12f..cc2563c7e79 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 8b3145ef900..3222f9fc593 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 eef30805aec..091548dc788 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 f5ccde47d8c..8f992eb2d14 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 c000d61b5b4..5554ae35d29 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 0d876da3c7a..a33a81d099f 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 78fbe8ac9c7..963780d797b 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 cff6391fc84..397be233cee 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 a6e09a94a5c..daf76a568b4 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 656d530ecca..8a5b5632041 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 f5997416bcc..26b51083589 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 8e9f13c6b30..f077f6dd4a8 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 9ea3d9be31a..785537d8f26 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 4fd5f2ded07..bcd5739e47a 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 f48518e713e..dcdc1406cd2 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 048b2572e62..c1ec8dd36a5 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 28003716cc8..1ab12fd520f 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 73471e0298a..775af783b9a 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 02f7e6e2ead..708ea02d181 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 ebcb4ac0211..69d3fbedc22 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 00000000000..7a88499af87 --- /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 00000000000..9a40691d7bc --- /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 00000000000..c9577e91651 --- /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 00000000000..557af6987aa --- /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 00000000000..ffb9c4f51b6 --- /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 00000000000..7a053e1d50b --- /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 00000000000..0b6f76f1567 --- /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 00000000000..7edda6fbb13 --- /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 00000000000..d13d18fadb7 --- /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 00000000000..07f554f332f --- /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 b870b796301..380517ca3e0 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; };