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 1dedcc3a2c..94761c77b6 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 52b732dbd8..9635b6281f 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 61aba60c68..e24e6b3af0 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 5fc6a6dbbc..66c334a447 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 5422c8de6b..0f98b416d3 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 12f97b3697..07896013d5 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 60ec04b1ce..34fb41a987 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 4c82c1b4ea..5cb9a49009 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 5773ae8de3..4ab69a95a1 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 6d27f0e0ce..d2e7d52221 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 88c69194bb..f8796369e9 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 666761997d..58d8a626c8 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,