Files
twenty/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts
T
Marie 0d5fedfb0a 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.
2025-09-01 16:35:40 +00:00

227 lines
6.9 KiB
TypeScript

import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.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 { findManyObjectMetadataQueryFactory } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-query-factory.util';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { FieldMetadataType } from 'twenty-shared/types';
describe('Custom object renaming', () => {
let listingObjectId = '';
const STANDARD_OBJECT_RELATIONS = [
'noteTarget',
'attachment',
'favorite',
'taskTarget',
'timelineActivity',
];
const standardObjectRelationsMap = STANDARD_OBJECT_RELATIONS.reduce(
(acc, relation) => ({
...acc,
[relation]: {
objectMetadataId: '',
foreignKeyFieldMetadataId: '',
relationFieldMetadataId: '',
},
}),
{},
);
const standardObjectsGraphqlOperation = findManyObjectMetadataQueryFactory({
gqlFields: `
id
nameSingular
`,
input: {
filter: {
isCustom: { isNot: true },
},
paging: { first: 1000 },
},
});
const fieldsGraphqlOperation = findManyFieldsMetadataQueryFactory({
gqlFields: `
id
name
label
type
object {
id
}
`,
input: {
filter: {},
paging: { first: 1000 },
},
});
// @ts-expect-error legacy noImplicitAny
const fillStandardObjectRelationsMapObjectMetadataId = (standardObjects) => {
STANDARD_OBJECT_RELATIONS.forEach((relation) => {
// @ts-expect-error legacy noImplicitAny
standardObjectRelationsMap[relation].objectMetadataId =
standardObjects.body.data.objects.edges.find(
// @ts-expect-error legacy noImplicitAny
(object) => object.node.nameSingular === relation,
).node.id;
});
};
it('1. should create one custom object with standard relations', async () => {
// Arrange
const standardObjects = await makeMetadataAPIRequest(
standardObjectsGraphqlOperation,
);
fillStandardObjectRelationsMapObjectMetadataId(standardObjects);
const CUSTOM_OBJECT = {
namePlural: 'customObjectNamePlural',
nameSingular: 'customObjectNameSingular',
labelPlural: 'customObjectLabelPlural',
labelSingular: 'customObjectLabelSingular',
description: 'Custom object description',
icon: 'IconListNumbers',
isLabelSyncedWithName: false,
};
// Act
const { data } = await createOneObjectMetadata({
input: CUSTOM_OBJECT,
gqlFields: `
id
nameSingular
`,
});
// Assert
expect(data.createOneObject.nameSingular).toBe(CUSTOM_OBJECT.nameSingular);
listingObjectId = data.createOneObject.id;
const fields = await makeMetadataAPIRequest(fieldsGraphqlOperation);
const relationFieldsMetadataForListing = fields.body.data.fields.edges
.filter(
// @ts-expect-error legacy noImplicitAny
(field) =>
field.node.name === `${CUSTOM_OBJECT.nameSingular}` &&
field.node.type === FieldMetadataType.RELATION,
)
// @ts-expect-error legacy noImplicitAny
.map((field) => field.node);
STANDARD_OBJECT_RELATIONS.forEach((relation) => {
// relation field
const relationFieldMetadataId = relationFieldsMetadataForListing.find(
// @ts-expect-error legacy noImplicitAny
(field) =>
field.object.id ===
// @ts-expect-error legacy noImplicitAny
standardObjectRelationsMap[relation].objectMetadataId,
).id;
expect(relationFieldMetadataId).not.toBeUndefined();
// @ts-expect-error legacy noImplicitAny
standardObjectRelationsMap[relation].relationFieldMetadataId =
relationFieldMetadataId;
});
});
it('2. should rename custom object', async () => {
// Arrange
const HOUSE_NAME_SINGULAR = 'house';
const HOUSE_NAME_PLURAL = 'houses';
const HOUSE_LABEL_SINGULAR = 'House';
const HOUSE_LABEL_PLURAL = 'Houses';
// Act
const { data } = await updateOneObjectMetadata({
gqlFields: `
nameSingular
labelSingular
namePlural
labelPlural
`,
input: {
idToUpdate: listingObjectId,
updatePayload: {
nameSingular: HOUSE_NAME_SINGULAR,
namePlural: HOUSE_NAME_PLURAL,
labelSingular: HOUSE_LABEL_SINGULAR,
labelPlural: HOUSE_LABEL_PLURAL,
},
},
});
// Assert
expect(data.updateOneObject.nameSingular).toBe(HOUSE_NAME_SINGULAR);
expect(data.updateOneObject.namePlural).toBe(HOUSE_NAME_PLURAL);
expect(data.updateOneObject.labelSingular).toBe(HOUSE_LABEL_SINGULAR);
expect(data.updateOneObject.labelPlural).toBe(HOUSE_LABEL_PLURAL);
const fieldsResponse = await makeMetadataAPIRequest(fieldsGraphqlOperation);
const fieldsMetadata = fieldsResponse.body.data.fields.edges.map(
// @ts-expect-error legacy noImplicitAny
(field) => field.node,
);
// standard relations have been updated
STANDARD_OBJECT_RELATIONS.forEach((relation) => {
// relation field
const relationFieldMetadataId =
// @ts-expect-error legacy noImplicitAny
standardObjectRelationsMap[relation].relationFieldMetadataId;
const updatedRelationFieldMetadata = fieldsMetadata.find(
// @ts-expect-error legacy noImplicitAny
(field) => field.id === relationFieldMetadataId,
);
expect(updatedRelationFieldMetadata.name).toBe(HOUSE_NAME_SINGULAR);
expect(updatedRelationFieldMetadata.label).toBe(HOUSE_LABEL_SINGULAR);
});
});
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,
},
});
expect(data.deleteOneObject.id).toBe(listingObjectId);
});
});