From 23e21cbeea544d8b00504410add10afe2b533491 Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:27:21 +0200 Subject: [PATCH] Label identifier validation v2 (#14867) # Introduction Adding a hacky way to validate object against fields before fields validation ( bi-directional validation process ) If you encounter an identical setup we will add a specific devXp as cleanup validation but for the moment this seems enough close https://github.com/twentyhq/core-team-issues/issues/1639 --- .../engine/dataloaders/dataloader.service.ts | 4 +- ...tion-flat-field-metadatas-or-throw.util.ts | 11 +- ...-flat-field-metadata-maps-or-throw.util.ts | 8 +- ...-to-flat-field-metadatas-to-delete.util.ts | 11 +- ...on-flat-field-metadata-name-update.util.ts | 11 +- ...t-field-metadata-name-availability.util.ts | 11 +- .../compare-two-flat-object-metadata.util.ts | 66 +++---- ...-to-flat-field-metadatas-to-delete.util.ts | 11 +- ...ject-metadata-singular-name-update.util.ts | 11 +- ...morph-field-on-object-names-update.util.ts | 11 +- ...e-flat-object-metadata-identifiers.util.ts | 23 +-- ...at-object-metadata-name-and-labels.util.ts | 62 +++++++ .../object-metadata-v2.service.ts | 11 +- .../object-metadata/object-metadata.entity.ts | 3 +- ...ce-migration-build-orchestrator.service.ts | 23 ++- ...ation-v2-object-actions-builder.service.ts | 18 +- .../workspace-migration-object-action-v2.ts | 18 +- .../flat-object-metadata-validator.service.ts | 123 ++++--------- .../delete-object-action-handler.service.ts | 11 +- .../update-object-action-handler.service.ts | 4 +- ...e-object-metadata.integration-spec.ts.snap | 162 ++++++++++++++++++ ...te-one-object-metadata.integration-spec.ts | 37 ++-- 22 files changed, 417 insertions(+), 233 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts diff --git a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts index 9be6698ff5..ea92b89ebd 100644 --- a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts +++ b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts @@ -18,7 +18,7 @@ import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/rel import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { resolveFieldMetadataStandardOverride } from 'src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util'; import { findAllOthersMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util'; -import { findObjectFieldsInFlatFieldMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; +import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util'; import { fromMorphOrRelationFlatFieldMetadataToRelationDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-morph-or-relation-flat-field-metadata-to-relation-dto.util'; import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; @@ -339,7 +339,7 @@ export class DataloaderService { flatEntityMaps: flatObjectMetadataMaps, }); const { objectFlatFieldMetadatas } = - findObjectFieldsInFlatFieldMetadataMapsOrThrow({ + findObjectFlatFieldMetadatasOrThrow({ flatFieldMetadataMaps: flatFieldMetadataMaps, flatObjectMetadata, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util.ts index e921db08d1..5ec15b8462 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util.ts @@ -7,7 +7,7 @@ import { } from 'src/engine/core-modules/common/exceptions/flat-entity-maps.exception'; import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { findObjectFieldsInFlatFieldMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; +import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @@ -30,11 +30,10 @@ export const findAllOthersMorphRelationFlatFieldMetadatasOrThrow = ({ ); } - const { objectFlatFieldMetadatas } = - findObjectFieldsInFlatFieldMetadataMapsOrThrow({ - flatFieldMetadataMaps, - flatObjectMetadata, - }); + const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({ + flatFieldMetadataMaps, + flatObjectMetadata, + }); return objectFlatFieldMetadatas.filter( ( diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util.ts index af7ede5da1..c2a3f05cb8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util.ts @@ -9,24 +9,22 @@ type FindObjectFieldsInFlatFieldMetadataMapsArgs = { flatFieldMetadataMaps: FlatEntityMaps; flatObjectMetadata: FlatObjectMetadata; }; -export const findObjectFieldsInFlatFieldMetadataMapsOrThrow = ({ +export const findObjectFlatFieldMetadatasOrThrow = ({ flatFieldMetadataMaps, flatObjectMetadata, }: FindObjectFieldsInFlatFieldMetadataMapsArgs): { - objectFlatFieldMetadataById: FlatEntityMaps; objectFlatFieldMetadatas: FlatFieldMetadata[]; } => { - const objectFlatFieldMetadataById = getSubFlatEntityMapsOrThrow({ + const objectFlatFieldMetadataMaps = getSubFlatEntityMapsOrThrow({ flatEntityIds: flatObjectMetadata.fieldMetadataIds, flatEntityMaps: flatFieldMetadataMaps, }); const objectFlatFieldMetadatas = Object.values( - objectFlatFieldMetadataById.byId, + objectFlatFieldMetadataMaps.byId, ).filter(isDefined); return { - objectFlatFieldMetadataById, objectFlatFieldMetadatas, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts index 877edfa4f6..04d7416bb7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts @@ -13,7 +13,7 @@ import { } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util'; -import { findObjectFieldsInFlatFieldMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; +import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util'; @@ -140,11 +140,10 @@ export const fromDeleteFieldInputToFlatFieldMetadatasToDelete = ({ flatEntityMaps: existingFlatObjectMetadataMaps, flatEntityId: flatIndex.objectMetadataId, }); - const { objectFlatFieldMetadatas } = - findObjectFieldsInFlatFieldMetadataMapsOrThrow({ - flatFieldMetadataMaps: existingFlatFieldMetadataMaps, - flatObjectMetadata, - }); + const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({ + flatFieldMetadataMaps: existingFlatFieldMetadataMaps, + flatObjectMetadata, + }); const newIndex = generateFlatIndexMetadataWithNameOrThrow({ flatObjectMetadata, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.util.ts index 9a37762b18..4ac5c29752 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.util.ts @@ -2,7 +2,7 @@ import { isDefined } from 'twenty-shared/utils'; import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { findObjectFieldsInFlatFieldMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; +import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util'; @@ -35,11 +35,10 @@ export const recomputeIndexOnFlatFieldMetadataNameUpdate = ({ return []; } - const { objectFlatFieldMetadatas } = - findObjectFieldsInFlatFieldMetadataMapsOrThrow({ - flatFieldMetadataMaps, - flatObjectMetadata, - }); + const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({ + flatFieldMetadataMaps, + flatObjectMetadata, + }); const optimisticObjectFlatFieldMetadatas = objectFlatFieldMetadatas.map( (flatFieldMetadata) => { if (flatFieldMetadata.id === fromFlatFieldMetadata.id) { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util.ts index f50828ab5b..618b43c7f6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util.ts @@ -10,7 +10,7 @@ import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-me import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { findObjectFieldsInFlatFieldMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; +import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @@ -50,11 +50,10 @@ export const validateFlatFieldMetadataNameAvailability = ({ remainingFlatEntityMapsToValidate?: FlatEntityMaps; }): FlatFieldMetadataValidationError[] => { const errors: FlatFieldMetadataValidationError[] = []; - const { objectFlatFieldMetadatas } = - findObjectFieldsInFlatFieldMetadataMapsOrThrow({ - flatFieldMetadataMaps, - flatObjectMetadata, - }); + const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({ + flatFieldMetadataMaps, + flatObjectMetadata, + }); const reservedCompositeFieldsNames = getReservedCompositeFieldNames( objectFlatFieldMetadatas, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/compare-two-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/compare-two-flat-object-metadata.util.ts index 8017c37f6e..5fa2b23dcf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/compare-two-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/compare-two-flat-object-metadata.util.ts @@ -9,7 +9,7 @@ import { type FlatObjectMetadataEntityJsonbProperties } from 'src/engine/metadat import { type FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util'; -import { type UpdateObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2'; +import { type FlatObjectPropertiesUpdates } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2'; import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util'; export const compareTwoFlatObjectMetadata = ({ @@ -50,42 +50,42 @@ export const compareTwoFlatObjectMetadata = ({ ); const objectMetadataDifference = diff(fromCompare, omit(toCompare, 'fields')); - return objectMetadataDifference.flatMap< - UpdateObjectAction['updates'][number] - >((difference) => { - switch (difference.type) { - case 'CHANGE': { - const { oldValue, path, value } = difference; - const property = path[0] as FlatObjectMetadataPropertiesToCompare; - const isJsonb = FLAT_OBJECT_METADATA_JSONB_PROPERTIES.includes( - property as FlatObjectMetadataEntityJsonbProperties, - ); + return objectMetadataDifference.flatMap( + (difference) => { + switch (difference.type) { + case 'CHANGE': { + const { oldValue, path, value } = difference; + const property = path[0] as FlatObjectMetadataPropertiesToCompare; + const isJsonb = FLAT_OBJECT_METADATA_JSONB_PROPERTIES.includes( + property as FlatObjectMetadataEntityJsonbProperties, + ); + + if (isJsonb) { + return { + from: parseJson(oldValue), + to: parseJson(value), + property, + }; + } - if (isJsonb) { return { - from: parseJson(oldValue), - to: parseJson(value), + from: oldValue, + to: value, property, }; } - - return { - from: oldValue, - to: value, - property, - }; + case 'CREATE': + case 'REMOVE': { + // Should never occurs ? should throw ? + return []; + } + default: { + assertUnreachable( + difference, + `Unexpected difference type: ${difference['type']}`, + ); + } } - case 'CREATE': - case 'REMOVE': { - // Should never occurs ? should throw ? - return []; - } - default: { - assertUnreachable( - difference, - `Unexpected difference type: ${difference['type']}`, - ); - } - } - }); + }, + ); }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util.ts index e93f1f4b68..7b84c0390c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util.ts @@ -6,7 +6,7 @@ import { import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { findObjectFieldsInFlatFieldMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; +import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; import { findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-relation-flat-field-metadatas-target-flat-field-metadata-or-throw.util'; import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; @@ -52,11 +52,10 @@ export const fromDeleteObjectInputToFlatFieldMetadatasToDelete = ({ ); } - const { objectFlatFieldMetadatas } = - findObjectFieldsInFlatFieldMetadataMapsOrThrow({ - flatFieldMetadataMaps, - flatObjectMetadata: flatObjectMetadataToDelete, - }); + const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({ + flatFieldMetadataMaps, + flatObjectMetadata: flatObjectMetadataToDelete, + }); const flatFieldMetadatasToDelete = objectFlatFieldMetadatas.flatMap( (flatFieldMetadata) => { if (isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util.ts index 5b6fedb8b4..df0742f4c7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util.ts @@ -1,7 +1,7 @@ import { isDefined } from 'twenty-shared/utils'; import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; -import { findObjectFieldsInFlatFieldMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; +import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util'; @@ -31,11 +31,10 @@ export const recomputeIndexAfterFlatObjectMetadataSingularNameUpdate = ({ nameSingular: updatedSingularName, }; - const { objectFlatFieldMetadatas } = - findObjectFieldsInFlatFieldMetadataMapsOrThrow({ - flatFieldMetadataMaps, - flatObjectMetadata: optimisticFlatObjectMetadata, - }); + const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({ + flatFieldMetadataMaps, + flatObjectMetadata: optimisticFlatObjectMetadata, + }); return allRelatedFlatIndexMetadata.map((flatIndex) => { const newIndex = generateFlatIndexMetadataWithNameOrThrow({ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts index 8382e6de6a..3a44029ab6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts @@ -8,7 +8,7 @@ import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { findObjectFieldsInFlatFieldMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; +import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-flat-object-metadata-many-to-one-target-morph-relation-flat-field-metadatas-or-throw.util'; @@ -43,11 +43,10 @@ export const renameRelatedMorphFieldOnObjectNamesUpdate = ({ toFlatObjectMetadata, flatFieldMetadataMaps, }: RenameRelatedMorphFieldOnObjectNamesUpdateArgs): FlatFieldMetadata[] => { - const { objectFlatFieldMetadatas } = - findObjectFieldsInFlatFieldMetadataMapsOrThrow({ - flatFieldMetadataMaps, - flatObjectMetadata: fromFlatObjectMetadata, - }); + const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({ + flatFieldMetadataMaps, + flatObjectMetadata: fromFlatObjectMetadata, + }); const manyToOneMorphRelationFlatFieldMetadatas = getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow({ flatFieldMetadataMaps, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util.ts index 82fc6a104b..7a7cf5fad1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util.ts @@ -4,6 +4,8 @@ import { isLabelIdentifierFieldMetadataTypes, } from 'twenty-shared/utils'; +import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @@ -11,24 +13,25 @@ import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object- export const validateFlatObjectMetadataIdentifiers = ({ flatObjectMetadata, - objectFlatFieldMetadatas, + flatFieldMetadataMaps, }: { flatObjectMetadata: Pick< FlatObjectMetadata, 'labelIdentifierFieldMetadataId' | 'imageIdentifierFieldMetadataId' >; - objectFlatFieldMetadatas: FlatFieldMetadata[]; + flatFieldMetadataMaps: FlatEntityMaps; }) => { const errors: FlatObjectMetadataValidationError[] = []; const { labelIdentifierFieldMetadataId, imageIdentifierFieldMetadataId } = flatObjectMetadata; + // TODO should not be nullable if (isDefined(labelIdentifierFieldMetadataId)) { - const flatFieldMetadata = objectFlatFieldMetadatas.find( - (flatFieldMetadata) => - flatFieldMetadata.id === labelIdentifierFieldMetadataId, - ); + const flatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: labelIdentifierFieldMetadataId, + flatEntityMaps: flatFieldMetadataMaps, + }); if (!isDefined(flatFieldMetadata)) { errors.push({ @@ -48,10 +51,10 @@ export const validateFlatObjectMetadataIdentifiers = ({ } if (isDefined(imageIdentifierFieldMetadataId)) { - const relatedFlatFieldMetadata = objectFlatFieldMetadatas.find( - (flatFieldMetadata) => - flatFieldMetadata.id === imageIdentifierFieldMetadataId, - ); + const relatedFlatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: imageIdentifierFieldMetadataId, + flatEntityMaps: flatFieldMetadataMaps, + }); if (!isDefined(relatedFlatFieldMetadata)) { errors.push({ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts new file mode 100644 index 0000000000..fe576887ce --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts @@ -0,0 +1,62 @@ +import { t } from '@lingui/core/macro'; + +import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; +import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { areFlatObjectMetadataNamesSyncedWithLabels } from 'src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util'; +import { validateFlatObjectMetadataLabel } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util'; +import { validateFlatObjectMetadataNames } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util'; +import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { doesOtherObjectWithSameNameExists } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util'; + +export const validateFlatObjectMetadataNameAndLabels = ({ + optimisticFlatObjectMetadataMaps, + flatObjectMetadataToValidate, +}: { + flatObjectMetadataToValidate: FlatObjectMetadata; + optimisticFlatObjectMetadataMaps: FlatEntityMaps; +}): FlatObjectMetadataValidationError[] => { + const errors: FlatObjectMetadataValidationError[] = []; + + errors.push( + ...validateFlatObjectMetadataNames({ + namePlural: flatObjectMetadataToValidate.namePlural, + nameSingular: flatObjectMetadataToValidate.nameSingular, + }), + ); + + errors.push( + ...validateFlatObjectMetadataLabel({ + labelPlural: flatObjectMetadataToValidate.labelPlural, + labelSingular: flatObjectMetadataToValidate.labelSingular, + }), + ); + + if ( + flatObjectMetadataToValidate.isLabelSyncedWithName && + !areFlatObjectMetadataNamesSyncedWithLabels(flatObjectMetadataToValidate) + ) { + errors.push({ + code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + message: t`Names are not synced with labels`, + userFriendlyMessage: t`Names are not synced with labels`, + }); + } + + if ( + doesOtherObjectWithSameNameExists({ + objectMetadataNamePlural: flatObjectMetadataToValidate.namePlural, + objectMetadataNameSingular: flatObjectMetadataToValidate.nameSingular, + objectMetadataMaps: optimisticFlatObjectMetadataMaps, + existingObjectMetadataId: flatObjectMetadataToValidate.id, + }) + ) { + errors.push({ + code: ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS, + message: 'Object already exists', + userFriendlyMessage: t`Object already exists`, + }); + } + + return errors; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts index ea05dfb6cd..3e0583aba4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts @@ -14,7 +14,7 @@ import { FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view import { fromCreateViewFieldInputToFlatViewFieldToCreate } from 'src/engine/core-modules/view/flat-view/utils/from-create-view-field-input-to-flat-view-field-to-create.util'; import { fromCreateViewInputToFlatViewToCreate } from 'src/engine/core-modules/view/flat-view/utils/from-create-view-input-to-flat-view-to-create.util'; import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { findObjectFieldsInFlatFieldMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; +import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util'; import { fromDeleteObjectInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util'; @@ -353,11 +353,10 @@ export class ObjectMetadataServiceV2 { flatEntityMaps: existingFlatViewMaps, }); - const { objectFlatFieldMetadatas } = - findObjectFieldsInFlatFieldMetadataMapsOrThrow({ - flatFieldMetadataMaps: toFlatFieldMetadataMaps, - flatObjectMetadata: flatObjectMetadataToCreate, - }); + const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({ + flatFieldMetadataMaps: toFlatFieldMetadataMaps, + flatObjectMetadata: flatObjectMetadataToCreate, + }); const flatDefaultViewFieldsToCreate = await this.createDefaultFlatViewFields({ objectFlatFieldMetadatas, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts index 5a4e5933b1..321b1da1ed 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts @@ -12,13 +12,13 @@ import { } from 'typeorm'; import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { type ObjectStandardOverridesDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-standard-overrides.dto'; import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; -import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; @Entity('objectMetadata') @Unique('IDX_OBJECT_METADATA_NAME_SINGULAR_WORKSPACE_ID_UNIQUE', [ @@ -96,6 +96,7 @@ export class ObjectMetadataEntity implements Required { @Column({ nullable: true, type: 'varchar' }) shortcut: string | null; + // TODO: This should not be nullable - legacy field introduced when label identifier was nullable @Column({ nullable: true, type: 'uuid' }) labelIdentifierFieldMetadataId: string | null; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts index ca98135236..b2056bab83 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts @@ -114,12 +114,23 @@ export class WorkspaceMigrationBuildOrchestratorService { await this.workspaceMigrationV2ObjectActionsBuilderService.validateAndBuild( { buildOptions, - dependencyOptimisticFlatEntityMaps: undefined, - // TODO That's hacky also not reliable ? - // dependencyOptimisticFlatEntityMaps: { - // flatFieldMetadataMaps: - // flatFieldMetadataMaps?.to ?? EMPTY_FLAT_ENTITY_MAPS, - // }, + // Note: That's a hacky way to allow validating object against field metadatas, not optimal + dependencyOptimisticFlatEntityMaps: { + flatFieldMetadataMaps: { + byId: { + ...dependencyAllFlatEntityMaps?.flatFieldMetadataMaps?.byId, + ...flatFieldMetadataMaps?.from.byId, + ...flatFieldMetadataMaps?.to.byId, + }, + idByUniversalIdentifier: { + ...dependencyAllFlatEntityMaps?.flatFieldMetadataMaps + ?.idByUniversalIdentifier, + ...flatFieldMetadataMaps?.from.idByUniversalIdentifier, + ...flatFieldMetadataMaps?.to.idByUniversalIdentifier, + }, + }, + }, + /// from: fromFlatObjectMetadataMaps, to: toFlatObjectMetadataMaps, workspaceId, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service.ts index 43a13f4191..f772b4cd13 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { compareTwoFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/compare-two-flat-object-metadata.util'; import { @@ -13,13 +14,13 @@ import { WorkspaceMigrationObjectActionV2, } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2'; import { FlatObjectMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-object-metadata-validator.service'; -// export type ObjectMetadataRelatedFlatEntityMaps = Pick< -// AllFlatEntityMaps, -// 'flatFieldMetadataMaps' -// >; +export type ObjectMetadataRelatedFlatEntityMaps = Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' +>; // TODO find a solution in order to handle label indentifier validation -export type ObjectMetadataRelatedFlatEntityMaps = undefined; +// export type ObjectMetadataRelatedFlatEntityMaps = undefined; @Injectable() export class WorkspaceMigrationV2ObjectActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service< @@ -134,12 +135,12 @@ export class WorkspaceMigrationV2ObjectActionsBuilderService extends WorkspaceEn > | undefined > { - const objectMetadataUpdatedProperties = compareTwoFlatObjectMetadata({ + const flatObjectPropertiesUpdates = compareTwoFlatObjectMetadata({ fromFlatObjectMetadata, toFlatObjectMetadata, }); - if (objectMetadataUpdatedProperties.length === 0) { + if (flatObjectPropertiesUpdates.length === 0) { return undefined; } @@ -149,6 +150,7 @@ export class WorkspaceMigrationV2ObjectActionsBuilderService extends WorkspaceEn flatObjectMetadataToValidate: toFlatObjectMetadata, optimisticFlatObjectMetadataMaps, dependencyOptimisticFlatEntityMaps, + flatObjectPropertiesUpdates: flatObjectPropertiesUpdates, }); if (validationResult.errors.length > 0) { @@ -161,7 +163,7 @@ export class WorkspaceMigrationV2ObjectActionsBuilderService extends WorkspaceEn const updateObjectAction: UpdateObjectAction = { type: 'update_object', objectMetadataId: toFlatObjectMetadata.id, - updates: objectMetadataUpdatedProperties, + updates: flatObjectPropertiesUpdates, }; return { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2.ts index 6a2be09bbf..7b6cfee58a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2.ts @@ -9,17 +9,19 @@ export type CreateObjectAction = { flatFieldMetadatas: FlatFieldMetadata[]; }; +export type FlatObjectPropertiesUpdates = Array< + { + [P in FlatObjectMetadataPropertiesToCompare]: PropertyUpdate< + FlatObjectMetadata, + P + >; + }[FlatObjectMetadataPropertiesToCompare] +>; + export type UpdateObjectAction = { type: 'update_object'; objectMetadataId: string; - updates: Array< - { - [P in FlatObjectMetadataPropertiesToCompare]: PropertyUpdate< - FlatObjectMetadata, - P - >; - }[FlatObjectMetadataPropertiesToCompare] - >; + updates: FlatObjectPropertiesUpdates; }; export type DeleteObjectAction = { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-object-metadata-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-object-metadata-validator.service.ts index 7f708253f5..dce73b2b1b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-object-metadata-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-object-metadata-validator.service.ts @@ -4,17 +4,15 @@ import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; import { FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; -import { FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type'; import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { areFlatObjectMetadataNamesSyncedWithLabels } from 'src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util'; -import { validateFlatObjectMetadataLabel } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util'; -import { validateFlatObjectMetadataNames } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.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 { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util'; -import { doesOtherObjectWithSameNameExists } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util'; import { ObjectMetadataRelatedFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service'; import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type'; import { WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-builder-options.type'; +import { FlatObjectPropertiesUpdates } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2'; export type ValidateOneObjectMetadataArgs = { flatObjectMetadataToValidate: FlatObjectMetadata; @@ -28,8 +26,11 @@ export class FlatObjectMetadataValidatorService { public validateFlatObjectMetadataUpdate({ optimisticFlatObjectMetadataMaps, flatObjectMetadataToValidate: updatedFlatObjectMetadata, - // dependencyOptimisticFlatEntityMaps: { flatFieldMetadataMaps }, - }: ValidateOneObjectMetadataArgs): FailedFlatEntityValidation { + dependencyOptimisticFlatEntityMaps: { flatFieldMetadataMaps }, + flatObjectPropertiesUpdates, + }: ValidateOneObjectMetadataArgs & { + flatObjectPropertiesUpdates: FlatObjectPropertiesUpdates; + }): FailedFlatEntityValidation { const validationResult: FailedFlatEntityValidation = { type: 'update_object', errors: [], @@ -57,24 +58,34 @@ export class FlatObjectMetadataValidatorService { }; validationResult.errors.push( - ...this.validateFlatObjectMetadataNameAndLabels({ + ...validateFlatObjectMetadataNameAndLabels({ optimisticFlatObjectMetadataMaps, flatObjectMetadataToValidate: updatedFlatObjectMetadata, }), ); - // const { objectFlatFieldMetadatas } = - // findObjectFieldsInFlatFieldMetadataMapsOrThrow({ - // flatFieldMetadataMaps, - // flatObjectMetadata: existingFlatObjectMetadata, - // }); + const labelIdentifierFieldMetadataIdUpdate = + flatObjectPropertiesUpdates.find( + (update) => update.property === 'labelIdentifierFieldMetadataId', + ); - // validationResult.errors.push( - // ...validateFlatObjectMetadataIdentifiers({ - // flatObjectMetadata: existingFlatObjectMetadata, - // objectFlatFieldMetadatas, - // }), - // ); + // TODO remove this once we migrated labelIdentifierFieldMetadataId as non nullable + if (isDefined(labelIdentifierFieldMetadataIdUpdate)) { + if (!isDefined(labelIdentifierFieldMetadataIdUpdate.to)) { + validationResult.errors.push({ + code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + message: 'labelIdentifierFieldMetadataId cannot be null', + userFriendlyMessage: t`Field label identifier is required`, + }); + } + + validationResult.errors.push( + ...validateFlatObjectMetadataIdentifiers({ + flatObjectMetadata: updatedFlatObjectMetadata, + flatFieldMetadataMaps, + }), + ); + } return validationResult; } @@ -142,7 +153,7 @@ export class FlatObjectMetadataValidatorService { public async validateFlatObjectMetadataCreation({ optimisticFlatObjectMetadataMaps, flatObjectMetadataToValidate, - // dependencyOptimisticFlatEntityMaps: { flatFieldMetadataMaps }, + dependencyOptimisticFlatEntityMaps: { flatFieldMetadataMaps }, }: ValidateOneObjectMetadataArgs): Promise< FailedFlatEntityValidation > { @@ -177,20 +188,14 @@ export class FlatObjectMetadataValidatorService { }); } - // const { objectFlatFieldMetadatas } = - // findObjectFieldsInFlatFieldMetadataMapsOrThrow({ - // flatFieldMetadataMaps, - // flatObjectMetadata: flatObjectMetadataToValidate, - // }); - - // objectValidationResult.errors.push( - // ...validateFlatObjectMetadataIdentifiers({ - // flatObjectMetadata: flatObjectMetadataToValidate, - // objectFlatFieldMetadatas, - // }), - // ); objectValidationResult.errors.push( - ...this.validateFlatObjectMetadataNameAndLabels({ + ...validateFlatObjectMetadataIdentifiers({ + flatObjectMetadata: flatObjectMetadataToValidate, + flatFieldMetadataMaps, + }), + ); + objectValidationResult.errors.push( + ...validateFlatObjectMetadataNameAndLabels({ optimisticFlatObjectMetadataMaps, flatObjectMetadataToValidate, }), @@ -198,56 +203,4 @@ export class FlatObjectMetadataValidatorService { return objectValidationResult; } - - private validateFlatObjectMetadataNameAndLabels({ - optimisticFlatObjectMetadataMaps, - flatObjectMetadataToValidate, - }: { - flatObjectMetadataToValidate: FlatObjectMetadata; - optimisticFlatObjectMetadataMaps: FlatEntityMaps; - }): FlatObjectMetadataValidationError[] { - const errors: FlatObjectMetadataValidationError[] = []; - - errors.push( - ...validateFlatObjectMetadataNames({ - namePlural: flatObjectMetadataToValidate.namePlural, - nameSingular: flatObjectMetadataToValidate.nameSingular, - }), - ); - - errors.push( - ...validateFlatObjectMetadataLabel({ - labelPlural: flatObjectMetadataToValidate.labelPlural, - labelSingular: flatObjectMetadataToValidate.labelSingular, - }), - ); - - if ( - flatObjectMetadataToValidate.isLabelSyncedWithName && - !areFlatObjectMetadataNamesSyncedWithLabels(flatObjectMetadataToValidate) - ) { - errors.push({ - code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - message: t`Names are not synced with labels`, - userFriendlyMessage: t`Names are not synced with labels`, - }); - } - - if ( - doesOtherObjectWithSameNameExists({ - objectMetadataNamePlural: flatObjectMetadataToValidate.namePlural, - objectMetadataNameSingular: flatObjectMetadataToValidate.nameSingular, - objectMetadataMaps: optimisticFlatObjectMetadataMaps, - existingObjectMetadataId: flatObjectMetadataToValidate.id, - }) - ) { - errors.push({ - code: ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS, - message: 'Object already exists', - userFriendlyMessage: t`Object already exists`, - }); - } - - return errors; - } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/delete-object-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/delete-object-action-handler.service.ts index 1fe1890044..202f8cfee5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/delete-object-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/delete-object-action-handler.service.ts @@ -8,7 +8,7 @@ import { import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { findObjectFieldsInFlatFieldMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; +import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util'; import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; @@ -90,11 +90,10 @@ export class DeleteObjectActionHandlerService extends WorkspaceMigrationRunnerAc schemaName, tableName, }); - const { objectFlatFieldMetadatas } = - findObjectFieldsInFlatFieldMetadataMapsOrThrow({ - flatFieldMetadataMaps, - flatObjectMetadata, - }); + const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({ + flatFieldMetadataMaps, + flatObjectMetadata, + }); const enumOrCompositeFlatFieldMetadatas = objectFlatFieldMetadatas.filter( (field) => isEnumFlatFieldMetadata(field) || isCompositeFlatFieldMetadata(field), diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/update-object-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/update-object-action-handler.service.ts index 5c2a0a51d5..47060ac900 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/update-object-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/update-object-action-handler.service.ts @@ -8,7 +8,7 @@ import { import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; -import { findObjectFieldsInFlatFieldMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; +import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util'; import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util'; import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; @@ -122,7 +122,7 @@ export class UpdateObjectActionHandlerService extends WorkspaceMigrationRunnerAc }); const { objectFlatFieldMetadatas } = - findObjectFieldsInFlatFieldMetadataMapsOrThrow({ + findObjectFlatFieldMetadatasOrThrow({ flatFieldMetadataMaps, flatObjectMetadata: updatedObjectMetadata, }); 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 58ecdb166e..36ddc0a97b 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 @@ -1,5 +1,113 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Object metadata update should fail when labelIdentifier is not a TEXT or NAME field 1`] = ` +[ + { + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "labelIdentifierFieldMetadataId validation failed: field type not compatible", + "userFriendlyMessage": "Field cannot be used as label identifier", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "namePlural": "whatevers", + "nameSingular": "whatever", + }, + "status": "fail", + "type": "update_object", + }, + ], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while updating object", + "name": "GraphQLError", + }, +] +`; + +exports[`Object metadata update should fail when labelIdentifier is not a known field metadata id 1`] = ` +[ + { + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "labelIdentifierFieldMetadataId validation failed: related field metadata not found", + "userFriendlyMessage": "Field declared as label identifier not found", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "namePlural": "whatevers", + "nameSingular": "whatever", + }, + "status": "fail", + "type": "update_object", + }, + ], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while updating object", + "name": "GraphQLError", + }, +] +`; + exports[`Object metadata update should fail when labelIdentifier is not a uuid 1`] = ` [ { @@ -15,3 +123,57 @@ exports[`Object metadata update should fail when labelIdentifier is not a uuid 1 }, ] `; + +exports[`Object metadata update should fail when labelIdentifier is null 1`] = ` +[ + { + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "labelIdentifierFieldMetadataId cannot be null", + "userFriendlyMessage": "Field label identifier is required", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "namePlural": "whatevers", + "nameSingular": "whatever", + }, + "status": "fail", + "type": "update_object", + }, + ], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while updating object", + "name": "GraphQLError", + }, +] +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-update-one-object-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-update-one-object-metadata.integration-spec.ts index cb9f650968..ebbf8b23e6 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-update-one-object-metadata.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-update-one-object-metadata.integration-spec.ts @@ -29,25 +29,24 @@ const labelIdentifierFailingTestsUseCase: CreateOneObjectMetadataItemTestingCont labelIdentifierFieldMetadataId: 'not-a-uuid', }, }, - // TODO prastoin fix label identifier validators - // { - // title: 'when labelIdentifier is not a known field metadata id', - // context: { - // labelIdentifierFieldMetadataId: '42422020-f49c-4159-8751-76a24f47b360', - // }, - // }, - // { - // title: 'when labelIdentifier is null', - // context: { - // labelIdentifierFieldMetadataId: null as any, - // }, - // }, - // { - // title: 'when labelIdentifier is not a TEXT or NAME field', - // context: ({ numberFieldMetadataId }) => ({ - // labelIdentifierFieldMetadataId: numberFieldMetadataId, - // }), - // }, + { + title: 'when labelIdentifier is not a known field metadata id', + context: { + labelIdentifierFieldMetadataId: '42422020-f49c-4159-8751-76a24f47b360', + }, + }, + { + title: 'when labelIdentifier is null', + context: { + labelIdentifierFieldMetadataId: null as any, + }, + }, + { + title: 'when labelIdentifier is not a TEXT or NAME field', + context: ({ numberFieldMetadataId }) => ({ + labelIdentifierFieldMetadataId: numberFieldMetadataId, + }), + }, ]; const allTestsUseCases = [...labelIdentifierFailingTestsUseCase];