From 0d5fedfb0a5cf943a83156c24501f267f13cff01 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Mon, 1 Sep 2025 18:35:40 +0200 Subject: [PATCH] Fix user friendly message for non available object name (#14210) Fixes https://github.com/twentyhq/twenty/issues/13577. When naming or renaming an object, we are already checking that the name was available compared to other existing objects. But we also need to check that the relation fields that will be created or updated are available as well: we create relation fields on Attachment, Note, Favorite, Task and TimelineActivites, that bear the name of the object. Thus, it is not possible to create an object that has the same name as one of the fields on Attachment, Note etc: name, createdAt, ... are not valid names. --- .../field-metadata-relation.service.ts | 6 +- .../field-metadata-validation.service.ts | 2 +- .../object-metadata.exception.ts | 1 + .../object-metadata.service.ts | 47 ++++-- .../object-metadata-field-relation.service.ts | 154 ++++++++++++++---- .../object-metadata-migration.service.ts | 2 +- ...d-default-fields-for-custom-object.util.ts | 2 +- ...data-graphql-api-exception-handler.util.ts | 1 + .../validate-field-name-availability.spec.ts | 4 +- .../validate-field-name-availability.utils.ts | 16 +- ...relation-creation.integration-spec.ts.snap | 4 +- ...relation-creation.integration-spec.ts.snap | 8 +- .../rename-custom-object.integration-spec.ts | 28 +++- 13 files changed, 208 insertions(+), 67 deletions(-) diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts index 1dedcc3a2c5..94761c77b65 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts @@ -160,7 +160,7 @@ export class FieldMetadataRelationService { ) { validateFieldNameAvailabilityOrThrow({ name: `${fieldMetadataInput.name}Id`, - objectMetadata, + fieldMetadataMapById: objectMetadata.fieldsById, }); const relationCreationPayload = ( @@ -189,12 +189,12 @@ export class FieldMetadataRelationService { validateFieldNameAvailabilityOrThrow({ name: computedMetadataNameFromLabel, - objectMetadata: objectMetadataTarget, + fieldMetadataMapById: objectMetadataTarget.fieldsById, }); validateFieldNameAvailabilityOrThrow({ name: `${computedMetadataNameFromLabel}Id`, - objectMetadata: objectMetadataTarget, + fieldMetadataMapById: objectMetadataTarget.fieldsById, }); if ( diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service.ts index 52b732dbd83..9635b6281f6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service.ts @@ -170,7 +170,7 @@ export class FieldMetadataValidationService { try { validateFieldNameAvailabilityOrThrow({ name: fieldMetadataInput.name, - objectMetadata, + fieldMetadataMapById: objectMetadata.fieldsById, }); } catch (error) { if (error instanceof InvalidMetadataException) { diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts index 61aba60c68f..e24e6b3af0a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts @@ -10,4 +10,5 @@ export enum ObjectMetadataExceptionCode { MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD = 'MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD', INVALID_ORM_OUTPUT = 'INVALID_ORM_OUTPUT', INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', + NAME_CONFLICT = 'NAME_CONFLICT', } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 5fc6a6dbbcd..66c334a447e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -40,6 +40,7 @@ import { } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util'; import { SearchVectorService } from 'src/engine/metadata-modules/search-vector/search-vector.service'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { validateMetadataIdentifierFieldMetadataIds } from 'src/engine/metadata-modules/utils/validate-metadata-identifier-field-metadata-id.utils'; import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; import { validatesNoOtherObjectWithSameNameExistsOrThrows } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util'; @@ -221,10 +222,18 @@ export class ObjectMetadataService extends TypeOrmQueryService ({ + ...acc, + [field.id]: field, + }), + {}, + ); + const createdRelatedObjectMetadataCollection = await this.objectMetadataFieldRelationService.createRelationsAndForeignKeysMetadata( createObjectInput.workspaceId, - createdObjectMetadata, + { ...createdObjectMetadata, fieldsById }, objectMetadataMaps, queryRunner, ); @@ -374,12 +383,14 @@ export class ObjectMetadataService extends TypeOrmQueryService, + >; objectMetadataForUpdate: Pick< ObjectMetadataItemWithFieldMaps, | 'nameSingular' @@ -617,10 +634,11 @@ export class ObjectMetadataService extends TypeOrmQueryService, - inputPayload: UpdateObjectPayload, - queryRunner: QueryRunner, - ): Promise<{ didUpdateLabelOrIcon: boolean }> { + >; + inputPayload: UpdateObjectPayload; + queryRunner: QueryRunner; + objectMetadataMaps: ObjectMetadataMaps; + }): Promise<{ didUpdateLabelOrIcon: boolean }> { const newTargetTableName = computeObjectTargetTable( objectMetadataForUpdate, ); @@ -638,9 +656,12 @@ export class ObjectMetadataService extends TypeOrmQueryService +> & + Required, 'name'>>; + @Injectable() export class ObjectMetadataFieldRelationService { constructor( @@ -52,7 +60,7 @@ export class ObjectMetadataFieldRelationService { workspaceId: string, sourceObjectMetadata: Pick< ObjectMetadataItemWithFieldMaps, - 'id' | 'nameSingular' | 'labelSingular' + 'id' | 'nameSingular' | 'labelSingular' | 'fieldsById' >, objectMetadataMaps: ObjectMetadataMaps, queryRunner?: QueryRunner, @@ -83,8 +91,9 @@ export class ObjectMetadataFieldRelationService { workspaceId: string; sourceObjectMetadata: Pick< ObjectMetadataItemWithFieldMaps, - 'id' | 'nameSingular' | 'labelSingular' + 'id' | 'nameSingular' | 'labelSingular' | 'fieldsById' >; + objectMetadataMaps: ObjectMetadataMaps; relationObjectMetadataStandardId: string; queryRunner?: QueryRunner; @@ -116,7 +125,7 @@ export class ObjectMetadataFieldRelationService { workspaceId: string, sourceObjectMetadata: Pick< ObjectMetadataItemWithFieldMaps, - 'id' | 'nameSingular' | 'labelSingular' + 'id' | 'nameSingular' | 'labelSingular' | 'fieldsById' >, targetObjectMetadata: ObjectMetadataItemWithFieldMaps, queryRunner?: QueryRunner, @@ -127,12 +136,22 @@ export class ObjectMetadataFieldRelationService { targetObjectMetadata, ); + this.validateFieldNameAvailabilityOrThrow({ + name: sourceFieldMetadata.name, + fieldMetadataMapById: sourceObjectMetadata.fieldsById, + }); + const targetFieldMetadata = this.createTargetFieldMetadata( workspaceId, sourceObjectMetadata, targetObjectMetadata, ); + this.validateFieldNameAvailabilityOrThrow({ + name: targetFieldMetadata.name, + fieldMetadataMapById: sourceObjectMetadata.fieldsById, + }); + const fieldMetadataRepository = queryRunner ? queryRunner.manager.getRepository(FieldMetadataEntity) : this.fieldMetadataRepository; @@ -159,16 +178,22 @@ export class ObjectMetadataFieldRelationService { ]); } - public async updateRelationsAndForeignKeysMetadata( - workspaceId: string, + public async updateRelationsAndForeignKeysMetadata({ + workspaceId, + updatedObjectMetadata, + objectMetadataMaps, + queryRunner, + }: { + workspaceId: string; updatedObjectMetadata: Pick< ObjectMetadataEntity, 'nameSingular' | 'isCustom' | 'id' | 'labelSingular' - >, - queryRunner?: QueryRunner, - ): Promise< + >; + objectMetadataMaps: ObjectMetadataMaps; + queryRunner?: QueryRunner; + }): Promise< { - targetObjectMetadata: ObjectMetadataEntity; + targetObjectMetadata: ObjectMetadataItemWithFieldMaps; targetFieldMetadata: FieldMetadataEntity; sourceFieldMetadata: FieldMetadataEntity; }[] @@ -176,39 +201,61 @@ export class ObjectMetadataFieldRelationService { return await Promise.all( DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map( async (relationObjectMetadataStandardId) => - this.updateRelationAndForeignKeyMetadata( + this.updateRelationAndForeignKeyMetadata({ workspaceId, - updatedObjectMetadata, - relationObjectMetadataStandardId, + sourceObjectMetadata: updatedObjectMetadata, + targetObjectMetadataStandardId: relationObjectMetadataStandardId, + objectMetadataMaps, queryRunner, - ), + }), ), ); } - private async updateRelationAndForeignKeyMetadata( - workspaceId: string, + private async updateRelationAndForeignKeyMetadata({ + workspaceId, + sourceObjectMetadata, + targetObjectMetadataStandardId, + objectMetadataMaps, + queryRunner, + }: { + workspaceId: string; sourceObjectMetadata: Pick< ObjectMetadataEntity, 'nameSingular' | 'id' | 'isCustom' | 'labelSingular' - >, - targetObjectMetadataStandardId: string, - queryRunner?: QueryRunner, - ) { - const objectMetadataRepository = queryRunner - ? queryRunner.manager.getRepository(ObjectMetadataEntity) - : this.objectMetadataRepository; + >; + targetObjectMetadataStandardId: string; + objectMetadataMaps: ObjectMetadataMaps; + queryRunner?: QueryRunner; + }): Promise<{ + targetObjectMetadata: ObjectMetadataItemWithFieldMaps; + targetFieldMetadata: FieldMetadataEntity; + sourceFieldMetadata: FieldMetadataEntity; + }> { const fieldMetadataRepository = queryRunner ? queryRunner.manager.getRepository(FieldMetadataEntity) : this.fieldMetadataRepository; - const targetObjectMetadata = await objectMetadataRepository.findOneByOrFail( - { - standardId: targetObjectMetadataStandardId, - workspaceId: workspaceId, - isCustom: false, - }, - ); + const targetObjectMetadataId = Object.values(objectMetadataMaps.byId).find( + (objectMetadata) => + objectMetadata?.standardId === targetObjectMetadataStandardId && + objectMetadata.isCustom === false, + )?.id; + + if (!targetObjectMetadataId) { + throw new Error( + `Target object metadata id not found for standard ID: ${targetObjectMetadataStandardId}`, + ); + } + + const targetObjectMetadata = + objectMetadataMaps.byId[targetObjectMetadataId]; + + if (!targetObjectMetadata) { + throw new Error( + `Target object metadata not found for id: ${targetObjectMetadataId}`, + ); + } const targetFieldMetadataUpdateData = this.updateTargetFieldMetadata( sourceObjectMetadata, @@ -226,6 +273,25 @@ export class ObjectMetadataFieldRelationService { workspaceId: workspaceId, }); + const nameIsUpdated = + targetFieldMetadataUpdateData.name !== targetFieldMetadataToUpdate.name; + + if (nameIsUpdated) { + const targetObjectMetadataFieldsById = + objectMetadataMaps.byId[targetObjectMetadata.id]?.fieldsById; + + if (!targetObjectMetadataFieldsById) { + throw new Error( + `Target object metadata fields not found for ${targetObjectMetadata.id}`, + ); + } + + this.validateFieldNameAvailabilityOrThrow({ + name: targetFieldMetadataUpdateData.name, + fieldMetadataMapById: targetObjectMetadataFieldsById, + }); + } + const isTargetFieldMetadataManyToOneRelation = ( targetFieldMetadataToUpdate as FieldMetadataEntity @@ -293,7 +359,7 @@ export class ObjectMetadataFieldRelationService { ObjectMetadataItemWithFieldMaps, 'namePlural' | 'labelSingular' >, - ): Partial> { + ): PartialRelationFieldMetadata { const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural; const { description } = buildDescriptionForRelationFieldMetadataOnFromField( @@ -353,7 +419,7 @@ export class ObjectMetadataFieldRelationService { ObjectMetadataItemWithFieldMaps, 'namePlural' | 'labelSingular' | 'id' | 'nameSingular' >, - ): Partial> { + ): PartialRelationFieldMetadata { const customStandardFieldId = // @ts-expect-error legacy noImplicitAny STANDARD_OBJECT_FIELD_IDS[targetObjectMetadata.nameSingular].custom; @@ -524,4 +590,30 @@ export class ObjectMetadataFieldRelationService { return morphRelationFieldMetadataToUpdateWithNewJoinColumnName; } + + private validateFieldNameAvailabilityOrThrow({ + name, + fieldMetadataMapById, + }: { + name: string; + fieldMetadataMapById: FieldMetadataMap; + }) { + try { + validateFieldNameAvailabilityOrThrow({ + name, + fieldMetadataMapById, + }); + } catch (error) { + if (error instanceof InvalidMetadataException) { + throw new ObjectMetadataException( + `Name "${name}" is not available.`, + ObjectMetadataExceptionCode.NAME_CONFLICT, + { + userFriendlyMessage: `Name "${name}" is not available.`, + }, + ); + } + throw error; + } + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts index 5422c8de6be..0f98b416d35 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts @@ -142,7 +142,7 @@ export class ObjectMetadataMigrationService { currentObjectMetadata: Pick, alteredObjectMetadata: Pick, relationMetadataCollection: { - targetObjectMetadata: ObjectMetadataEntity; + targetObjectMetadata: ObjectMetadataItemWithFieldMaps; targetFieldMetadata: FieldMetadataEntity; sourceFieldMetadata: FieldMetadataEntity; }[], diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util.ts index 12f97b3697c..07896013d5d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util.ts @@ -9,7 +9,7 @@ import { export const buildDefaultFieldsForCustomObject = ( workspaceId: string, -): Partial[] => [ +): Array & { id: string }> => [ { id: v4(), standardId: BASE_OBJECT_STANDARD_FIELD_IDS.id, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util.ts index 60ec04b1ce7..34fb41a9875 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util.ts @@ -31,6 +31,7 @@ export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => { case ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT: throw new UserInputError(error); case ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED: + case ObjectMetadataExceptionCode.NAME_CONFLICT: throw new ForbiddenError(error); case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS: throw new ConflictError(error); diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-field-name-availability.spec.ts b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-field-name-availability.spec.ts index 4c82c1b4ea2..5cb9a490093 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-field-name-availability.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-field-name-availability.spec.ts @@ -64,14 +64,14 @@ describe('validateFieldNameAvailabilityOrThrow', () => { expect(() => validateFieldNameAvailabilityOrThrow({ name: input, - objectMetadata: objectMetadataMapItemMock, + fieldMetadataMapById: objectMetadataMapItemMock.fieldsById, }), ).not.toThrow(); } else { expect(() => validateFieldNameAvailabilityOrThrow({ name: input, - objectMetadata: objectMetadataMapItemMock, + fieldMetadataMapById: objectMetadataMapItemMock.fieldsById, }), ).toThrowErrorMatchingSnapshot(); } diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts index 5773ae8de3a..4ab69a95a17 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts @@ -4,18 +4,18 @@ import { FieldMetadataType } from 'twenty-shared/types'; 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 { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { InvalidMetadataException, InvalidMetadataExceptionCode, } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; const getReservedCompositeFieldNames = ( - objectMetadata: ObjectMetadataItemWithFieldMaps, + fieldMetadataMapById: FieldMetadataMap, ) => { const reservedCompositeFieldsNames: string[] = []; - for (const field of Object.values(objectMetadata.fieldsById)) { + for (const field of Object.values(fieldMetadataMapById)) { if (isCompositeFieldMetadataType(field.type)) { const base = field.name; const compositeType = compositeTypeDefinitions.get(field.type); @@ -33,17 +33,17 @@ const getReservedCompositeFieldNames = ( type ValidateFieldNameAvailabilityOrThrowArgs = { name: string; - objectMetadata: ObjectMetadataItemWithFieldMaps; + fieldMetadataMapById: FieldMetadataMap; }; export const validateFieldNameAvailabilityOrThrow = ({ name, - objectMetadata, + fieldMetadataMapById, }: ValidateFieldNameAvailabilityOrThrowArgs) => { const reservedCompositeFieldsNames = - getReservedCompositeFieldNames(objectMetadata); + getReservedCompositeFieldNames(fieldMetadataMapById); if ( - Object.values(objectMetadata.fieldsById).some( + Object.values(fieldMetadataMapById).some( (field) => field.name === name || (field.type === FieldMetadataType.RELATION && @@ -54,7 +54,7 @@ export const validateFieldNameAvailabilityOrThrow = ({ `Name "${name}" is not available as it is already used by another field`, InvalidMetadataExceptionCode.NOT_AVAILABLE, { - userFriendlyMessage: t`This name is not available as it is already used by another field`, + userFriendlyMessage: t`This name is not available as it is already used by another field.`, }, ); } diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-field-metadata-morph-relation-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-field-metadata-morph-relation-creation.integration-spec.ts.snap index 6d27f0e0ce8..d2e7d522210 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-field-metadata-morph-relation-creation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-field-metadata-morph-relation-creation.integration-spec.ts.snap @@ -6,7 +6,7 @@ exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE "extensions": { "code": "BAD_USER_INPUT", "subCode": "Name not available", - "userFriendlyMessage": "This name is not available as it is already used by another field", + "userFriendlyMessage": "This name is not available as it is already used by another field.", }, "message": "Name "collisionfieldlabel" is not available as it is already used by another field", "name": "UserInputError", @@ -103,7 +103,7 @@ exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY "extensions": { "code": "BAD_USER_INPUT", "subCode": "Name not available", - "userFriendlyMessage": "This name is not available as it is already used by another field", + "userFriendlyMessage": "This name is not available as it is already used by another field.", }, "message": "Name "collisionfieldlabel" is not available as it is already used by another field", "name": "UserInputError", 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 88c69194bb6..f8796369e96 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 @@ -6,7 +6,7 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela "extensions": { "code": "BAD_USER_INPUT", "subCode": "Name not available", - "userFriendlyMessage": "This name is not available as it is already used by another field", + "userFriendlyMessage": "This name is not available as it is already used by another field.", }, "message": "Name "fieldNameBisId" is not available as it is already used by another field", "name": "UserInputError", @@ -20,7 +20,7 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela "extensions": { "code": "BAD_USER_INPUT", "subCode": "Name not available", - "userFriendlyMessage": "This name is not available as it is already used by another field", + "userFriendlyMessage": "This name is not available as it is already used by another field.", }, "message": "Name "fieldName" is not available as it is already used by another field", "name": "UserInputError", @@ -145,7 +145,7 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela "extensions": { "code": "BAD_USER_INPUT", "subCode": "Name not available", - "userFriendlyMessage": "This name is not available as it is already used by another field", + "userFriendlyMessage": "This name is not available as it is already used by another field.", }, "message": "Name "fieldNameBisId" is not available as it is already used by another field", "name": "UserInputError", @@ -159,7 +159,7 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela "extensions": { "code": "BAD_USER_INPUT", "subCode": "Name not available", - "userFriendlyMessage": "This name is not available as it is already used by another field", + "userFriendlyMessage": "This name is not available as it is already used by another field.", }, "message": "Name "fieldName" is not available as it is already used by another field", "name": "UserInputError", diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts index 666761997d5..58d8a626c81 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts @@ -188,7 +188,33 @@ describe('Custom object renaming', () => { }); }); - it('3. should delete custom object', async () => { + it('3. should fail when trying to rename object to "name"', async () => { + // Act & Assert + const { errors } = await updateOneObjectMetadata({ + gqlFields: ` + nameSingular + labelSingular + namePlural + labelPlural + `, + input: { + idToUpdate: listingObjectId, + updatePayload: { + nameSingular: 'name', + }, + }, + expectToFail: true, + }); + + // Assert that an error was thrown + expect(errors).toBeDefined(); + expect(errors).toHaveLength(1); + expect(errors[0].extensions.userFriendlyMessage).toContain( + 'Name "name" is not available', + ); + }); + + it('4. should delete custom object', async () => { const { data } = await deleteOneObjectMetadata({ input: { idToDelete: listingObjectId,