From af0af0a237a9fbba0f5c759d5799f221eefc4586 Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:58:06 +0100 Subject: [PATCH] Fix cross app installation (#18151) # Introduction - Fixing app dependencyFlatEntityMaps load ( to load current app + dependent app ) - Fix empty flat entity maps and undefined optimistic override - Refactor the optimistic rendering to be mutation oriented at orchestrator level --- .../application-manifest-migration.service.ts | 22 +- .../services/application-sync.service.ts | 2 +- ...quired-metadata-for-validation.constant.ts | 4 + ...l-metadata-serialized-relation.constant.ts | 3 + .../types/flat-entity-maps.type.ts | 1 + ...ated-metadata-names-for-validation.util.ts | 16 + ...metadata-serialized-relation-names.util.ts | 13 +- ...b-flat-entity-by-ids-maps-or-throw.util.ts | 14 +- ...idate-object-metadata-cross-entity.util.ts | 126 +++++++ ...object-metadata-system-fields-integrity.ts | 123 ------ ...t-metadata-system-fields-integrity.util.ts | 73 ++++ .../twenty-standard-application.service.ts | 1 + ...ce-migration-build-orchestrator.service.ts | 353 ++---------------- ...igration-validate-build-and-run-service.ts | 79 +++- .../workspace-migration-orchestrator.type.ts | 5 +- ...ity-maps-from-to-through-mutation.util.ts} | 18 +- ...ross-entity-transversal-validation.util.ts | 4 +- ...kspace-entity-migration-builder.service.ts | 15 +- ...led-flat-entity-validate-and-build.type.ts | 2 - ...ful-flat-entity-validate-and-build.type.ts | 2 - .../flat-object-metadata-validator.service.ts | 16 - ...rkspace-migration.integration-spec.ts.snap | 63 ++++ ...on-workspace-migration.integration-spec.ts | 55 +++ ...e-object-metadata.integration-spec.ts.snap | 42 +-- 24 files changed, 481 insertions(+), 571 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names-for-validation.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-cross-entity.util.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-system-fields-integrity.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-system-fields-integrity.util.ts rename packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/{compute-universal-flat-entity-maps-from-to.util.ts => compute-universal-flat-entity-maps-from-to-through-mutation.util.ts} (86%) 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 820f95579f2..139a2792518 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 @@ -65,6 +65,11 @@ export class ApplicationManifestMigrationService { const { featureFlagsMap, ...existingAllFlatEntityMaps } = cacheResult; + const fromAllFlatEntityMaps = getApplicationSubAllFlatEntityMaps({ + applicationIds: [ownerFlatApplication.id], + fromAllFlatEntityMaps: existingAllFlatEntityMaps, + }); + const toAllUniversalFlatEntityMaps = computeApplicationManifestAllUniversalFlatEntityMaps({ manifest, @@ -72,20 +77,15 @@ export class ApplicationManifestMigrationService { now, }); - const fromAllFlatEntityMaps = getApplicationSubAllFlatEntityMaps({ - applicationIds: [ownerFlatApplication.id], + const dependencyAllFlatEntityMaps = getApplicationSubAllFlatEntityMaps({ + applicationIds: + ownerFlatApplication.universalIdentifier === + TWENTY_STANDARD_APPLICATION.universalIdentifier + ? [twentyStandardFlatApplication.id] + : [ownerFlatApplication.id, twentyStandardFlatApplication.id], fromAllFlatEntityMaps: existingAllFlatEntityMaps, }); - const dependencyAllFlatEntityMaps = - ownerFlatApplication.universalIdentifier === - TWENTY_STANDARD_APPLICATION.universalIdentifier - ? undefined - : getApplicationSubAllFlatEntityMaps({ - applicationIds: [twentyStandardFlatApplication.id], - fromAllFlatEntityMaps: existingAllFlatEntityMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigrationFromTo( { 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 e25814e8aa7..370323bbc7d 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 @@ -1,9 +1,9 @@ import { Injectable, Logger } from '@nestjs/common'; +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; import { FileFolder } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { PackageJson } from 'type-fest'; -import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts index 2e251d60e17..c25d599d148 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts @@ -11,6 +11,10 @@ type MetadataRequiredForValidation = { }; }; +export type MetadataRelatedMetadataNameForValidation< + T extends AllMetadataName, +> = keyof (typeof ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION)[T]; + // TODO deprecate in favor of ALL_METADATA_SERIALIZED_RELATION export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = { fieldMetadata: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts index 610a9b023a6..92c3119689a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts @@ -12,6 +12,9 @@ type MetadataSerializedRelationProperties = { : Partial>; }; +export type MetadataSerializedRelatedMetadataName = + keyof (typeof ALL_METADATA_SERIALIZED_RELATION)[T]; + export const ALL_METADATA_SERIALIZED_RELATION = { agent: {}, skill: {}, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type.ts index 56220f12753..6e180f358c0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type.ts @@ -4,5 +4,6 @@ import { type UniversalFlatEntityMaps } from 'src/engine/workspace-manager/works export type FlatEntityMaps = UniversalFlatEntityMaps & { universalIdentifierById: Partial>; + // TODO refactor or replicate to be universal and UniversalFlatEntityMaps lvl located universalIdentifiersByApplicationId: Partial>; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names-for-validation.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names-for-validation.util.ts new file mode 100644 index 00000000000..846c3ab3dfd --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names-for-validation.util.ts @@ -0,0 +1,16 @@ +import { type AllMetadataName } from 'twenty-shared/metadata'; + +import { + ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION, + type MetadataRelatedMetadataNameForValidation, +} from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant'; + +export const getMetadataRelatedMetadataNamesForValidation = < + T extends AllMetadataName, +>( + metadataName: T, +) => { + return Object.keys( + ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION[metadataName], + ) as MetadataRelatedMetadataNameForValidation[]; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/get-metadata-serialized-relation-names.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/get-metadata-serialized-relation-names.util.ts index 42e716ce4f4..a8b3decb77c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/get-metadata-serialized-relation-names.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/get-metadata-serialized-relation-names.util.ts @@ -1,11 +1,14 @@ import { type AllMetadataName } from 'twenty-shared/metadata'; -import { ALL_METADATA_SERIALIZED_RELATION } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant'; +import { + ALL_METADATA_SERIALIZED_RELATION, + type MetadataSerializedRelatedMetadataName, +} from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant'; -export const getMetadataSerializedRelationNames = ( - metadataName: AllMetadataName, -): AllMetadataName[] => { +export const getMetadataSerializedRelationNames = ( + metadataName: T, +) => { return Object.keys( ALL_METADATA_SERIALIZED_RELATION[metadataName], - ) as AllMetadataName[]; + ) as MetadataSerializedRelatedMetadataName[]; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-by-ids-maps-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-by-ids-maps-or-throw.util.ts index 99c01abda86..91944eabac5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-by-ids-maps-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-by-ids-maps-or-throw.util.ts @@ -1,8 +1,8 @@ import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; -import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; export const getSubFlatEntityByIdsMapsOrThrow = ({ flatEntityIds, @@ -11,15 +11,19 @@ export const getSubFlatEntityByIdsMapsOrThrow = ({ flatEntityMaps: FlatEntityMaps; flatEntityIds: string[]; }): FlatEntityMaps => { - return flatEntityIds.reduce>((acc, flatEntityId) => { + const flatEntityMapsToMutate = createEmptyFlatEntityMaps(); + + flatEntityIds.forEach((flatEntityId) => { const flatEntity = findFlatEntityByIdInFlatEntityMapsOrThrow({ flatEntityId, flatEntityMaps, }); - return addFlatEntityToFlatEntityMapsOrThrow({ + addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity, - flatEntityMaps: acc, + flatEntityMapsToMutate, }); - }, createEmptyFlatEntityMaps()); + }); + + return flatEntityMapsToMutate; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-cross-entity.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-cross-entity.util.ts new file mode 100644 index 00000000000..9c260228609 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-cross-entity.util.ts @@ -0,0 +1,126 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; +import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps-or-throw.util'; +import { validateFlatObjectMetadataIdentifiers } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util'; +import { validateObjectMetadataSystemFieldsIntegrity } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-system-fields-integrity.util'; +import { + type OrchestratorActionsReport, + type OrchestratorFailureReport, +} from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type'; +import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type'; +import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util'; + +type ValidateObjectMetadataSystemFieldsIntegrityArgs = { + orchestratorActionsReport: Pick< + OrchestratorActionsReport, + 'fieldMetadata' | 'objectMetadata' + >; + optimisticUniversalFlatMaps: Pick< + AllUniversalFlatEntityMaps, + 'flatFieldMetadataMaps' | 'flatObjectMetadataMaps' + >; +}; +export const validateObjectMetadataCrossEntity = ({ + optimisticUniversalFlatMaps, + orchestratorActionsReport, +}: ValidateObjectMetadataSystemFieldsIntegrityArgs): Pick< + OrchestratorFailureReport, + 'objectMetadata' +> => { + const metadataValidationErrors: Pick< + OrchestratorFailureReport, + 'objectMetadata' + > = { + objectMetadata: [], + }; + + const createdObjectMetadatas = + findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow({ + universalIdentifiers: orchestratorActionsReport.objectMetadata.create.map( + (createObjectAction) => + createObjectAction.flatEntity.universalIdentifier, + ), + flatEntityMaps: optimisticUniversalFlatMaps.flatObjectMetadataMaps, + }); + + for (const universalFlatObjectMetadata of createdObjectMetadatas) { + const createFailedFlatEntityValidations = getEmptyFlatEntityValidationError( + { + flatEntityMinimalInformation: { + universalIdentifier: universalFlatObjectMetadata.universalIdentifier, + namePlural: universalFlatObjectMetadata.namePlural, + nameSingular: universalFlatObjectMetadata.nameSingular, + }, + metadataName: 'objectMetadata', + type: 'create', + }, + ); + + createFailedFlatEntityValidations.errors.push( + ...validateObjectMetadataSystemFieldsIntegrity({ + universalFlatFieldMetadataMaps: + optimisticUniversalFlatMaps.flatFieldMetadataMaps, + universalFlatObjectMetadata, + }), + ); + + createFailedFlatEntityValidations.errors.push( + ...validateFlatObjectMetadataIdentifiers({ + universalFlatObjectMetadata, + universalFlatFieldMetadataMaps: + optimisticUniversalFlatMaps.flatFieldMetadataMaps, + }), + ); + + if (createFailedFlatEntityValidations.errors.length > 0) { + metadataValidationErrors.objectMetadata.push( + createFailedFlatEntityValidations, + ); + } + } + + for (const objectMetadataUpdate of orchestratorActionsReport.objectMetadata + .update) { + const updatedFlatObjectMetadata = + findFlatEntityByUniversalIdentifierOrThrow({ + flatEntityMaps: optimisticUniversalFlatMaps.flatObjectMetadataMaps, + universalIdentifier: objectMetadataUpdate.universalIdentifier, + }); + + const updateFailedFlatEntityValidations = getEmptyFlatEntityValidationError( + { + flatEntityMinimalInformation: { + universalIdentifier: updatedFlatObjectMetadata.universalIdentifier, + namePlural: updatedFlatObjectMetadata.namePlural, + nameSingular: updatedFlatObjectMetadata.nameSingular, + }, + metadataName: 'objectMetadata', + type: 'update', + }, + ); + + if ( + isDefined( + objectMetadataUpdate.update + .labelIdentifierFieldMetadataUniversalIdentifier, + ) + ) { + updateFailedFlatEntityValidations.errors.push( + ...validateFlatObjectMetadataIdentifiers({ + universalFlatObjectMetadata: updatedFlatObjectMetadata, + universalFlatFieldMetadataMaps: + optimisticUniversalFlatMaps.flatFieldMetadataMaps, + }), + ); + } + + if (updateFailedFlatEntityValidations.errors.length > 0) { + metadataValidationErrors.objectMetadata.push( + updateFailedFlatEntityValidations, + ); + } + } + + return metadataValidationErrors; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-system-fields-integrity.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-system-fields-integrity.ts deleted file mode 100644 index d86510b2f1e..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-system-fields-integrity.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { msg } from '@lingui/core/macro'; -import { isDefined } from 'twenty-shared/utils'; - -import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; -import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps-or-throw.util'; -import { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant'; -import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; -import { - type OrchestratorActionsReport, - type OrchestratorFailureReport, -} from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type'; -import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type'; -import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; -import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type'; -import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util'; -import { buildUniversalFlatObjectFieldByNameAndJoinColumnMaps } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/utils/build-universal-flat-object-field-by-name-and-join-column-maps.util'; - -type ValidateObjectMetadataSystemFieldsIntegrityArgs = { - orchestratorActionsReport: Pick< - OrchestratorActionsReport, - 'fieldMetadata' | 'objectMetadata' - >; - optimisticUniversalFlatMaps: Pick< - AllUniversalFlatEntityMaps, - 'flatFieldMetadataMaps' | 'flatObjectMetadataMaps' - >; -}; -export const validateObjectMetadataSystemFieldsIntegrity = ({ - optimisticUniversalFlatMaps, - orchestratorActionsReport, -}: ValidateObjectMetadataSystemFieldsIntegrityArgs): Pick< - OrchestratorFailureReport, - 'objectMetadata' -> => { - const metadataValidationErrors: Pick< - OrchestratorFailureReport, - 'objectMetadata' - > = { - objectMetadata: [], - }; - - const createdObjectMetadatas = - findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow({ - universalIdentifiers: orchestratorActionsReport.objectMetadata.create.map( - (createObjectAction) => - createObjectAction.flatEntity.universalIdentifier, - ), - flatEntityMaps: optimisticUniversalFlatMaps.flatObjectMetadataMaps, - }); - - for (const createdObjectMetadata of createdObjectMetadatas) { - const createdFailedFlatEntityValidations: FailedFlatEntityValidation< - 'objectMetadata', - 'create' - > = getEmptyFlatEntityValidationError({ - flatEntityMinimalInformation: { - universalIdentifier: createdObjectMetadata.universalIdentifier, - namePlural: createdObjectMetadata.namePlural, - nameSingular: createdObjectMetadata.nameSingular, - }, - metadataName: 'objectMetadata', - type: 'create', - }); - - const { fieldUniversalIdentifierByName } = - buildUniversalFlatObjectFieldByNameAndJoinColumnMaps({ - flatFieldMetadataMaps: - optimisticUniversalFlatMaps.flatFieldMetadataMaps, - flatObjectMetadata: createdObjectMetadata, - }); - - for (const expectedSystemField of Object.values( - PARTIAL_SYSTEM_FLAT_FIELD_METADATAS, - )) { - const matchingFieldUniversalIdentifier = - fieldUniversalIdentifierByName[expectedSystemField.name]; - - const expectedFieldName = expectedSystemField.name; - - if (!isDefined(matchingFieldUniversalIdentifier)) { - createdFailedFlatEntityValidations.errors.push({ - code: ObjectMetadataExceptionCode.MISSING_SYSTEM_FIELD, - message: `System field ${expectedFieldName} is missing`, - userFriendlyMessage: msg`System field ${expectedFieldName} is missing`, - value: expectedFieldName, - }); - } else { - const universalFlatFieldMetadata = - findFlatEntityByUniversalIdentifierOrThrow({ - flatEntityMaps: optimisticUniversalFlatMaps.flatFieldMetadataMaps, - universalIdentifier: matchingFieldUniversalIdentifier, - }); - - const propertiesToValidate = [ - 'type', - 'isSystem', - ] as const satisfies (keyof UniversalFlatFieldMetadata)[]; - - for (const property of propertiesToValidate) { - const expectedValue = expectedSystemField[property]; - const actualValue = universalFlatFieldMetadata[property]; - - if (actualValue !== expectedValue) { - createdFailedFlatEntityValidations.errors.push({ - code: ObjectMetadataExceptionCode.INVALID_SYSTEM_FIELD, - message: `System field ${expectedFieldName} has invalid ${property}: expected ${String(expectedValue)}, got ${String(actualValue)}`, - userFriendlyMessage: msg`System field ${expectedFieldName} has invalid ${property}`, - value: actualValue, - }); - } - } - } - } - - if (createdFailedFlatEntityValidations.errors.length > 0) { - metadataValidationErrors.objectMetadata.push( - createdFailedFlatEntityValidations, - ); - } - } - - return metadataValidationErrors; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-system-fields-integrity.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-system-fields-integrity.util.ts new file mode 100644 index 00000000000..1c3dea75ab6 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-system-fields-integrity.util.ts @@ -0,0 +1,73 @@ +import { msg } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; + +import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; +import { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant'; +import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type'; +import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; +import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type'; +import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type'; +import { buildUniversalFlatObjectFieldByNameAndJoinColumnMaps } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/utils/build-universal-flat-object-field-by-name-and-join-column-maps.util'; + +type ValidateObjectMetadataSystemFieldsIntegrityArgs = { + universalFlatObjectMetadata: UniversalFlatObjectMetadata; + universalFlatFieldMetadataMaps: AllUniversalFlatEntityMaps['flatFieldMetadataMaps']; +}; +export const validateObjectMetadataSystemFieldsIntegrity = ({ + universalFlatFieldMetadataMaps, + universalFlatObjectMetadata, +}: ValidateObjectMetadataSystemFieldsIntegrityArgs): FlatEntityValidationError[] => { + const errors: FlatEntityValidationError[] = []; + + const { fieldUniversalIdentifierByName } = + buildUniversalFlatObjectFieldByNameAndJoinColumnMaps({ + flatFieldMetadataMaps: universalFlatFieldMetadataMaps, + flatObjectMetadata: universalFlatObjectMetadata, + }); + + for (const expectedSystemField of Object.values( + PARTIAL_SYSTEM_FLAT_FIELD_METADATAS, + )) { + const matchingFieldUniversalIdentifier = + fieldUniversalIdentifierByName[expectedSystemField.name]; + + const expectedFieldName = expectedSystemField.name; + + if (!isDefined(matchingFieldUniversalIdentifier)) { + errors.push({ + code: ObjectMetadataExceptionCode.MISSING_SYSTEM_FIELD, + message: `System field ${expectedFieldName} is missing`, + userFriendlyMessage: msg`System field ${expectedFieldName} is missing`, + value: expectedFieldName, + }); + } else { + const universalFlatFieldMetadata = + findFlatEntityByUniversalIdentifierOrThrow({ + flatEntityMaps: universalFlatFieldMetadataMaps, + universalIdentifier: matchingFieldUniversalIdentifier, + }); + + const propertiesToValidate = [ + 'type', + 'isSystem', + ] as const satisfies (keyof UniversalFlatFieldMetadata)[]; + + for (const property of propertiesToValidate) { + const expectedValue = expectedSystemField[property]; + const actualValue = universalFlatFieldMetadata[property]; + + if (actualValue !== expectedValue) { + errors.push({ + code: ObjectMetadataExceptionCode.INVALID_SYSTEM_FIELD, + message: `System field ${expectedFieldName} has invalid ${property}: expected ${String(expectedValue)}, got ${String(actualValue)}`, + userFriendlyMessage: msg`System field ${expectedFieldName} has invalid ${property}`, + value: actualValue, + }); + } + } + } + } + + return errors; +}; 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 fc9bffccfad..eee986fb162 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 @@ -20,6 +20,7 @@ import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspa import { FromToAllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type'; import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; +// TODO completely deprecate this file once we've created the twenty-standard twenty-app manifest @Injectable() export class TwentyStandardApplicationService { constructor( 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 169a7403e58..58babdcef1c 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 @@ -1,10 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { type AllMetadataName } from 'twenty-shared/metadata'; import { isDefined } from 'twenty-shared/utils'; import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant'; -import { type MetadataUniversalFlatEntityAndRelatedUniversalFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-types.type'; import { createEmptyOrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/constant/empty-orchestrator-actions-report.constant'; import { EMPTY_ORCHESTRATOR_FAILURE_REPORT } from 'src/engine/workspace-manager/workspace-migration/constant/empty-orchestrator-failure-report.constant'; import { @@ -74,6 +72,12 @@ export class WorkspaceMigrationBuildOrchestratorService { WorkspaceMigrationOrchestratorBuildArgs, 'fromToAllFlatEntityMaps' | 'dependencyAllFlatEntityMaps' >): AllUniversalFlatEntityMaps { + if (isDefined(dependencyAllFlatEntityMaps)) { + return { + ...createEmptyAllFlatEntityMaps(), + ...dependencyAllFlatEntityMaps, + }; + } const allFromToFlatEntityMapsKeys = Object.keys( fromToAllFlatEntityMaps, ) as (keyof AllUniversalFlatEntityMaps)[]; @@ -82,10 +86,7 @@ export class WorkspaceMigrationBuildOrchestratorService { (allFlatEntityMaps, currFlatMaps) => { const fromToOccurence = fromToAllFlatEntityMaps[currFlatMaps]; - if ( - !isDefined(fromToOccurence) || - isDefined(allFlatEntityMaps[currFlatMaps]) - ) { + if (!isDefined(fromToOccurence)) { return allFlatEntityMaps; } @@ -96,31 +97,10 @@ export class WorkspaceMigrationBuildOrchestratorService { }, { ...createEmptyAllFlatEntityMaps(), - ...dependencyAllFlatEntityMaps, }, ); } - private mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation< - T extends AllMetadataName, - >({ - allFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps, - }: { - flatEntityMapsAndRelatedFlatEntityMaps: MetadataUniversalFlatEntityAndRelatedUniversalFlatEntityMaps; - allFlatEntityMaps: AllUniversalFlatEntityMaps; - }) { - const flatEntityMapsKeys = Object.keys( - flatEntityMapsAndRelatedFlatEntityMaps, - ) as (keyof MetadataUniversalFlatEntityAndRelatedUniversalFlatEntityMaps)[]; - - for (const flatEntityMapsKey of flatEntityMapsKeys) { - // @ts-expect-error TODO improve - allFlatEntityMaps[flatEntityMapsKey] = - flatEntityMapsAndRelatedFlatEntityMaps[flatEntityMapsKey]; - } - } - public async buildWorkspaceMigration({ workspaceId, buildOptions, @@ -142,6 +122,7 @@ export class WorkspaceMigrationBuildOrchestratorService { fromToAllFlatEntityMaps, dependencyAllFlatEntityMaps, }); + const { flatObjectMetadataMaps, flatViewFieldMaps, @@ -177,32 +158,13 @@ export class WorkspaceMigrationBuildOrchestratorService { { additionalCacheDataMaps, buildOptions, - // Note: That's a hacky way to allow validating object against field metadatas, not optimal - dependencyOptimisticFlatEntityMaps: { - flatFieldMetadataMaps: { - byUniversalIdentifier: { - ...dependencyAllFlatEntityMaps?.flatFieldMetadataMaps - ?.byUniversalIdentifier, - ...flatFieldMetadataMaps?.from.byUniversalIdentifier, - ...flatFieldMetadataMaps?.to.byUniversalIdentifier, - }, - }, - }, - /// + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, from: fromFlatObjectMetadataMaps, to: toFlatObjectMetadataMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - objectResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (objectResult.status === 'fail') { orchestratorFailureReport.objectMetadata.push(...objectResult.errors); } else { @@ -220,22 +182,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatFieldMetadataMaps, to: toFlatFieldMetadataMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatObjectMetadataMaps: - optimisticAllFlatEntityMaps.flatObjectMetadataMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - fieldResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (fieldResult.status === 'fail') { orchestratorFailureReport.fieldMetadata.push(...fieldResult.errors); } else { @@ -252,24 +203,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatIndexMaps, to: toFlatIndexMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatFieldMetadataMaps: - optimisticAllFlatEntityMaps.flatFieldMetadataMaps, - flatObjectMetadataMaps: - optimisticAllFlatEntityMaps.flatObjectMetadataMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - indexResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (indexResult.status === 'fail') { orchestratorFailureReport.index.push(...indexResult.errors); } else { @@ -283,12 +221,7 @@ export class WorkspaceMigrationBuildOrchestratorService { await this.workspaceMigrationViewActionsBuilderService.validateAndBuild( { additionalCacheDataMaps, - dependencyOptimisticFlatEntityMaps: { - flatObjectMetadataMaps: - optimisticAllFlatEntityMaps.flatObjectMetadataMaps, - flatFieldMetadataMaps: - optimisticAllFlatEntityMaps.flatFieldMetadataMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, from: fromFlatViewMaps, to: toFlatViewMaps, buildOptions, @@ -296,14 +229,6 @@ export class WorkspaceMigrationBuildOrchestratorService { }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - viewResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (viewResult.status === 'fail') { orchestratorFailureReport.view.push(...viewResult.errors); } else { @@ -322,21 +247,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatViewFieldGroupMaps, to: toFlatViewFieldGroupMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatViewMaps: optimisticAllFlatEntityMaps.flatViewMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - viewFieldGroupResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (viewFieldGroupResult.status === 'fail') { orchestratorFailureReport.viewFieldGroup.push( ...viewFieldGroupResult.errors, @@ -356,27 +271,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatViewFieldMaps, to: toFlatViewFieldMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatObjectMetadataMaps: - optimisticAllFlatEntityMaps.flatObjectMetadataMaps, - flatFieldMetadataMaps: - optimisticAllFlatEntityMaps.flatFieldMetadataMaps, - flatViewMaps: optimisticAllFlatEntityMaps.flatViewMaps, - flatViewFieldGroupMaps: - optimisticAllFlatEntityMaps.flatViewFieldGroupMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - viewFieldResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (viewFieldResult.status === 'fail') { orchestratorFailureReport.viewField.push(...viewFieldResult.errors); } else { @@ -397,21 +296,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatViewFilterGroupMaps, to: toFlatViewFilterGroupMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatViewMaps: optimisticAllFlatEntityMaps.flatViewMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - viewFilterGroupResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (viewFilterGroupResult.status === 'fail') { orchestratorFailureReport.viewFilterGroup.push( ...viewFilterGroupResult.errors, @@ -432,25 +321,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatViewFilterMaps, to: toFlatViewFilterMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatFieldMetadataMaps: - optimisticAllFlatEntityMaps.flatFieldMetadataMaps, - flatViewMaps: optimisticAllFlatEntityMaps.flatViewMaps, - flatViewFilterGroupMaps: - optimisticAllFlatEntityMaps.flatViewFilterGroupMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - viewFilterResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (viewFilterResult.status === 'fail') { orchestratorFailureReport.viewFilter.push(...viewFilterResult.errors); } else { @@ -468,23 +343,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatViewGroupMaps, to: toFlatViewGroupMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatFieldMetadataMaps: - optimisticAllFlatEntityMaps.flatFieldMetadataMaps, - flatViewMaps: optimisticAllFlatEntityMaps.flatViewMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - viewGroupResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (viewGroupResult.status === 'fail') { orchestratorFailureReport.viewGroup.push(...viewGroupResult.errors); } else { @@ -504,23 +367,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatRowLevelPermissionPredicateGroupMaps, to: toFlatRowLevelPermissionPredicateGroupMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatRoleMaps: optimisticAllFlatEntityMaps.flatRoleMaps, - flatObjectMetadataMaps: - optimisticAllFlatEntityMaps.flatObjectMetadataMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - rowLevelPermissionPredicateGroupResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (rowLevelPermissionPredicateGroupResult.status === 'fail') { orchestratorFailureReport.rowLevelPermissionPredicateGroup.push( ...rowLevelPermissionPredicateGroupResult.errors, @@ -543,27 +394,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatRowLevelPermissionPredicateMaps, to: toFlatRowLevelPermissionPredicateMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatFieldMetadataMaps: - optimisticAllFlatEntityMaps.flatFieldMetadataMaps, - flatObjectMetadataMaps: - optimisticAllFlatEntityMaps.flatObjectMetadataMaps, - flatRowLevelPermissionPredicateGroupMaps: - optimisticAllFlatEntityMaps.flatRowLevelPermissionPredicateGroupMaps, - flatRoleMaps: optimisticAllFlatEntityMaps.flatRoleMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - rowLevelPermissionPredicateResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (rowLevelPermissionPredicateResult.status === 'fail') { orchestratorFailureReport.rowLevelPermissionPredicate.push( ...rowLevelPermissionPredicateResult.errors, @@ -585,19 +420,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatLogicFunctionMaps, to: toFlatLogicFunctionMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: undefined, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - logicFunctionResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (logicFunctionResult.status === 'fail') { orchestratorFailureReport.logicFunction.push( ...logicFunctionResult.errors, @@ -617,19 +444,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatRoleMaps, to: toFlatRoleMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: undefined, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - roleResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (roleResult.status === 'fail') { orchestratorFailureReport.role.push(...roleResult.errors); } else { @@ -648,22 +467,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatRoleTargetMaps, to: toFlatRoleTargetMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatAgentMaps: optimisticAllFlatEntityMaps.flatAgentMaps, - flatRoleMaps: optimisticAllFlatEntityMaps.flatRoleMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - roleTargetResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (roleTargetResult.status === 'fail') { orchestratorFailureReport.roleTarget.push(...roleTargetResult.errors); } else { @@ -681,21 +489,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatAgentMaps, to: toFlatAgentMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatRoleMaps: optimisticAllFlatEntityMaps.flatRoleMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - agentResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (agentResult.status === 'fail') { orchestratorFailureReport.agent.push(...agentResult.errors); } else { @@ -713,19 +511,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatSkillMaps, to: toFlatSkillMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: undefined, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - skillResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (skillResult.status === 'fail') { orchestratorFailureReport.skill.push(...skillResult.errors); } else { @@ -746,24 +536,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatCommandMenuItemMaps, to: toFlatCommandMenuItemMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatObjectMetadataMaps: - optimisticAllFlatEntityMaps.flatObjectMetadataMaps, - flatFrontComponentMaps: - optimisticAllFlatEntityMaps.flatFrontComponentMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - commandMenuItemResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (commandMenuItemResult.status === 'fail') { orchestratorFailureReport.commandMenuItem.push( ...commandMenuItemResult.errors, @@ -787,23 +564,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatNavigationMenuItemMaps, to: toFlatNavigationMenuItemMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatObjectMetadataMaps: - optimisticAllFlatEntityMaps.flatObjectMetadataMaps, - flatViewMaps: optimisticAllFlatEntityMaps.flatViewMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - navigationMenuItemResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (navigationMenuItemResult.status === 'fail') { orchestratorFailureReport.navigationMenuItem.push( ...navigationMenuItemResult.errors, @@ -825,24 +590,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatPageLayoutMaps, to: toFlatPageLayoutMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatObjectMetadataMaps: - optimisticAllFlatEntityMaps.flatObjectMetadataMaps, - flatPageLayoutTabMaps: - optimisticAllFlatEntityMaps.flatPageLayoutTabMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - pageLayoutResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (pageLayoutResult.status === 'fail') { orchestratorFailureReport.pageLayout.push(...pageLayoutResult.errors); } else { @@ -850,9 +602,6 @@ export class WorkspaceMigrationBuildOrchestratorService { } } - // Page layout tabs must be processed before page layout widgets because - // widgets reference tabs, and the optimistic cache needs to contain the - // newly created tabs before widget validation runs if (isDefined(flatPageLayoutTabMaps)) { const { from: fromFlatPageLayoutTabMaps, to: toFlatPageLayoutTabMaps } = flatPageLayoutTabMaps; @@ -864,22 +613,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatPageLayoutTabMaps, to: toFlatPageLayoutTabMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatPageLayoutMaps: - optimisticAllFlatEntityMaps.flatPageLayoutMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - pageLayoutTabResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (pageLayoutTabResult.status === 'fail') { orchestratorFailureReport.pageLayoutTab.push( ...pageLayoutTabResult.errors, @@ -902,24 +640,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatPageLayoutWidgetMaps, to: toFlatPageLayoutWidgetMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: { - flatObjectMetadataMaps: - optimisticAllFlatEntityMaps.flatObjectMetadataMaps, - flatPageLayoutTabMaps: - optimisticAllFlatEntityMaps.flatPageLayoutTabMaps, - }, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - pageLayoutWidgetResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (pageLayoutWidgetResult.status === 'fail') { orchestratorFailureReport.pageLayoutWidget.push( ...pageLayoutWidgetResult.errors, @@ -941,19 +666,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatFrontComponentMaps, to: toFlatFrontComponentMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: undefined, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - frontComponentResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (frontComponentResult.status === 'fail') { orchestratorFailureReport.frontComponent.push( ...frontComponentResult.errors, @@ -974,19 +691,11 @@ export class WorkspaceMigrationBuildOrchestratorService { from: fromFlatWebhookMaps, to: toFlatWebhookMaps, buildOptions, - dependencyOptimisticFlatEntityMaps: undefined, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, workspaceId, }, ); - this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( - { - allFlatEntityMaps: optimisticAllFlatEntityMaps, - flatEntityMapsAndRelatedFlatEntityMaps: - webhookResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, - }, - ); - if (webhookResult.status === 'fail') { orchestratorFailureReport.webhook.push(...webhookResult.errors); } else { 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 8d640f06d3e..864b5ab37f7 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 @@ -8,13 +8,20 @@ import { isDefined } from 'twenty-shared/utils'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { MetadataEventEmitter } from 'src/engine/metadata-event-emitter/metadata-event-emitter'; -import { ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; import { FlatEntityToCreateDeleteUpdate } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-to-create-delete-update.type'; +import { MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; import { MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type'; import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; +import { getMetadataRelatedMetadataNamesForValidation } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names-for-validation.util'; +import { getSubFlatEntityMapsByApplicationIdsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-by-application-ids-or-throw.util'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; import { WorkspaceMigrationV2Exception } from 'src/engine/workspace-manager/workspace-migration.exception'; import { WORKSPACE_MIGRATION_ADDITIONAL_CACHE_DATA_MAPS_KEY } from 'src/engine/workspace-manager/workspace-migration/constant/workspace-migration-additional-cache-data-maps-key.constant'; import { @@ -29,7 +36,7 @@ import { 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 { computeUniversalFlatEntityMapsFromToThroughMutation } from 'src/engine/workspace-manager/workspace-migration/utils/compute-universal-flat-entity-maps-from-to-through-mutation.util'; import { InferDeletionFromMissingEntities } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/infer-deletion-from-missing-entities.type'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service'; @@ -39,8 +46,6 @@ type ValidateBuildAndRunWorkspaceMigrationFromMatriceArgs = { [P in AllMetadataName]?: FlatEntityToCreateDeleteUpdate

; }; isSystemBuild?: boolean; - // TODO remove once application synchronization do not consume services atomically anymore - // Should always be the universal workspace custom app id applicationUniversalIdentifier: string; }; @@ -66,29 +71,30 @@ export class WorkspaceMigrationValidateBuildAndRunService { private async computeAllRelatedFlatEntityMaps({ allFlatEntityOperationByMetadataName, workspaceId, + applicationUniversalIdentifier, }: ValidateBuildAndRunWorkspaceMigrationFromMatriceArgs) { const allMetadataNameToCompare = Object.keys( allFlatEntityOperationByMetadataName, ) as AllMetadataName[]; - const allDependencyMetadataName = allMetadataNameToCompare.flatMap( - (metadataName) => - Object.keys( - ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION[metadataName], - ) as AllMetadataName[], - ); const allMetadataNameCacheToCompute = [ - ...new Set([...allMetadataNameToCompare, ...allDependencyMetadataName]), + ...new Set([ + ...allMetadataNameToCompare, + ...allMetadataNameToCompare.flatMap( + getMetadataRelatedMetadataNamesForValidation, + ), + ]), ]; const allFlatEntityMapsCacheKeysToCompute = allMetadataNameCacheToCompute.map(getMetadataFlatEntityMapsKey); - const allRelatedFlatEntityMaps = + const { flatApplicationMaps, ...allRelatedFlatEntityMaps } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ ...allFlatEntityMapsCacheKeysToCompute, ...WORKSPACE_MIGRATION_ADDITIONAL_CACHE_DATA_MAPS_KEY, + 'flatApplicationMaps', ]); - const initialAccumulator = allDependencyMetadataName.reduce< + const initialAccumulator = allMetadataNameCacheToCompute.reduce< Partial >( (allFlatEntityMaps, metadataName) => ({ @@ -98,7 +104,29 @@ export class WorkspaceMigrationValidateBuildAndRunService { }), {}, ); - const dependencyAllFlatEntityMaps = allDependencyMetadataName.reduce( + + const twentyStandardApplicationId = + flatApplicationMaps.idByUniversalIdentifier[ + TWENTY_STANDARD_APPLICATION.universalIdentifier + ]; + + const applicationId = + flatApplicationMaps.idByUniversalIdentifier[ + applicationUniversalIdentifier + ]; + + if (!isDefined(twentyStandardApplicationId) || !isDefined(applicationId)) { + throw new FlatEntityMapsException( + 'Application to build and its dependent application not found', + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const isBuildingTwentyStandardApplication = + applicationUniversalIdentifier === + TWENTY_STANDARD_APPLICATION.universalIdentifier; + + const dependencyAllFlatEntityMaps = allMetadataNameCacheToCompute.reduce( (allFlatEntityMaps, metadataName) => { const metadataFlatEntityMapsKey = getMetadataFlatEntityMapsKey(metadataName); @@ -106,7 +134,15 @@ export class WorkspaceMigrationValidateBuildAndRunService { return { ...allFlatEntityMaps, [metadataFlatEntityMapsKey]: - allRelatedFlatEntityMaps[metadataFlatEntityMapsKey], + getSubFlatEntityMapsByApplicationIdsOrThrow< + MetadataFlatEntity + >({ + applicationIds: isBuildingTwentyStandardApplication + ? [applicationId] + : [applicationId, twentyStandardApplicationId], + flatEntityMaps: + allRelatedFlatEntityMaps[metadataFlatEntityMapsKey], + }), }; }, initialAccumulator, @@ -153,10 +189,10 @@ export class WorkspaceMigrationValidateBuildAndRunService { }); const fromToAllFlatEntityMaps: FromToAllUniversalFlatEntityMaps = {}; - const inferDeletionFromMissingEntities: InferDeletionFromMissingEntities = - {}; const idByUniversalIdentifierByMetadataName: IdByUniversalIdentifierByMetadataName = {}; + const inferDeletionFromMissingEntities: InferDeletionFromMissingEntities = + {}; const allMetadataNameToCompare = Object.keys( allFlatEntityOperationByMetadataName, ) as AllMetadataName[]; @@ -166,7 +202,10 @@ export class WorkspaceMigrationValidateBuildAndRunService { allFlatEntityOperationByMetadataName[metadataName]; if (!isDefined(flatEntityOperations)) { - throw new Error('Should never occurs'); + throw new FlatEntityMapsException( + `Could not load flat entity maps to compare for ${metadataName}, should never occur`, + FlatEntityMapsExceptionCode.INTERNAL_SERVER_ERROR, + ); } const { flatEntityToCreate, flatEntityToDelete, flatEntityToUpdate } = flatEntityOperations; @@ -193,8 +232,8 @@ export class WorkspaceMigrationValidateBuildAndRunService { // @ts-expect-error Metadata flat entity maps cache key and metadataName colliding fromToAllFlatEntityMaps[flatEntityMapsKey] = - computeUniversalFlatEntityMapsFromTo({ - flatEntityMaps, + computeUniversalFlatEntityMapsFromToThroughMutation({ + flatEntityMaps: structuredClone(flatEntityMaps), flatEntityToCreate, flatEntityToDelete, flatEntityToUpdate, 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 1ef352e6a51..0738efc0621 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 @@ -21,8 +21,11 @@ export type WorkspaceMigrationOrchestratorBuildArgs = { workspaceId: string; buildOptions: WorkspaceMigrationBuilderOptions; fromToAllFlatEntityMaps: FromToAllUniversalFlatEntityMaps; - dependencyAllFlatEntityMaps?: Partial; additionalCacheDataMaps: WorkspaceMigrationBuilderAdditionalCacheDataMaps; + /** + * Dependency maps must contain current application and its dependent app app flat entity maps + */ + dependencyAllFlatEntityMaps?: Partial; }; export type OrchestratorFailureReport = { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/compute-universal-flat-entity-maps-from-to.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/compute-universal-flat-entity-maps-from-to-through-mutation.util.ts similarity index 86% rename from packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/compute-universal-flat-entity-maps-from-to.util.ts rename to packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/compute-universal-flat-entity-maps-from-to-through-mutation.util.ts index d758d52c4e3..de495184c63 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/compute-universal-flat-entity-maps-from-to.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/compute-universal-flat-entity-maps-from-to-through-mutation.util.ts @@ -13,7 +13,8 @@ export type ComputeUniversalFlatEntityMapsFromToArgs< flatEntityMaps: MetadataUniversalFlatEntityMaps; } & FlatEntityToCreateDeleteUpdate; -export const computeUniversalFlatEntityMapsFromTo = < +// Note: We don't load the whole application flat entity maps in the from for performances purposes +export const computeUniversalFlatEntityMapsFromToThroughMutation = < T extends AllMetadataName, >({ flatEntityMaps, @@ -25,15 +26,12 @@ export const computeUniversalFlatEntityMapsFromTo = < to: MetadataUniversalFlatEntityMaps; } => { const fromFlatEntityMaps = - flatEntityToDelete.length > 0 - ? getSubUniversalFlatEntityByUniversalIdentifiersMapsOrThrow({ - universalIdentifiers: [ - ...flatEntityToDelete, - ...flatEntityToUpdate, - ].map(({ universalIdentifier }) => universalIdentifier), - universalFlatEntityMaps: flatEntityMaps, - }) - : flatEntityMaps; + getSubUniversalFlatEntityByUniversalIdentifiersMapsOrThrow({ + universalIdentifiers: [...flatEntityToDelete, ...flatEntityToUpdate].map( + ({ universalIdentifier }) => universalIdentifier, + ), + universalFlatEntityMaps: flatEntityMaps, + }); const toFlatEntityMaps: MetadataUniversalFlatEntityMaps = structuredClone(fromFlatEntityMaps); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/cross-entity-transversal-validation.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/cross-entity-transversal-validation.util.ts index f7232ed62e8..3899200f233 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/cross-entity-transversal-validation.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/cross-entity-transversal-validation.util.ts @@ -1,4 +1,4 @@ -import { validateObjectMetadataSystemFieldsIntegrity } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-system-fields-integrity'; +import { validateObjectMetadataCrossEntity } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-cross-entity.util'; import { type OrchestratorActionsReport, type OrchestratorFailureReport, @@ -12,7 +12,7 @@ export const crossEntityTransversalValidation = ({ optimisticUniversalFlatMaps: AllUniversalFlatEntityMaps; orchestratorActionsReport: OrchestratorActionsReport; }): Pick => { - const { objectMetadata } = validateObjectMetadataSystemFieldsIntegrity({ + const { objectMetadata } = validateObjectMetadataCrossEntity({ optimisticUniversalFlatMaps, orchestratorActionsReport, }); 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 ec611b94543..f6ffd948db6 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 @@ -9,14 +9,13 @@ import { FlatEntityMapsException, 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'; import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; import { WorkspaceMigrationBuilderAdditionalCacheDataMaps } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-builder-additional-cache-data-maps.type'; +import { AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-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'; @@ -37,7 +36,7 @@ import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-mana export type ValidateAndBuildArgs = { buildOptions: WorkspaceMigrationBuilderOptions; - dependencyOptimisticFlatEntityMaps: MetadataValidationRelatedUniversalFlatEntityMaps; + dependencyOptimisticFlatEntityMaps: AllUniversalFlatEntityMaps; workspaceId: string; additionalCacheDataMaps: WorkspaceMigrationBuilderAdditionalCacheDataMaps; } & FromTo>; @@ -59,7 +58,8 @@ export abstract class WorkspaceEntityMigrationBuilderService< public async validateAndBuild({ buildOptions, - dependencyOptimisticFlatEntityMaps: inputDependencyOptimisticFlatEntityMaps, + dependencyOptimisticFlatEntityMaps: + optimisticFlatEntityMapsAndRelatedFlatEntityMaps, from: fromFlatEntityMaps, to: toFlatEntityMaps, additionalCacheDataMaps, @@ -96,11 +96,6 @@ export abstract class WorkspaceEntityMigrationBuilderService< this.logger.time(`EntityBuilder ${this.metadataName}`, 'entity processing'); const flatEntityMapsKey = getMetadataFlatEntityMapsKey(this.metadataName); - const optimisticFlatEntityMapsAndRelatedFlatEntityMaps = { - [flatEntityMapsKey]: structuredClone(fromFlatEntityMaps), - ...structuredClone(inputDependencyOptimisticFlatEntityMaps), - } as MetadataUniversalFlatEntityAndRelatedFlatEntityMapsForValidation; - const actionsResult = getMetadataEmptyWorkspaceMigrationActionRecord( this.metadataName, ); @@ -322,7 +317,6 @@ export abstract class WorkspaceEntityMigrationBuilderService< return { status: 'fail', errors: allValidationResult, - optimisticFlatEntityMapsAndRelatedFlatEntityMaps, }; } @@ -334,7 +328,6 @@ export abstract class WorkspaceEntityMigrationBuilderService< return { status: 'success', actions: actionsResult, - optimisticFlatEntityMapsAndRelatedFlatEntityMaps, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/failed-flat-entity-validate-and-build.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/failed-flat-entity-validate-and-build.type.ts index 6be93a1fe20..194fcd154da 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/failed-flat-entity-validate-and-build.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/failed-flat-entity-validate-and-build.type.ts @@ -1,11 +1,9 @@ import { type AllMetadataName } from 'twenty-shared/metadata'; -import { type MetadataUniversalFlatEntityAndRelatedUniversalFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-types.type'; import { type WorkspaceMigrationActionType } from 'src/engine/metadata-modules/flat-entity/types/metadata-workspace-migration-action.type'; import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type'; export type FailedFlatEntityValidateAndBuild = { status: 'fail'; errors: FailedFlatEntityValidation[]; - optimisticFlatEntityMapsAndRelatedFlatEntityMaps: MetadataUniversalFlatEntityAndRelatedUniversalFlatEntityMaps; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/successful-flat-entity-validate-and-build.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/successful-flat-entity-validate-and-build.type.ts index fc7e4f7f9b7..267cb8446f4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/successful-flat-entity-validate-and-build.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/successful-flat-entity-validate-and-build.type.ts @@ -1,10 +1,8 @@ import { type AllMetadataName } from 'twenty-shared/metadata'; -import { type MetadataUniversalFlatEntityAndRelatedUniversalFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-types.type'; import { type MetadataUniversalWorkspaceMigrationActionsRecord } from 'src/engine/metadata-modules/flat-entity/types/metadata-workspace-migration-action.type'; export type SuccessfulFlatEntityValidateAndBuild = { status: 'success'; actions: MetadataUniversalWorkspaceMigrationActionsRecord; - optimisticFlatEntityMapsAndRelatedFlatEntityMaps: MetadataUniversalFlatEntityAndRelatedUniversalFlatEntityMaps; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-metadata-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-metadata-validator.service.ts index 3e598e7ee29..33de278cf6a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-metadata-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-metadata-validator.service.ts @@ -5,7 +5,6 @@ import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; import { isDefined } from 'twenty-shared/utils'; import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; -import { validateFlatObjectMetadataIdentifiers } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util'; import { validateFlatObjectMetadataNameAndLabels } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util'; import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util'; @@ -23,7 +22,6 @@ export class FlatObjectMetadataValidatorService { buildOptions, optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps, - flatFieldMetadataMaps, }, }: FlatEntityUpdateValidationArgs< typeof ALL_METADATA_NAME.objectMetadata @@ -93,13 +91,6 @@ export class FlatObjectMetadataValidatorService { userFriendlyMessage: msg`Field label identifier is required`, }); } - - validationResult.errors.push( - ...validateFlatObjectMetadataIdentifiers({ - universalFlatObjectMetadata: updatedFlatObjectMetadata, - universalFlatFieldMetadataMaps: flatFieldMetadataMaps, - }), - ); } return validationResult; @@ -175,7 +166,6 @@ export class FlatObjectMetadataValidatorService { flatEntityToValidate: flatObjectMetadataToValidate, optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { flatObjectMetadataMaps: optimisticUniversalFlatObjectMetadataMaps, - flatFieldMetadataMaps, }, buildOptions, }: UniversalFlatEntityValidationArgs< @@ -214,12 +204,6 @@ export class FlatObjectMetadataValidatorService { }); } - objectValidationResult.errors.push( - ...validateFlatObjectMetadataIdentifiers({ - universalFlatObjectMetadata: flatObjectMetadataToValidate, - universalFlatFieldMetadataMaps: flatFieldMetadataMaps, - }), - ); objectValidationResult.errors.push( ...validateFlatObjectMetadataNameAndLabels({ optimisticUniversalFlatObjectMetadataMaps, diff --git a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap index 3a135cbecce..2d84059b44b 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/successful-sync-application-workspace-migration.integration-spec.ts.snap @@ -1,5 +1,68 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`syncApplication should create a TEXT field on the standard Company object 1`] = ` +{ + "syncApplication": { + "actions": [ + { + "flatEntity": { + "applicationUniversalIdentifier": Any, + "createdAt": Any, + "defaultValue": null, + "description": "The industry of the company", + "icon": "IconBuildingFactory2", + "isActive": true, + "isCustom": true, + "isLabelSyncedWithName": false, + "isNullable": true, + "isSystem": false, + "isUIReadOnly": false, + "isUnique": false, + "label": "Industry", + "morphId": null, + "name": "industry", + "objectMetadataUniversalIdentifier": Any, + "options": null, + "relationTargetFieldMetadataUniversalIdentifier": null, + "relationTargetObjectMetadataUniversalIdentifier": null, + "standardOverrides": null, + "type": "TEXT", + "universalIdentifier": Any, + "universalSettings": null, + "updatedAt": Any, + }, + "metadataName": "fieldMetadata", + "type": "create", + }, + { + "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", + "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 1`] = ` { "syncApplication": { 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 index 6e90d88f06b..b3a7ec04bf9 100644 --- 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 @@ -4,6 +4,7 @@ import { syncApplication } from 'test/integration/metadata/suites/application/ut import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util'; import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; import { type Manifest } from 'twenty-shared/application'; +import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; import { FieldMetadataType } from 'twenty-shared/types'; import { v4 as uuidv4 } from 'uuid'; @@ -207,4 +208,58 @@ describe('syncApplication', () => { extractRecordIdsAndDatesAsExpectAny(secondSyncData), ); }, 60000); + + it('should create a TEXT field on the standard Company object', async () => { + const companyFieldId = uuidv4(); + + const manifest: 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, + apiClientChecksum: null, + }, + roles: [ + { + universalIdentifier: TEST_ROLE_ID, + label: 'Test Role', + description: 'A test role', + }, + ], + skills: [], + objects: [], + fields: [ + { + universalIdentifier: companyFieldId, + type: FieldMetadataType.TEXT, + name: 'industry', + label: 'Industry', + description: 'The industry of the company', + icon: 'IconBuildingFactory2', + objectUniversalIdentifier: + STANDARD_OBJECTS.company.universalIdentifier, + }, + ], + logicFunctions: [], + frontComponents: [], + publicAssets: [], + views: [], + navigationMenuItems: [], + pageLayouts: [], + }; + + const { data: syncData } = await syncApplication({ + manifest, + expectToFail: false, + }); + + expect(syncData).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny(syncData), + ); + }, 60000); }); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap index 2f595bb2601..06a48f511cc 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap @@ -21,52 +21,14 @@ exports[`Object metadata update should fail when labelIdentifier is not a TEXT o "universalIdentifier": Any, }, "metadataName": "objectMetadata", - "status": "fail", "type": "update", }, ], - "viewField": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "View field position cannot be lower than label identifier view field position", - "userFriendlyMessage": "View field position cannot be lower than label identifier view field position", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "View field position cannot be lower than label identifier view field position", - "userFriendlyMessage": "View field position cannot be lower than label identifier view field position", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - ], }, - "message": "Validation failed for 1 objectMetadata, 2 viewFields", + "message": "Validation failed for 1 objectMetadata", "summary": { "objectMetadata": 1, - "totalErrors": 3, - "viewField": 2, + "totalErrors": 1, }, "userFriendlyMessage": "Metadata validation failed", },