diff --git a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts index c21be65e5a..dbab55f3a0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts @@ -70,7 +70,6 @@ export class CronTriggerV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, }, ); @@ -141,7 +140,6 @@ export class CronTriggerV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, }, ); @@ -218,7 +216,9 @@ export class CronTriggerV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: true, + inferDeletionFromMissingEntities: { + cronTrigger: true, + }, }, workspaceId, }, diff --git a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts index 6c9e6e8b3e..6bad026633 100644 --- a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts @@ -75,7 +75,6 @@ export class DatabaseEventTriggerV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, }, ); @@ -156,7 +155,6 @@ export class DatabaseEventTriggerV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, }, ); @@ -242,7 +240,9 @@ export class DatabaseEventTriggerV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: true, + inferDeletionFromMissingEntities: { + databaseEventTrigger: true, + }, }, workspaceId, }, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts index 9bff2c8658..037e5f55d2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts @@ -138,7 +138,10 @@ export class FieldMetadataServiceV2 { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: true, + inferDeletionFromMissingEntities: { + fieldMetadata: true, + index: true, + }, }, fromToAllFlatEntityMaps: { flatFieldMetadataMaps: { @@ -202,6 +205,8 @@ export class FieldMetadataServiceV2 { const { flatFieldMetadatasToUpdate: optimisticallyUpdatedFlatFieldMetadatas, flatIndexMetadatasToUpdate, + flatIndexMetadatasToDelete, + flatIndexMetadatasToCreate, } = inputTranspilationResult.result; const toFlatFieldMetadataMaps = @@ -219,16 +224,38 @@ export class FieldMetadataServiceV2 { }), ); - const toFlatIndexMaps = flatIndexMetadatasToUpdate.reduce( - (flatIndexMaps, flatIndexMetadata) => - replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: flatIndexMetadata, + const fromFlatIndexMaps = getSubFlatEntityMapsOrThrow({ + flatEntityIds: [ + ...flatIndexMetadatasToUpdate, + ...flatIndexMetadatasToDelete, + ].map(({ id }) => id), + flatEntityMaps: existingFlatIndexMaps, + }); + const toFlatIndexMapsWithCreatedFlatIndex = + flatIndexMetadatasToCreate.reduce( + (flatIndexMaps, flatIndex) => + addFlatEntityToFlatEntityMapsOrThrow({ + flatEntity: flatIndex, + flatEntityMaps: flatIndexMaps, + }), + fromFlatIndexMaps, + ); + const toFlatIndexMapsWithUpdatedFlatIndex = + flatIndexMetadatasToUpdate.reduce( + (flatIndexMaps, flatIndex) => + replaceFlatEntityInFlatEntityMapsOrThrow({ + flatEntity: flatIndex, + flatEntityMaps: flatIndexMaps, + }), + toFlatIndexMapsWithCreatedFlatIndex, + ); + const toFlatIndexMaps = flatIndexMetadatasToDelete.reduce( + (flatIndexMaps, flatIndex) => + deleteFlatEntityFromFlatEntityMapsOrThrow({ + entityToDeleteId: flatIndex.id, flatEntityMaps: flatIndexMaps, }), - getSubFlatEntityMapsOrThrow({ - flatEntityIds: flatIndexMetadatasToUpdate.map(({ id }) => id), - flatEntityMaps: existingFlatIndexMaps, - }), + toFlatIndexMapsWithUpdatedFlatIndex, ); const validateAndBuildResult = @@ -243,13 +270,15 @@ export class FieldMetadataServiceV2 { to: toFlatFieldMetadataMaps, }, flatIndexMaps: { - from: existingFlatIndexMaps, + from: fromFlatIndexMaps, to: toFlatIndexMaps, }, }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, + inferDeletionFromMissingEntities: { + index: true, + }, }, workspaceId, }, @@ -365,7 +394,6 @@ export class FieldMetadataServiceV2 { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, workspaceId, }, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception.ts index 9c76be4888..6256c9aa74 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception.ts @@ -1,15 +1,14 @@ -import { CustomException } from 'src/utils/custom-exception'; +import { + appendCommonExceptionCode, + CustomException, +} from 'src/utils/custom-exception'; -export class FlatEntityMapsException extends CustomException { - code: FlatEntityMapsExceptionCode; +export class FlatEntityMapsException extends CustomException< + keyof typeof FlatEntityMapsExceptionCode +> {} - constructor(message: string, code: FlatEntityMapsExceptionCode) { - super(message, code); - } -} - -export enum FlatEntityMapsExceptionCode { - ENTITY_ALREADY_EXISTS = 'ENTITY_ALREADY_EXISTS', - ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND', - ENTITY_MALFORMED = 'ENTITY_MALFORMED', -} +export const FlatEntityMapsExceptionCode = appendCommonExceptionCode({ + ENTITY_ALREADY_EXISTS: 'ENTITY_ALREADY_EXISTS', + ENTITY_NOT_FOUND: 'ENTITY_NOT_FOUND', + ENTITY_MALFORMED: 'ENTITY_MALFORMED', +} as const); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util.ts index 873ca0f100..d40ab260c4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util.ts @@ -17,6 +17,7 @@ import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { fromMorphRelationCreateFieldInputToFlatFieldMetadatas } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util'; import { fromRelationCreateFieldInputToFlatFieldMetadatas } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadatas.util'; +import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util'; import { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util'; import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; @@ -167,6 +168,18 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({ case FieldMetadataType.RICH_TEXT_V2: case FieldMetadataType.ACTOR: case FieldMetadataType.ARRAY: { + const indexMetadatas: FlatIndexMetadata[] = []; + + if (commonFlatFieldMetadata.isUnique) { + indexMetadatas.push( + generateIndexForFlatFieldMetadata({ + flatFieldMetadata: commonFlatFieldMetadata, + flatObjectMetadata: parentFlatObjectMetadata, + workspaceId, + }), + ); + } + return { status: 'success', result: { @@ -176,7 +189,7 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({ type: createFieldInput.type, }, ], - indexMetadatas: [], + indexMetadatas, }, }; } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts index ffb660f7f3..8d839d0859 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts @@ -13,20 +13,18 @@ import { } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { type FieldMetadataStandardOverridesProperties } from 'src/engine/metadata-modules/field-metadata/types/field-metadata-standard-overrides-properties.type'; import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; -import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant'; import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type'; import { type FlatFieldMetadataEditableProperties } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-editable-properties.constant'; 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 { recomputeIndexOnFlatFieldMetadataNameUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.util'; +import { handleIndexChangesDuringFieldUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util'; import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util'; type UpdatedFlatFieldMetadataAndIndexToUpdate = { flatFieldMetadata: FlatFieldMetadata; - flatIndexMetadataToUpdate: FlatIndexMetadata[]; }; type SanitizedUpdateFieldInput = ReturnType< @@ -50,66 +48,58 @@ const applyUpdatesToFlatFieldMetadata = ({ flatObjectMetadataMaps, flatIndexMaps, flatFieldMetadataMaps, -}: ApplyUpdatesToFlatFieldMetadataArgs) => { - return FLAT_FIELD_METADATA_EDITABLE_PROPERTIES.reduce( - ({ flatFieldMetadata, flatIndexMetadataToUpdate }, property) => { - const updatedPropertyValue = updatedEditableFieldProperties[property]; - const isPropertyUpdated = - updatedPropertyValue !== undefined && - flatFieldMetadata[property] !== updatedPropertyValue; +}: ApplyUpdatesToFlatFieldMetadataArgs): { + flatFieldMetadata: FlatFieldMetadata; + flatIndexMetadataToUpdate: FlatIndexMetadata[]; + flatIndexMetadatasToDelete: FlatIndexMetadata[]; + flatIndexMetadatasToCreate: FlatIndexMetadata[]; +} => { + const { flatFieldMetadata: updatedFlatFieldMetadata } = + FLAT_FIELD_METADATA_EDITABLE_PROPERTIES.reduce( + ({ flatFieldMetadata }, property) => { + const updatedPropertyValue = updatedEditableFieldProperties[property]; + const isPropertyUpdated = + updatedPropertyValue !== undefined && + flatFieldMetadata[property] !== updatedPropertyValue; - if (!isPropertyUpdated) { - return { - flatFieldMetadata, - flatIndexMetadataToUpdate, + if (!isPropertyUpdated) { + return { flatFieldMetadata }; + } + + const updatedFlatFieldMetadata = { + ...flatFieldMetadata, + [property]: updatedPropertyValue, }; - } - const updatedFlatFieldMetadata = { - ...flatFieldMetadata, - [property]: updatedPropertyValue, - }; - if (property === 'options') { - updatedFlatFieldMetadata.options = - updatedEditableFieldProperties[property]?.map((option) => ({ - id: v4(), - ...option, - })) ?? []; - } + if (property === 'options') { + updatedFlatFieldMetadata.options = + updatedEditableFieldProperties[property]?.map((option) => ({ + id: v4(), + ...option, + })) ?? []; + } - let newFlatIndexMetadataToUpdate: FlatIndexMetadata[] = []; + return { + flatFieldMetadata: updatedFlatFieldMetadata, + }; + }, + { + flatFieldMetadata: structuredClone(fromFlatFieldMetadata), + }, + ); - if (property === 'name') { - const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityMaps: flatObjectMetadataMaps, - flatEntityId: flatFieldMetadata.objectMetadataId, - }); + const indexChanges = handleIndexChangesDuringFieldUpdate({ + originalFlatFieldMetadata: fromFlatFieldMetadata, + updatedFlatFieldMetadata, + flatIndexMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + }); - newFlatIndexMetadataToUpdate = - recomputeIndexOnFlatFieldMetadataNameUpdate({ - flatFieldMetadataMaps, - flatObjectMetadata, - fromFlatFieldMetadata, - toFlatFieldMetadata: { - name: updatedFlatFieldMetadata.name, - }, - flatIndexMaps, - }); - } - - return { - flatFieldMetadata: updatedFlatFieldMetadata, - flatIndexMetadataToUpdate: [ - ...flatIndexMetadataToUpdate, - ...newFlatIndexMetadataToUpdate, - ], - }; - }, - { - flatFieldMetadata: structuredClone(fromFlatFieldMetadata), - flatIndexMetadataToUpdate: [], - }, - ); + return { + flatFieldMetadata: updatedFlatFieldMetadata, + ...indexChanges, + }; }; type FromUpdateFieldInputToFlatFieldMetadataArgs = { @@ -122,6 +112,8 @@ type FromUpdateFieldInputToFlatFieldMetadataArgs = { type FlatFieldMetadataAndIndexToUpdate = { flatFieldMetadatasToUpdate: FlatFieldMetadata[]; flatIndexMetadatasToUpdate: FlatIndexMetadata[]; + flatIndexMetadatasToDelete: FlatIndexMetadata[]; + flatIndexMetadatasToCreate: FlatIndexMetadata[]; }; export const fromUpdateFieldInputToFlatFieldMetadata = ({ flatIndexMaps, @@ -198,6 +190,8 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ result: { flatFieldMetadatasToUpdate: [updatedStandardFlatFieldMetadata], flatIndexMetadatasToUpdate: [], + flatIndexMetadatasToDelete: [], + flatIndexMetadatasToCreate: [], }, }; } @@ -229,14 +223,18 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ const optimisticiallyUpdatedFlatFieldMetadatas = flatFieldMetadatasToUpdate.reduce( (acc, fromFlatFieldMetadata) => { - const { flatFieldMetadata, flatIndexMetadataToUpdate } = - applyUpdatesToFlatFieldMetadata({ - flatObjectMetadataMaps: existingFlatObjectMetadataMaps, - fromFlatFieldMetadata, - flatFieldMetadataMaps, - flatIndexMaps, - updatedEditableFieldProperties, - }); + const { + flatFieldMetadata, + flatIndexMetadataToUpdate, + flatIndexMetadatasToDelete, + flatIndexMetadatasToCreate, + } = applyUpdatesToFlatFieldMetadata({ + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + fromFlatFieldMetadata, + flatFieldMetadataMaps, + flatIndexMaps, + updatedEditableFieldProperties, + }); return { flatFieldMetadatasToUpdate: [ @@ -247,11 +245,21 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ ...acc.flatIndexMetadatasToUpdate, ...flatIndexMetadataToUpdate, ], + flatIndexMetadatasToDelete: [ + ...acc.flatIndexMetadatasToDelete, + ...flatIndexMetadatasToDelete, + ], + flatIndexMetadatasToCreate: [ + ...acc.flatIndexMetadatasToCreate, + ...flatIndexMetadatasToCreate, + ], }; }, { flatFieldMetadatasToUpdate: [], flatIndexMetadatasToUpdate: [], + flatIndexMetadatasToDelete: [], + flatIndexMetadatasToCreate: [], }, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util.ts new file mode 100644 index 0000000000..5bf0d2fe25 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util.ts @@ -0,0 +1,53 @@ +import { v4 } from 'uuid'; + +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +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 { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types'; +import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util'; + +type GenerateIndexForFlatFieldMetadataArgs = { + flatFieldMetadata: FlatFieldMetadata; + flatObjectMetadata: FlatObjectMetadata; + workspaceId: string; +}; + +export const generateIndexForFlatFieldMetadata = ({ + flatFieldMetadata, + flatObjectMetadata, + workspaceId, +}: GenerateIndexForFlatFieldMetadataArgs): FlatIndexMetadata => { + const indexId = v4(); + const createdAt = new Date(); + + const flatIndex: FlatIndexMetadata = generateFlatIndexMetadataWithNameOrThrow( + { + objectFlatFieldMetadatas: [flatFieldMetadata], + flatIndex: { + createdAt, + flatIndexFieldMetadatas: [ + { + createdAt, + fieldMetadataId: flatFieldMetadata.id, + id: v4(), + indexMetadataId: indexId, + order: 0, + updatedAt: createdAt, + }, + ], + id: indexId, + indexType: IndexType.BTREE, + indexWhereClause: null, + isCustom: true, + isUnique: flatFieldMetadata.isUnique ?? false, + objectMetadataId: flatObjectMetadata.id, + universalIdentifier: indexId, + updatedAt: createdAt, + workspaceId, + }, + flatObjectMetadata, + }, + ); + + return flatIndex; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts index 910988a923..552cbce825 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts @@ -5,11 +5,10 @@ import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadat import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type'; import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util'; import { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.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 { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types'; -import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util'; import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type'; import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; @@ -123,47 +122,17 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({ relationTargetObjectMetadataId: sourceFlatObjectMetadata.id, }; - const indexId = v4(); - const createdAt = new Date(); - const indexMetadata: FlatIndexMetadata = - generateFlatIndexMetadataWithNameOrThrow({ - objectFlatFieldMetadatas: - relationCreationPayload.type === RelationType.MANY_TO_ONE - ? [sourceFlatFieldMetadata] - : [targetFlatFieldMetadata], - flatIndex: { - createdAt, - flatIndexFieldMetadatas: [ - { - createdAt, - fieldMetadataId: - relationCreationPayload.type === RelationType.MANY_TO_ONE - ? sourceFlatFieldMetadata.id - : targetFlatFieldMetadata.id, - id: v4(), - indexMetadataId: indexId, - order: 0, - updatedAt: createdAt, - }, - ], - id: indexId, - indexType: IndexType.BTREE, - indexWhereClause: null, - isCustom: true, - isUnique: false, - objectMetadataId: - relationCreationPayload.type === RelationType.MANY_TO_ONE - ? sourceFlatObjectMetadata.id - : targetFlatObjectMetadata.id, - universalIdentifier: indexId, - updatedAt: createdAt, - workspaceId, - }, - flatObjectMetadata: - relationCreationPayload.type === RelationType.MANY_TO_ONE - ? sourceFlatObjectMetadata - : targetFlatObjectMetadata, - }); + const indexMetadata: FlatIndexMetadata = generateIndexForFlatFieldMetadata({ + flatFieldMetadata: + relationCreationPayload.type === RelationType.MANY_TO_ONE + ? sourceFlatFieldMetadata + : targetFlatFieldMetadata, + flatObjectMetadata: + relationCreationPayload.type === RelationType.MANY_TO_ONE + ? sourceFlatObjectMetadata + : targetFlatObjectMetadata, + workspaceId, + }); return { flatFieldMetadatas: [sourceFlatFieldMetadata, targetFlatFieldMetadata], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util.ts new file mode 100644 index 0000000000..ffaabc99f3 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util.ts @@ -0,0 +1,176 @@ +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-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 { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util'; +import { recomputeIndexOnFlatFieldMetadataNameUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.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'; + +type IndexChanges = { + flatIndexMetadataToUpdate: FlatIndexMetadata[]; + flatIndexMetadatasToDelete: FlatIndexMetadata[]; + flatIndexMetadatasToCreate: FlatIndexMetadata[]; +}; + +const NO_INDEX_CHANGES: IndexChanges = { + flatIndexMetadataToUpdate: [], + flatIndexMetadatasToDelete: [], + flatIndexMetadatasToCreate: [], +}; + +export const handleIndexChangesDuringFieldUpdate = ({ + originalFlatFieldMetadata, + updatedFlatFieldMetadata, + flatIndexMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, +}: { + originalFlatFieldMetadata: FlatFieldMetadata; + updatedFlatFieldMetadata: FlatFieldMetadata; + flatIndexMaps: FlatEntityMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; +}): IndexChanges => { + if ( + !hasIndexRelevantChanges({ + originalFlatFieldMetadata, + updatedFlatFieldMetadata, + }) + ) { + return NO_INDEX_CHANGES; + } + + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: originalFlatFieldMetadata.objectMetadataId, + }); + + const relatedIndexes = findRelatedIndexes({ + flatFieldMetadata: originalFlatFieldMetadata, + flatObjectMetadata, + flatIndexMaps, + }); + + if (relatedIndexes.length === 0) { + return handleNoExistingIndexes({ + updatedFlatFieldMetadata, + flatObjectMetadata, + }); + } + + return handleExistingIndexes({ + updatedFlatFieldMetadata, + originalFlatFieldMetadata, + relatedIndexes, + flatObjectMetadata, + flatFieldMetadataMaps, + }); +}; + +const hasIndexRelevantChanges = ({ + originalFlatFieldMetadata, + updatedFlatFieldMetadata, +}: { + originalFlatFieldMetadata: FlatFieldMetadata; + updatedFlatFieldMetadata: FlatFieldMetadata; +}): boolean => + originalFlatFieldMetadata.name !== updatedFlatFieldMetadata.name || + originalFlatFieldMetadata.isUnique !== updatedFlatFieldMetadata.isUnique; + +const findRelatedIndexes = ({ + flatFieldMetadata, + flatObjectMetadata, + flatIndexMaps, +}: { + flatFieldMetadata: FlatFieldMetadata; + flatObjectMetadata: FlatObjectMetadata; + flatIndexMaps: FlatEntityMaps; +}): FlatIndexMetadata[] => { + const objectIndexes = findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatIndexMaps, + flatEntityIds: flatObjectMetadata.indexMetadataIds, + }); + + return objectIndexes.filter((index) => + index.flatIndexFieldMetadatas.some( + (indexField) => indexField.fieldMetadataId === flatFieldMetadata.id, + ), + ); +}; + +const handleNoExistingIndexes = ({ + updatedFlatFieldMetadata, + flatObjectMetadata, +}: { + updatedFlatFieldMetadata: FlatFieldMetadata; + flatObjectMetadata: FlatObjectMetadata; +}): IndexChanges => { + if (!updatedFlatFieldMetadata.isUnique) { + return NO_INDEX_CHANGES; + } + + const newIndex = generateIndexForFlatFieldMetadata({ + flatFieldMetadata: updatedFlatFieldMetadata, + flatObjectMetadata, + workspaceId: flatObjectMetadata.workspaceId, + }); + + return { + ...NO_INDEX_CHANGES, + flatIndexMetadatasToCreate: [newIndex], + }; +}; + +const handleExistingIndexes = ({ + updatedFlatFieldMetadata, + originalFlatFieldMetadata, + relatedIndexes, + flatObjectMetadata, + flatFieldMetadataMaps, +}: { + updatedFlatFieldMetadata: FlatFieldMetadata; + originalFlatFieldMetadata: FlatFieldMetadata; + relatedIndexes: FlatIndexMetadata[]; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: AllFlatEntityMaps['flatFieldMetadataMaps']; +}): IndexChanges => { + if (updatedFlatFieldMetadata.isUnique === false) { + const expectedUniqueIndex = generateIndexForFlatFieldMetadata({ + flatFieldMetadata: { + ...originalFlatFieldMetadata, + isUnique: true, + }, + flatObjectMetadata, + workspaceId: flatObjectMetadata.workspaceId, + }); + + const uniqueIndexToDelete = relatedIndexes.find( + (index) => index.name === expectedUniqueIndex.name, + ); + + return { + ...NO_INDEX_CHANGES, + flatIndexMetadatasToDelete: uniqueIndexToDelete + ? [uniqueIndexToDelete] + : [], + }; + } + + const updatedIndexes = recomputeIndexOnFlatFieldMetadataNameUpdate({ + flatFieldMetadataMaps, + flatObjectMetadata, + fromFlatFieldMetadata: originalFlatFieldMetadata, + toFlatFieldMetadata: { + name: updatedFlatFieldMetadata.name, + isUnique: updatedFlatFieldMetadata.isUnique, + }, + relatedFlatIndexMetadata: relatedIndexes, + }); + + return { + ...NO_INDEX_CHANGES, + flatIndexMetadataToUpdate: updatedIndexes, + }; +}; 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 6835c59aa6..892b33fb58 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 @@ -1,5 +1,3 @@ -import { isDefined } from 'twenty-shared/utils'; - import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-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'; @@ -10,27 +8,17 @@ import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-mo type RecomputeIndexOnFlatFieldMetadataNameUpdateArgs = { flatObjectMetadata: FlatObjectMetadata; fromFlatFieldMetadata: FlatFieldMetadata; - toFlatFieldMetadata: Pick; -} & Pick; + toFlatFieldMetadata: Pick; + relatedFlatIndexMetadata: FlatIndexMetadata[]; +} & Pick; export const recomputeIndexOnFlatFieldMetadataNameUpdate = ({ fromFlatFieldMetadata, toFlatFieldMetadata, flatObjectMetadata, - flatIndexMaps, flatFieldMetadataMaps, + relatedFlatIndexMetadata, }: RecomputeIndexOnFlatFieldMetadataNameUpdateArgs): FlatIndexMetadata[] => { - const relatedFlatIndexMetadata = Object.values(flatIndexMaps.byId).filter( - (flatIndexMetadata): flatIndexMetadata is FlatIndexMetadata => - isDefined(flatIndexMetadata) && - flatIndexMetadata.objectMetadataId === - fromFlatFieldMetadata.objectMetadataId && - flatIndexMetadata.flatIndexFieldMetadatas.some( - (flatIndexField) => - flatIndexField.fieldMetadataId === fromFlatFieldMetadata.id, - ), - ); - if (relatedFlatIndexMetadata.length === 0) { return []; } @@ -46,6 +34,7 @@ export const recomputeIndexOnFlatFieldMetadataNameUpdate = ({ return { ...flatFieldMetadata, name: toFlatFieldMetadata.name, + isUnique: toFlatFieldMetadata.isUnique, }; } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/exceptions/index-exception-code.ts b/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/exceptions/index-exception-code.ts index 338c5a5719..de9f39a43c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/exceptions/index-exception-code.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/exceptions/index-exception-code.ts @@ -7,4 +7,6 @@ export enum IndexExceptionCode { INDEX_ALREADY_EXISTS = 'INDEX_ALREADY_EXISTS', INDEX_NOT_FOUND = 'INDEX_NOT_FOUND', INDEX_OBJECT_NOT_FOUND = 'INDEX_OBJECT_NOT_FOUND', + INDEX_FIELD_INVALID_TYPE_FOR_UNIQUE = 'INDEX_FIELD_INVALID_TYPE_FOR_UNIQUE', + INDEX_FIELD_INVALID_DEFAULT_VALUE = 'INDEX_FIELD_INVALID_DEFAULT_VALUE', } diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util.ts index f74653e457..a4804f16f9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util.ts @@ -20,7 +20,7 @@ export const generateFlatIndexMetadataWithNameOrThrow = ({ objectFlatFieldMetadatas, flatIndex, }: GenerateFlatIndexArgs): FlatIndexMetadata => { - const orderedFlatFieldNames = flatIndex.flatIndexFieldMetadatas + const orderedFlatFields = flatIndex.flatIndexFieldMetadatas .sort((a, b) => a.order - b.order) .map((flatIndexField) => { const relatedFlatFieldMetadata = objectFlatFieldMetadatas.find( @@ -42,17 +42,23 @@ export const generateFlatIndexMetadataWithNameOrThrow = ({ return { name, + isUnique: relatedFlatFieldMetadata.isUnique, }; }); + const isUnique = orderedFlatFields.some((flatField) => flatField.isUnique); + const orderedFlatFieldNames = orderedFlatFields.map((flatField) => ({ + name: flatField.name, + })); const name = generateDeterministicIndexNameV2({ flatObjectMetadata, - isUnique: flatIndex.isUnique, relatedFieldNames: orderedFlatFieldNames, + isUnique, }); return { ...flatIndex, name, + isUnique, }; }; 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 5d3429c180..e3a47c8c6a 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 @@ -162,7 +162,6 @@ export class ObjectMetadataServiceV2 { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, workspaceId, }, @@ -307,7 +306,11 @@ export class ObjectMetadataServiceV2 { }, }, buildOptions: { - inferDeletionFromMissingEntities: true, + inferDeletionFromMissingEntities: { + objectMetadata: true, + fieldMetadata: true, + index: true, + }, isSystemBuild, }, workspaceId, @@ -449,7 +452,6 @@ export class ObjectMetadataServiceV2 { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, workspaceId, }, diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts index 6dda77c75c..411ebd52b2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts @@ -69,7 +69,6 @@ export class RouteTriggerV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, }, ); @@ -140,7 +139,6 @@ export class RouteTriggerV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, }, ); @@ -217,7 +215,9 @@ export class RouteTriggerV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: true, + inferDeletionFromMissingEntities: { + routeTrigger: true, + }, }, workspaceId, }, diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service.ts index a88f79493d..56de15a3d4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service.ts @@ -71,7 +71,6 @@ export class ServerlessFunctionV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, }, ); @@ -142,7 +141,6 @@ export class ServerlessFunctionV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, }, ); @@ -218,7 +216,6 @@ export class ServerlessFunctionV2Service { }, }, buildOptions: { - inferDeletionFromMissingEntities: false, isSystemBuild, }, workspaceId, @@ -298,7 +295,9 @@ export class ServerlessFunctionV2Service { }, buildOptions: { isSystemBuild, - inferDeletionFromMissingEntities: true, + inferDeletionFromMissingEntities: { + serverlessFunction: true, + }, }, workspaceId, }, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field-v2.service.ts index f12c0e21e5..945c65225e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field-v2.service.ts @@ -79,7 +79,6 @@ export class ViewFieldV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, workspaceId, }, @@ -154,7 +153,6 @@ export class ViewFieldV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, workspaceId, }, @@ -229,7 +227,6 @@ export class ViewFieldV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, workspaceId, }, @@ -303,7 +300,9 @@ export class ViewFieldV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: true, + inferDeletionFromMissingEntities: { + viewField: true, + }, }, workspaceId, }, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-filter/services/view-filter-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-filter/services/view-filter-v2.service.ts index 7e6baa0d58..98e9164424 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-filter/services/view-filter-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-filter/services/view-filter-v2.service.ts @@ -79,7 +79,6 @@ export class ViewFilterV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, workspaceId, }, @@ -154,7 +153,6 @@ export class ViewFilterV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, workspaceId, }, @@ -229,7 +227,6 @@ export class ViewFilterV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, workspaceId, }, @@ -308,7 +305,9 @@ export class ViewFilterV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: true, + inferDeletionFromMissingEntities: { + viewFilter: true, + }, }, workspaceId, }, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group-v2.service.ts index acdd581d3c..29beb5d62d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group-v2.service.ts @@ -76,7 +76,6 @@ export class ViewGroupV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, workspaceId, }, @@ -151,7 +150,6 @@ export class ViewGroupV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, workspaceId, }, @@ -226,7 +224,6 @@ export class ViewGroupV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, workspaceId, }, @@ -305,7 +302,9 @@ export class ViewGroupV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: true, + inferDeletionFromMissingEntities: { + viewGroup: true, + }, }, workspaceId, }, diff --git a/packages/twenty-server/src/engine/metadata-modules/view/services/view-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/view/services/view-v2.service.ts index 0ed9e80d4e..cfb8c2f8e7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/services/view-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/services/view-v2.service.ts @@ -66,7 +66,6 @@ export class ViewV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, workspaceId, }, @@ -134,7 +133,6 @@ export class ViewV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: false, }, workspaceId, }, @@ -201,7 +199,6 @@ export class ViewV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: true, }, workspaceId, }, @@ -268,7 +265,9 @@ export class ViewV2Service { }, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingEntities: true, + inferDeletionFromMissingEntities: { + view: true, + }, }, workspaceId, }, diff --git a/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/utils/build-sql-column-definition.util.ts b/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/utils/build-sql-column-definition.util.ts index 9a733fca12..a5eeee04f2 100644 --- a/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/utils/build-sql-column-definition.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/utils/build-sql-column-definition.util.ts @@ -26,10 +26,6 @@ export const buildSqlColumnDefinition = ( parts.push('NOT NULL'); } - if (column.isUnique) { - parts.push('UNIQUE'); - } - if (isDefined(column.default) && column.type !== 'tsvector') { parts.push(`DEFAULT ${column.default}`); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/flat-entity-deleted-created-updated-matrix-dispatcher.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/flat-entity-deleted-created-updated-matrix-dispatcher.util.ts index e761274951..9d4dd2d60b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/flat-entity-deleted-created-updated-matrix-dispatcher.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/flat-entity-deleted-created-updated-matrix-dispatcher.util.ts @@ -1,7 +1,7 @@ import { type FromTo } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { type ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY } from 'src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant'; +import { ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY } from 'src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant'; import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/metadata-modules/flat-entity/constant/empty-flat-entity-maps.constant'; import { type AllMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-metadata-name.type'; import { type FlatEntityPropertiesToCompare } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-to-compare.type'; @@ -33,11 +33,8 @@ export type UniversalIdentifierItem = { type FlatEntityDeletedCreatedUpdatedMatrixDispatcherArgs< T extends AllMetadataName, > = FromTo[]> & { + metadataName: T; buildOptions: WorkspaceMigrationBuilderOptions; - comparisonOptions: Pick< - (typeof ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY)[T], - 'propertiesToCompare' | 'propertiesToStringify' - >; }; export const flatEntityDeletedCreatedUpdatedMatrixDispatcher = < @@ -45,8 +42,8 @@ export const flatEntityDeletedCreatedUpdatedMatrixDispatcher = < >({ from, to, + metadataName, buildOptions, - comparisonOptions: { propertiesToCompare, propertiesToStringify }, }: FlatEntityDeletedCreatedUpdatedMatrixDispatcherArgs): DeletedCreatedUpdatedMatrix => { const initialDispatcher: DeletedCreatedUpdatedMatrix = { createdFlatEntityMaps: EMPTY_FLAT_ENTITY_MAPS, @@ -57,7 +54,7 @@ export const flatEntityDeletedCreatedUpdatedMatrixDispatcher = < const fromMap = new Map(from.map((obj) => [obj.universalIdentifier, obj])); const toMap = new Map(to.map((obj) => [obj.universalIdentifier, obj])); - if (buildOptions.inferDeletionFromMissingEntities) { + if (buildOptions.inferDeletionFromMissingEntities?.[metadataName]) { for (const [universalIdentifier, fromEntity] of fromMap) { if (toMap.has(universalIdentifier)) { continue; @@ -81,6 +78,9 @@ export const flatEntityDeletedCreatedUpdatedMatrixDispatcher = < }); } + const { propertiesToCompare, propertiesToStringify } = + ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY[metadataName]; + for (const [universalIdentifier, fromFlatEntity] of fromMap) { const toFlatEntity = toMap.get(universalIdentifier); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service.ts index ae0bf8f8ea..689e11702e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service.ts @@ -4,7 +4,6 @@ import { type FromTo } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { LoggerService } from 'src/engine/core-modules/logger/logger.service'; -import { ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY } from 'src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant'; import { FlatEntityMapsException, FlatEntityMapsExceptionCode, @@ -74,15 +73,8 @@ export abstract class WorkspaceEntityMigrationBuilderV2Service< } = flatEntityDeletedCreatedUpdatedMatrixDispatcher({ from: fromFlatEntities, to: toFlatEntities, + metadataName: this.metadataName, buildOptions, - comparisonOptions: { - propertiesToCompare: - ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY[this.metadataName] - .propertiesToCompare, - propertiesToStringify: - ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY[this.metadataName] - .propertiesToStringify, - }, }); this.logger.timeEnd( @@ -163,7 +155,8 @@ export abstract class WorkspaceEntityMigrationBuilderV2Service< deletedFlatEntityMaps, ); - for (const flatEntityToDeleteId in buildOptions.inferDeletionFromMissingEntities + for (const flatEntityToDeleteId in buildOptions + .inferDeletionFromMissingEntities?.[this.metadataName] ? deletedFlatEntityMaps.byId : {}) { const flatEntityToDelete = diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-builder-options.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-builder-options.type.ts index 3d91ca8569..5686fc22ad 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-builder-options.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-builder-options.type.ts @@ -1,4 +1,6 @@ +import { type AllMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-metadata-name.type'; + export type WorkspaceMigrationBuilderOptions = { - inferDeletionFromMissingEntities: boolean; + inferDeletionFromMissingEntities?: Partial>; isSystemBuild: boolean; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-index-metadata-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-index-metadata-validator.service.ts index 49e90c7a6b..730dd2cf59 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-index-metadata-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-index-metadata-validator.service.ts @@ -1,11 +1,15 @@ import { Injectable } from '@nestjs/common'; import { msg, t } from '@lingui/core/macro'; +import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { isValidUniqueFieldDefaultValueCombination } from 'src/engine/metadata-modules/field-metadata/utils/is-valid-unique-input.util'; import { ALL_METADATA_NAME } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-name.constant'; import { FlatEntityMapsExceptionCode } from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util'; import { IndexExceptionCode } from 'src/engine/metadata-modules/flat-index-metadata/exceptions/index-exception-code'; import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type'; @@ -133,6 +137,51 @@ export class FlatIndexValidatorService { userFriendlyMessage: msg`Field cannot be indexed as it belongs to a different object`, }); } + + if (flatIndexToValidate.isUnique) { + if ( + isDefined(relatedFlatField.defaultValue) && + !isValidUniqueFieldDefaultValueCombination({ + defaultValue: relatedFlatField.defaultValue, + isUnique: relatedFlatField.isUnique ?? false, + type: relatedFlatField.type, + }) + ) { + const fieldName = relatedFlatField.name; + const fieldType = relatedFlatField.type; + + validationResult.errors.push({ + code: IndexExceptionCode.INDEX_FIELD_INVALID_DEFAULT_VALUE, + message: t`Unique index cannot be created for field ${fieldName} of type ${fieldType}`, + userFriendlyMessage: msg`${fieldType} fields cannot have a default value.`, + }); + } + + const isCompositeFieldWithNonIncludedUniqueConstraint = + isCompositeFlatFieldMetadata(relatedFlatField) && + !compositeTypeDefinitions + .get(relatedFlatField.type) + ?.properties.some( + (property) => property.isIncludedInUniqueConstraint, + ); + + if ( + [ + FieldMetadataType.MORPH_RELATION, + FieldMetadataType.RELATION, + ].includes(relatedFlatField.type) || + isCompositeFieldWithNonIncludedUniqueConstraint + ) { + const fieldType = relatedFlatField.type; + const fieldName = relatedFlatField.name; + + validationResult.errors.push({ + code: IndexExceptionCode.INDEX_FIELD_INVALID_TYPE_FOR_UNIQUE, + message: t`Unique index cannot be created for field ${fieldName} of type ${fieldType}`, + userFriendlyMessage: msg`${fieldType} fields cannot be unique.`, + }); + } + } } if (flatIndexField.indexMetadataId !== flatIndexToValidate.id) { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/index/services/create-index-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/index/services/create-index-action-handler.service.ts index e8a1d7daa7..5ebf11a0f8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/index/services/create-index-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/index/services/create-index-action-handler.service.ts @@ -11,14 +11,20 @@ import { WorkspaceQueryRunnerException, WorkspaceQueryRunnerExceptionCode, } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; +import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; 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 { 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 { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { FlatIndexFieldMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service'; @@ -105,34 +111,10 @@ export class CreateIndexActionHandlerService extends WorkspaceMigrationRunnerAct flatObjectMetadata, }); - const quotedColumns = flatIndexMetadata.flatIndexFieldMetadatas.map( - ({ fieldMetadataId }) => { - const flatFieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityId: fieldMetadataId, - flatEntityMaps: flatFieldMetadataMaps, - }); - - if (!isDefined(flatFieldMetadata)) { - throw new FlatEntityMapsException( - 'Index field related field metadata not found', - FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, - ); - } - - if (isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) { - if (!isDefined(flatFieldMetadata.settings?.joinColumnName)) { - throw new FlatEntityMapsException( - 'Join column name is not defined for relation field', - FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, - ); - } - - return `"${flatFieldMetadata.settings.joinColumnName}"`; - } - - return `"${flatFieldMetadata.name}"`; - }, - ); + const quotedColumns = this.computeFlatIndexFieldColumnNames({ + flatIndexFieldMetadatas: flatIndexMetadata.flatIndexFieldMetadatas, + flatFieldMetadataMaps, + }); await this.workspaceSchemaManagerService.indexManager.createIndex({ index: { @@ -147,4 +129,60 @@ export class CreateIndexActionHandlerService extends WorkspaceMigrationRunnerAct tableName, }); } + + private computeFlatIndexFieldColumnNames({ + flatIndexFieldMetadatas, + flatFieldMetadataMaps, + }: { + flatIndexFieldMetadatas: FlatIndexFieldMetadata[]; + flatFieldMetadataMaps: FlatEntityMaps; + }): string[] { + return flatIndexFieldMetadatas.flatMap(({ fieldMetadataId }) => { + const flatFieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: fieldMetadataId, + flatEntityMaps: flatFieldMetadataMaps, + }); + + if (!isDefined(flatFieldMetadata)) { + throw new FlatEntityMapsException( + 'Index field related field metadata not found', + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + if (isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) { + if (!isDefined(flatFieldMetadata.settings?.joinColumnName)) { + throw new FlatEntityMapsException( + 'Join column name is not defined for relation field', + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + return flatFieldMetadata.settings.joinColumnName; + } + + if (isCompositeFieldMetadataType(flatFieldMetadata.type)) { + const compositeType = compositeTypeDefinitions.get( + flatFieldMetadata.type, + ); + + if (!compositeType) { + throw new FlatEntityMapsException( + 'Composite type not found', + FlatEntityMapsExceptionCode.INTERNAL_SERVER_ERROR, + ); + } + + const uniqueCompositeProperties = compositeType.properties.filter( + (property) => property.isIncludedInUniqueConstraint, + ); + + return uniqueCompositeProperties.map((subField) => + computeCompositeColumnName(flatFieldMetadata.name, subField), + ); + } + + return flatFieldMetadata.name; + }); + } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/__tests__/generate-column-definitions.util.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/__tests__/generate-column-definitions.util.spec.ts index 6b3738a257..4800282a8e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/__tests__/generate-column-definitions.util.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/__tests__/generate-column-definitions.util.spec.ts @@ -54,6 +54,7 @@ describe('Generate Column Definitions', () => { type: `"${mockSchemaName}"."_person_status_enum"`, isArray: false, isNullable: true, + isPrimary: false, isUnique: false, default: 'NULL', }); @@ -97,6 +98,7 @@ describe('Generate Column Definitions', () => { type: `"${mockSchemaName}"."_person_tags_enum"`, isArray: true, isNullable: true, + isPrimary: false, isUnique: false, default: 'NULL', }); @@ -147,6 +149,7 @@ describe('Generate Column Definitions', () => { name: 'companyId', type: 'uuid', isNullable: true, + isPrimary: false, isUnique: false, default: null, isArray: false, @@ -187,6 +190,7 @@ describe('Generate Column Definitions', () => { // All composite columns must inherit parent nullable constraint columns.forEach((column) => { expect(column.isNullable).toBe(true); + expect(column.isPrimary).toBe(false); expect(column.isUnique).toBe(false); expect(column.default).toBe('NULL'); }); @@ -219,6 +223,7 @@ describe('Generate Column Definitions', () => { name: 'priceAmountMicros', type: 'numeric', isNullable: true, + isPrimary: false, isUnique: false, default: "'100000000'::numeric", }); @@ -227,6 +232,7 @@ describe('Generate Column Definitions', () => { name: 'priceCurrencyCode', type: 'text', isNullable: true, + isPrimary: false, isUnique: false, default: "'USD'::text", }); @@ -253,6 +259,7 @@ describe('Generate Column Definitions', () => { name: 'description', type: 'text', isNullable: true, + isPrimary: false, isUnique: false, default: 'NULL', isArray: false, @@ -279,6 +286,7 @@ describe('Generate Column Definitions', () => { name: 'isActive', type: 'boolean', isNullable: true, + isPrimary: false, isUnique: false, default: "'true'::boolean", isArray: false, @@ -306,6 +314,7 @@ describe('Generate Column Definitions', () => { name: 'content', type: 'text', isNullable: true, + isPrimary: false, isUnique: false, default: 'NULL', isArray: false, @@ -331,6 +340,7 @@ describe('Generate Column Definitions', () => { name: 'uuid', type: 'uuid', isNullable: true, + isPrimary: false, isUnique: false, default: 'NULL', isArray: false, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/generate-column-definitions.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/generate-column-definitions.util.ts index f2a152a8a1..1c94ddf40e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/generate-column-definitions.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/generate-column-definitions.util.ts @@ -79,6 +79,7 @@ export const generateCompositeColumnDefinition = ({ isUnique: parentFieldMetadata.isUnique ?? false, default: serializedDefaultValue, isArray: isArrayFlag, + isPrimary: false, }; return definition; @@ -98,6 +99,7 @@ const generateTsVectorColumnDefinition = ( default: null, asExpression: flatFieldMetadata.settings?.asExpression ?? undefined, generatedType: flatFieldMetadata.settings?.generatedType ?? undefined, + isPrimary: false, }; }; @@ -122,6 +124,7 @@ const generateRelationColumnDefinition = ( isArray: false, isUnique: false, default: null, + isPrimary: false, }; }; @@ -158,6 +161,7 @@ const generateColumnDefinition = ({ flatFieldMetadata.type === FieldMetadataType.MULTI_SELECT, isUnique: flatFieldMetadata.isUnique ?? false, default: serializedDefaultValue, + isPrimary: flatFieldMetadata.name === 'id', }; }; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/__snapshots__/create-one-unique-field-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/__snapshots__/create-one-unique-field-metadata.integration-spec.ts.snap new file mode 100644 index 0000000000..041ac9c9eb --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/__snapshots__/create-one-unique-field-metadata.integration-spec.ts.snap @@ -0,0 +1,111 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`failing createOne unique field metadata should fail for composite type without unique subfields (FULL_NAME) 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_FIELD_INVALID_TYPE_FOR_UNIQUE", + "message": "Unique index cannot be created for field uniqueFullName of type FULL_NAME", + "userFriendlyMessage": "FULL_NAME fields cannot be unique.", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_UNIQUE_98d410e804d758f9bcceb154dde", + }, + "status": "fail", + "type": "create_index", + }, + ], + "objectMetadata": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "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, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating fields", + "name": "GraphQLError", +} +`; + +exports[`failing createOne unique field metadata should fail when field has custom default value 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_FIELD_INVALID_DEFAULT_VALUE", + "message": "Unique index cannot be created for field uniqueFieldWithDefault of type TEXT", + "userFriendlyMessage": "TEXT fields cannot have a default value.", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_UNIQUE_a1b02809c523cdc853d35fbc413", + }, + "status": "fail", + "type": "create_index", + }, + ], + "objectMetadata": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "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, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating fields", + "name": "GraphQLError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/create-one-unique-field-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/create-one-unique-field-metadata.integration-spec.ts index 5ed578dc9b..04cb2827e2 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/create-one-unique-field-metadata.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/create-one-unique-field-metadata.integration-spec.ts @@ -2,33 +2,91 @@ import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-m import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; import { FieldMetadataType } from 'twenty-shared/types'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; -describe('create one unique field metadata', () => { +type SuccessfulTestCases = EachTestingContext< + (args: { + createdObjectMetadataId: string; + }) => Omit +>[]; + +type FailingTestCases = EachTestingContext< + (args: { + createdObjectMetadataId: string; + }) => Omit +>[]; + +const successfulTestCases: SuccessfulTestCases = [ + { + title: 'should create one unique field metadata with TEXT type', + context: ({ createdObjectMetadataId }) => ({ + name: 'uniqueTestField', + label: 'Unique Test Field', + type: FieldMetadataType.TEXT, + objectMetadataId: createdObjectMetadataId, + isUnique: true, + }), + }, + { + title: + 'should create unique field metadata for composite type with unique subfields (EMAILS)', + context: ({ createdObjectMetadataId }) => ({ + name: 'uniqueEmails', + label: 'Unique Emails', + type: FieldMetadataType.EMAILS, + objectMetadataId: createdObjectMetadataId, + isUnique: true, + }), + }, +]; + +const failingTestCases: FailingTestCases = [ + { + title: 'should fail when field has custom default value', + context: ({ createdObjectMetadataId }) => ({ + name: 'uniqueFieldWithDefault', + label: 'Unique Field With Default', + type: FieldMetadataType.TEXT, + objectMetadataId: createdObjectMetadataId, + isUnique: true, + defaultValue: "'?'", + }), + }, + { + title: + 'should fail for composite type without unique subfields (FULL_NAME)', + context: ({ createdObjectMetadataId }) => ({ + name: 'uniqueFullName', + label: 'Unique Full Name', + type: FieldMetadataType.FULL_NAME, + objectMetadataId: createdObjectMetadataId, + isUnique: true, + }), + }, +]; + +describe('successful createOne unique field metadata', () => { let createdObjectMetadataId = ''; beforeAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: false, - }); - }); - - beforeEach(async () => { const { data: { createOneObject: { id: objectMetadataId }, }, } = await createOneObjectMetadata({ + expectToFail: false, input: { - nameSingular: 'testObject', - namePlural: 'testObjects', - labelSingular: 'Test Object', - labelPlural: 'Test Objects', + nameSingular: 'testObjectForUniqueField', + namePlural: 'testObjectsForUniqueField', + labelSingular: 'Test Object For Unique Field', + labelPlural: 'Test Objects For Unique Field', icon: 'IconTest', }, }); @@ -36,7 +94,7 @@ describe('create one unique field metadata', () => { createdObjectMetadataId = objectMetadataId; }); - afterEach(async () => { + afterAll(async () => { await updateOneObjectMetadata({ expectToFail: false, input: { @@ -47,124 +105,97 @@ describe('create one unique field metadata', () => { }, }); await deleteOneObjectMetadata({ + expectToFail: false, input: { idToDelete: createdObjectMetadataId }, }); }); - afterAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: true, - }); - }); + it.each(eachTestingContextFilter(successfulTestCases))( + '$title', + async ({ context }) => { + const contextPayload = context({ + createdObjectMetadataId, + }); - it('should create one unique field metadata', async () => { - const createFieldInput = { - name: 'uniqueTestField', - label: 'Unique Test Field', - type: FieldMetadataType.TEXT, - objectMetadataId: createdObjectMetadataId, - isUnique: true, - }; + const { data, errors } = await createOneFieldMetadata({ + input: contextPayload, + expectToFail: false, + gqlFields: ` + id + name + label + type + isUnique + `, + }); - const { data, errors } = await createOneFieldMetadata({ - input: createFieldInput, - gqlFields: ` - id - name - label - type - isUnique - `, - }); - - expect(errors).toBeUndefined(); - expect(data).not.toBeNull(); - expect(data.createOneField).toBeDefined(); - expect(data.createOneField.name).toBe('uniqueTestField'); - expect(data.createOneField.isUnique).toBe(true); - }); - - it('should not create unique field metadata if it has custom default value', async () => { - const createFieldInput = { - name: 'uniqueFieldWithDefault', - label: 'Unique Field With Default', - type: FieldMetadataType.TEXT, - objectMetadataId: createdObjectMetadataId, - isUnique: true, - defaultValue: "'?'", - }; - - const { data, errors } = await createOneFieldMetadata({ - input: createFieldInput, - gqlFields: ` - id - name - label - type - isUnique - `, - expectToFail: true, - }); - - expect(data).toBeNull(); - expect(errors).toBeDefined(); - expect(errors[0].message).toBe('Unique field cannot have a default value'); - }); - - it('should not create unique field metadata for composite type without unique subfields', async () => { - const createFieldInput = { - name: 'uniqueFullName', - label: 'Unique Full Name', - type: FieldMetadataType.FULL_NAME, - objectMetadataId: createdObjectMetadataId, - isUnique: true, - }; - - const { data, errors } = await createOneFieldMetadata({ - input: createFieldInput, - gqlFields: ` - id - name - label - type - isUnique - `, - expectToFail: true, - }); - - expect(data).toBeNull(); - expect(errors).toBeDefined(); - expect(errors[0].message).toContain( - 'Unique index cannot be created for field uniqueFullName of type FULL_NAME', - ); - }); - - it('should create unique field metadata for composite type with unique subfields', async () => { - const createFieldInput = { - name: 'uniqueEmails', - label: 'Unique Emails', - type: FieldMetadataType.EMAILS, - objectMetadataId: createdObjectMetadataId, - isUnique: true, - }; - - const { data, errors } = await createOneFieldMetadata({ - input: createFieldInput, - gqlFields: ` - id - name - label - type - isUnique - `, - }); - - expect(errors).toBeUndefined(); - expect(data).not.toBeNull(); - expect(data.createOneField).toBeDefined(); - expect(data.createOneField.name).toBe('uniqueEmails'); - expect(data.createOneField.isUnique).toBe(true); - }); + expect(errors).toBeUndefined(); + expect(data).not.toBeNull(); + expect(data.createOneField).toBeDefined(); + expect(data.createOneField.name).toBe(contextPayload.name); + expect(data.createOneField.isUnique).toBe(true); + }, + ); +}); + +describe('failing createOne unique field metadata', () => { + let createdObjectMetadataId = ''; + + beforeAll(async () => { + const { + data: { + createOneObject: { id: objectMetadataId }, + }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: { + nameSingular: 'testObjectForFailingUniqueField', + namePlural: 'testObjectsForFailingUniqueField', + labelSingular: 'Test Object For Failing Unique Field', + labelPlural: 'Test Objects For Failing Unique Field', + icon: 'IconTest', + }, + }); + + createdObjectMetadataId = objectMetadataId; + }); + + afterAll(async () => { + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: createdObjectMetadataId, + updatePayload: { + isActive: false, + }, + }, + }); + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: createdObjectMetadataId }, + }); + }); + + it.each(eachTestingContextFilter(failingTestCases))( + '$title', + async ({ context }) => { + const contextPayload = context({ + createdObjectMetadataId, + }); + + const { errors } = await createOneFieldMetadata({ + input: contextPayload, + expectToFail: true, + }); + + expect(errors).toBeDefined(); + expect(errors.length).toBe(1); + const [firstError] = errors; + + expect(firstError).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny(firstError), + ); + expect(firstError.extensions.code).not.toBe('INTERNAL_SERVER_ERROR'); + }, + ); });