diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts index fc3fb8f3fc..33128db329 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts @@ -4,6 +4,7 @@ import { msg } from '@lingui/core/macro'; import { isDefined } from 'class-validator'; import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type'; import { type FlatFieldMetadataTypeValidator } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type'; import { FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; import { validateEnumSelectFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util'; @@ -14,6 +15,11 @@ const DEFAULT_NO_VALIDATION = async (): Promise< FlatFieldMetadataValidationError[] > => []; +export type GenericValidateFlatFieldMetadataTypeSpecificitiesArgs = + FlatEntityValidationArgs<'fieldMetadata'> & { + updates?: FlatEntityPropertiesUpdates<'fieldMetadata'>; + }; + @Injectable() export class FlatFieldMetadataTypeValidatorService { constructor() {} @@ -51,7 +57,7 @@ export class FlatFieldMetadataTypeValidatorService { }; public async validateFlatFieldMetadataTypeSpecificities( - args: FlatEntityValidationArgs<'fieldMetadata'>, + args: GenericValidateFlatFieldMetadataTypeSpecificitiesArgs, ): Promise { const { flatEntityToValidate } = args; const fieldType = flatEntityToValidate.type; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type.ts index 8cbb6d9ad4..a13712e808 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type.ts @@ -1,11 +1,14 @@ import { type FieldMetadataType } from 'twenty-shared/types'; +import { type GenericValidateFlatFieldMetadataTypeSpecificitiesArgs } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service'; 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 { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type'; export type FlatFieldMetadataTypeValidationArgs = - Omit, 'flatEntityToValidate'> & { + Omit< + GenericValidateFlatFieldMetadataTypeSpecificitiesArgs, + 'flatEntityToValidate' + > & { flatEntityToValidate: FlatFieldMetadata; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-object-field-names-and-join-column-names.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-object-field-names-and-join-column-names.util.ts new file mode 100644 index 0000000000..2ea3a31553 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-object-field-names-and-join-column-names.util.ts @@ -0,0 +1,59 @@ +import { RelationType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/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'; +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'; + +type ObjectFieldNamesAndJoinColumnNames = { + fieldNames: string[]; + joinColumnNames: string[]; +}; +export const getObjectFieldNamesAndJoinColumnNames = ({ + flatFieldMetadataMaps, + flatObjectMetadata, +}: { + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; +}): { + objectFieldNamesAndJoinColumnNames: ObjectFieldNamesAndJoinColumnNames; +} => { + const objectFlatFieldMetadatas = + findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityIds: flatObjectMetadata.fieldMetadataIds, + }); + const initialAccumulator: ObjectFieldNamesAndJoinColumnNames = { + joinColumnNames: [], + fieldNames: [], + }; + + const objectFieldNamesAndJoinColumnNames = objectFlatFieldMetadatas.reduce( + (acc, flatFieldMetadata) => { + if ( + isMorphOrRelationFlatFieldMetadata(flatFieldMetadata) && + flatFieldMetadata.settings.relationType === RelationType.MANY_TO_ONE && + isDefined(flatFieldMetadata.settings.joinColumnName) + ) { + return { + ...acc, + fieldNames: [...acc.fieldNames, flatFieldMetadata.name], + joinColumnNames: [ + ...acc.joinColumnNames, + flatFieldMetadata.settings.joinColumnName, + ], + }; + } + + return { + ...acc, + fieldNames: [...acc.fieldNames, flatFieldMetadata.name], + }; + }, + initialAccumulator, + ); + + return { objectFieldNamesAndJoinColumnNames }; +}; 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 499bc5ce19..af9b4bc430 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 @@ -1,20 +1,15 @@ import { msg } from '@lingui/core/macro'; -import { - FieldMetadataType, - compositeTypeDefinitions, -} from 'twenty-shared/types'; +import { compositeTypeDefinitions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; 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 { 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 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 { 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 { getObjectFieldNamesAndJoinColumnNames } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-object-field-names-and-join-column-names.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; const getReservedCompositeFieldNames = ( @@ -41,15 +36,13 @@ const getReservedCompositeFieldNames = ( }; export const validateFlatFieldMetadataNameAvailability = ({ - flatFieldMetadata, + name, flatFieldMetadataMaps, - remainingFlatEntityMapsToValidate, flatObjectMetadata, }: { - flatFieldMetadata: FlatFieldMetadata; + name: string; flatObjectMetadata: FlatObjectMetadata; flatFieldMetadataMaps: FlatEntityMaps; - remainingFlatEntityMapsToValidate?: FlatEntityMaps; }): FlatFieldMetadataValidationError[] => { const errors: FlatFieldMetadataValidationError[] = []; const objectFlatFieldMetadatas = @@ -60,63 +53,37 @@ export const validateFlatFieldMetadataNameAvailability = ({ const reservedCompositeFieldsNames = getReservedCompositeFieldNames( objectFlatFieldMetadatas, ); - const flatFieldMetadataName = flatFieldMetadata.name; - if ( - !isFlatFieldMetadataOfType( - // CHALLENGE Could remove the is morph relation assertion - flatFieldMetadata, - FieldMetadataType.MORPH_RELATION, - ) && - objectFlatFieldMetadatas.some((existingFlatFieldMetadata) => { - const firstDegreeCollision = - existingFlatFieldMetadata.name === flatFieldMetadataName; - - if (firstDegreeCollision) { - return true; - } - - if (!isMorphOrRelationFlatFieldMetadata(existingFlatFieldMetadata)) { - return false; - } - - const targetFlatFieldMetadata = - flatFieldMetadata.id === - existingFlatFieldMetadata.relationTargetFieldMetadataId - ? flatFieldMetadata - : (remainingFlatEntityMapsToValidate?.byId[ - existingFlatFieldMetadata.relationTargetFieldMetadataId - ] ?? - findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityId: - existingFlatFieldMetadata.relationTargetFieldMetadataId, - flatEntityMaps: flatFieldMetadataMaps, - })); - - if (!isMorphOrRelationFlatFieldMetadata(targetFlatFieldMetadata)) { - return false; - } - - return ( - targetFlatFieldMetadata.settings.joinColumnName === - flatFieldMetadataName - ); - }) - ) { + if (reservedCompositeFieldsNames.includes(name)) { errors.push({ - code: FieldMetadataExceptionCode.NOT_AVAILABLE, - value: flatFieldMetadataName, - message: `Name "${flatFieldMetadataName}" is not available as it is already used by another field`, - userFriendlyMessage: msg`Name "${flatFieldMetadataName}" is not available as it is already used by another field`, + code: FieldMetadataExceptionCode.RESERVED_KEYWORD, + message: `Name "${name}" is reserved composite field name`, + value: name, + userFriendlyMessage: msg`Name "${name}" is not available`, }); } - if (reservedCompositeFieldsNames.includes(flatFieldMetadataName)) { + const { objectFieldNamesAndJoinColumnNames } = + getObjectFieldNamesAndJoinColumnNames({ + flatFieldMetadataMaps, + flatObjectMetadata, + }); + + if (objectFieldNamesAndJoinColumnNames.fieldNames.includes(name)) { errors.push({ - code: FieldMetadataExceptionCode.RESERVED_KEYWORD, - message: `Name "${flatFieldMetadataName}" is reserved composite field name`, - value: flatFieldMetadataName, - userFriendlyMessage: msg`Name "${flatFieldMetadataName}" is not available`, + code: FieldMetadataExceptionCode.NOT_AVAILABLE, + value: name, + message: `Name "${name}" is not available as it is already used by another field`, + userFriendlyMessage: msg`Name "${name}" is not available as it is already used by another field`, + }); + } + + if (objectFieldNamesAndJoinColumnNames.joinColumnNames.includes(name)) { + errors.push({ + code: FieldMetadataExceptionCode.NOT_AVAILABLE, + value: name, + message: `Name "${name}" is not available as it is already used by another join column name`, + userFriendlyMessage: msg`Name "${name}" is not available as it is already used by join column name`, }); } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-or-relation-flat-field-join-column-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-or-relation-flat-field-join-column-name.util.ts new file mode 100644 index 0000000000..8eb68de161 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-or-relation-flat-field-join-column-name.util.ts @@ -0,0 +1,79 @@ +import { msg } from '@lingui/core/macro'; +import { RelationType } from 'twenty-shared/types'; +import { assertUnreachable, isDefined } from 'twenty-shared/utils'; + +import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type'; +import { type MetadataFlatEntityAndRelatedFlatEntityMapsForValidation } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-and-related-flat-entity-maps-for-validation.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.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 { validateFlatFieldMetadataNameAvailability } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util'; + +export const validateMorphOrRelationFlatFieldJoinColumName = ({ + flatFieldMetadata, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatFieldMetadataMaps, + flatObjectMetadataMaps, + }, +}: { + flatFieldMetadata: FlatFieldMetadata; + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: MetadataFlatEntityAndRelatedFlatEntityMapsForValidation<'fieldMetadata'>; +}): FlatFieldMetadataValidationError[] => { + const errors: FlatFieldMetadataValidationError[] = []; + + switch (flatFieldMetadata.settings.relationType) { + case RelationType.MANY_TO_ONE: { + if (!isDefined(flatFieldMetadata.settings.joinColumnName)) { + errors.push({ + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: + 'Many to one field metadata should carry the join column name in its settings', + userFriendlyMessage: msg`A many to one relation field should always declare a join column`, + }); + + return errors; + } + + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: flatFieldMetadata.objectMetadataId, + flatEntityMaps: flatObjectMetadataMaps, + }); + + if (!isDefined(flatObjectMetadata)) { + errors.push({ + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: 'Could not find relation field parent flat object', + userFriendlyMessage: msg`Could not find relation field parent flat object`, + }); + + return errors; + } + + errors.push( + ...validateFlatFieldMetadataNameAvailability({ + name: flatFieldMetadata.settings.joinColumnName, + flatFieldMetadataMaps, + flatObjectMetadata, + }), + ); + break; + } + case RelationType.ONE_TO_MANY: { + if (isDefined(flatFieldMetadata.settings.joinColumnName)) { + errors.push({ + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: + 'One to many field metadata should not carry the join column name in its settings', + userFriendlyMessage: msg`A one to many relation field should never declare a join column`, + }); + } + break; + } + default: { + assertUnreachable(flatFieldMetadata.settings.relationType); + } + } + + return errors; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-or-relation-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-or-relation-flat-field-metadata.util.ts index 589f196aef..40eadffe2b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-or-relation-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-or-relation-flat-field-metadata.util.ts @@ -3,8 +3,12 @@ import { isDefined, isValidUuid } from 'twenty-shared/utils'; import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { type FlatFieldMetadataTypeValidationArgs } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type'; import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { validateMorphOrRelationFlatFieldJoinColumName } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-or-relation-flat-field-join-column-name.util'; +import { findFlatEntityPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/utils/find-flat-entity-property-update.util'; export const validateMorphOrRelationFlatFieldMetadata = async ({ flatEntityToValidate: flatFieldMetadataToValidate, @@ -12,6 +16,7 @@ export const validateMorphOrRelationFlatFieldMetadata = async ({ flatFieldMetadataMaps, flatObjectMetadataMaps, }, + updates, remainingFlatEntityMapsToValidate, }: FlatFieldMetadataTypeValidationArgs): Promise< FlatFieldMetadataValidationError[] @@ -39,25 +44,29 @@ export const validateMorphOrRelationFlatFieldMetadata = async ({ const errors: FlatFieldMetadataValidationError[] = []; - const targetRelationFlatObjectMetadata = - flatObjectMetadataMaps?.byId[relationTargetObjectMetadataId]; + const targetFlatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: relationTargetObjectMetadataId, + flatEntityMaps: flatObjectMetadataMaps, + }); - if (!isDefined(targetRelationFlatObjectMetadata)) { + const targetFlatFieldMetadata = + remainingFlatEntityMapsToValidate?.byId[relationTargetFieldMetadataId] ?? + findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: relationTargetFieldMetadataId, + flatEntityMaps: flatFieldMetadataMaps, + }); + + if (!isDefined(targetFlatObjectMetadata)) { errors.push({ code: FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, message: 'Relation target object metadata not found', userFriendlyMessage: msg`Object targeted by the relation not found`, }); + + return errors; } - const targetRelationFlatFieldMetadata = - remainingFlatEntityMapsToValidate?.byId[relationTargetFieldMetadataId] ?? - flatFieldMetadataMaps.byId[relationTargetFieldMetadataId]; - - if ( - isDefined(targetRelationFlatObjectMetadata) && - !isDefined(targetRelationFlatFieldMetadata) - ) { + if (!isDefined(targetFlatFieldMetadata)) { errors.push({ code: FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, message: isDefined(remainingFlatEntityMapsToValidate) @@ -65,7 +74,33 @@ export const validateMorphOrRelationFlatFieldMetadata = async ({ : 'Relation field target metadata not found', userFriendlyMessage: msg`Relation field target metadata not found`, }); + } else if (!isMorphOrRelationFlatFieldMetadata(targetFlatFieldMetadata)) { + errors.push({ + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: + 'A relation field can only target a MORPH_RELATION or another RELATION field', + userFriendlyMessage: msg`Invalid relation field target`, + }); } + if ( + !isDefined(updates) || + isDefined( + findFlatEntityPropertyUpdate({ + flatEntityUpdates: updates, + property: 'settings', + }), + ) + ) + errors.push( + ...validateMorphOrRelationFlatFieldJoinColumName({ + flatFieldMetadata: flatFieldMetadataToValidate, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatFieldMetadataMaps, + flatObjectMetadataMaps, + }, + }), + ); + return errors; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts index 1fd3f673ba..ccdc632c50 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts @@ -131,7 +131,7 @@ export class FlatFieldMetadataValidatorService { validationResult.errors.push( ...validateFlatFieldMetadataName(flatFieldMetadataToValidate.name), ...validateFlatFieldMetadataNameAvailability({ - flatFieldMetadata: flatFieldMetadataToValidate, + name: flatFieldMetadataToValidate.name, flatFieldMetadataMaps: optimisticFlatFieldMetadataMaps, flatObjectMetadata, }), @@ -152,6 +152,7 @@ export class FlatFieldMetadataValidatorService { const fieldMetadataTypeValidationErrors = await this.flatFieldMetadataTypeValidatorService.validateFlatFieldMetadataTypeSpecificities( { + updates, optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { flatFieldMetadataMaps: optimisticFlatFieldMetadataMaps, flatObjectMetadataMaps, @@ -313,9 +314,8 @@ export class FlatFieldMetadataValidatorService { validationResult.errors.push( ...validateFlatFieldMetadataNameAvailability({ - flatFieldMetadata: flatFieldMetadataToValidate, + name: flatFieldMetadataToValidate.name, flatFieldMetadataMaps: optimisticFlatFieldMetadataMaps, - remainingFlatEntityMapsToValidate, flatObjectMetadata: parentFlatObjectMetadata, }), ); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts.snap index ba30b23624..5a289f2ff8 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts.snap @@ -117,8 +117,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 Morh re "errors": [ { "code": "NOT_AVAILABLE", - "message": "Name "fieldNamePersonForMorphRelationSecondId" is not available as it is already used by another field", - "userFriendlyMessage": "Name "fieldNamePersonForMorphRelationSecondId" is not available as it is already used by another field", + "message": "Name "fieldNamePersonForMorphRelationSecondId" is not available as it is already used by another join column name", + "userFriendlyMessage": "Name "fieldNamePersonForMorphRelationSecondId" is not available as it is already used by join column name", "value": "fieldNamePersonForMorphRelationSecondId", }, ], diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts index 6a8062253a..5d6058db0b 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts @@ -487,7 +487,7 @@ describe('failing createOne FieldMetadataService morph relation fields v2', () = input: { label: 'colliding field label', name: `fieldNamePersonForMorphRelationSecondId`, - objectMetadataId: createdObjectMetadataPersonId, + objectMetadataId: createdObjectMetadataCompanyId, type: FieldMetadataType.TEXT, }, expectToFail: true, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap index 6adc7e4fb6..f710b54d43 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap @@ -1455,3 +1455,181 @@ exports[`Field metadata relation creation should fail should fail when creating "name": "GraphQLError", } `; + +exports[`Field metadata relation creation should fail should fail when creating a relation field with name that conflicts with existing field name 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [ + { + "errors": [ + { + "code": "NOT_AVAILABLE", + "message": "Name "fieldName" is not available as it is already used by another field", + "userFriendlyMessage": "Name "fieldName" is not available as it is already used by another field", + "value": "fieldName", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "fieldName", + "objectMetadataId": Any, + }, + "status": "fail", + "type": "create_field", + }, + { + "errors": [ + { + "code": "FIELD_METADATA_NOT_FOUND", + "message": "Relation field target metadata not found in both existing and about to be created field metadatas", + "userFriendlyMessage": "Relation field target metadata not found", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "relatedObject", + "objectMetadataId": Any, + }, + "status": "fail", + "type": "create_field", + }, + ], + "index": [ + { + "errors": [ + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_7ee6c7ff1e4648b0665f60cd5e4", + }, + "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[`Field metadata relation creation should fail should fail when creating a relation field with name that conflicts with existing join column name 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [ + { + "errors": [ + { + "code": "NOT_AVAILABLE", + "message": "Name "companyId" is not available as it is already used by another join column name", + "userFriendlyMessage": "Name "companyId" is not available as it is already used by join column name", + "value": "companyId", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "companyId", + "objectMetadataId": Any, + }, + "status": "fail", + "type": "create_field", + }, + { + "errors": [ + { + "code": "FIELD_METADATA_NOT_FOUND", + "message": "Relation field target metadata not found in both existing and about to be created field metadatas", + "userFriendlyMessage": "Relation field target metadata not found", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "related", + "objectMetadataId": Any, + }, + "status": "fail", + "type": "create_field", + }, + ], + "index": [ + { + "errors": [ + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_ed8240d8bc1e69a39dab1c17fe2", + }, + "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/relation/failing-field-metadata-relation-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/failing-field-metadata-relation-creation.integration-spec.ts index f51215266c..64ac3c781e 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/failing-field-metadata-relation-creation.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/failing-field-metadata-relation-creation.integration-spec.ts @@ -1,13 +1,16 @@ import { faker } from '@faker-js/faker'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; 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 { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; -import { type EachTestingContext } from 'twenty-shared/testing'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; import { FieldMetadataType } from 'twenty-shared/types'; -import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; @@ -252,7 +255,7 @@ describe('Field metadata relation creation should fail', () => { } }); - it.each(failingLabelsCreationTestsUseCase)( + it.each(eachTestingContextFilter(failingLabelsCreationTestsUseCase))( 'relation ONE_TO_MANY $title', async ({ context }) => { const computedRelationCreationPayload = @@ -291,7 +294,7 @@ describe('Field metadata relation creation should fail', () => { }, ); - it.each(failingLabelsCreationTestsUseCase)( + it.each(eachTestingContextFilter(failingLabelsCreationTestsUseCase))( 'relation MANY_TO_ONE $title', async ({ context }) => { const computedRelationCreationPayload = @@ -376,4 +379,72 @@ describe('Field metadata relation creation should fail', () => { errors, }); }); + + it('should fail when creating a relation field with name that conflicts with existing field name', async () => { + const { errors } = await createOneFieldMetadata({ + expectToFail: true, + input: { + objectMetadataId: globalTestContext.objectMetadataIds.sourceObjectId, + name: globalTestContext.collisionFieldName, + label: 'Duplicate Field', + isLabelSyncedWithName: false, + type: FieldMetadataType.RELATION, + relationCreationPayload: { + targetFieldLabel: 'Related Object', + type: RelationType.MANY_TO_ONE, + targetObjectMetadataId: + globalTestContext.objectMetadataIds.targetObjectId, + targetFieldIcon: 'IconLink', + }, + }, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }); + + it('should fail when creating a relation field with name that conflicts with existing join column name', async () => { + const relationFieldName = 'company'; + + await createOneFieldMetadata({ + expectToFail: false, + input: { + objectMetadataId: globalTestContext.objectMetadataIds.sourceObjectId, + name: relationFieldName, + label: 'Company', + isLabelSyncedWithName: false, + type: FieldMetadataType.RELATION, + relationCreationPayload: { + targetFieldLabel: 'company', + type: RelationType.MANY_TO_ONE, + targetObjectMetadataId: + globalTestContext.objectMetadataIds.targetObjectId, + targetFieldIcon: 'IconBuilding', + }, + }, + }); + + const { errors } = await createOneFieldMetadata({ + expectToFail: true, + input: { + objectMetadataId: globalTestContext.objectMetadataIds.sourceObjectId, + name: `${relationFieldName}Id`, + label: 'Company Id Field', + isLabelSyncedWithName: false, + type: FieldMetadataType.RELATION, + relationCreationPayload: { + targetFieldLabel: 'Related', + type: RelationType.MANY_TO_ONE, + targetObjectMetadataId: + globalTestContext.objectMetadataIds.targetObjectId, + targetFieldIcon: 'IconLink', + }, + }, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }); });