diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook.ts index ca82a61ec8a6..0737b8ef65a9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook.ts @@ -1,8 +1,4 @@ -import { - ForbiddenException, - Injectable, - UnauthorizedException, -} from '@nestjs/common'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; import { BeforeCreateOneHook, @@ -11,27 +7,6 @@ import { import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; -const coreObjectNames = [ - 'appToken', - 'billingSubscription', - 'billingSubscriptionItem', - 'featureFlag', - 'user', - 'userWorkspace', - 'workspace', -]; - -const reservedKeywords = [ - ...coreObjectNames, - 'event', - 'field', - 'link', - 'currency', - 'fullName', - 'address', - 'links', -]; - @Injectable() export class BeforeCreateOneObject implements BeforeCreateOneHook @@ -46,14 +21,6 @@ export class BeforeCreateOneObject throw new UnauthorizedException(); } - if ( - reservedKeywords.includes(instance.input.nameSingular) || - reservedKeywords.includes(instance.input.namePlural) - ) { - throw new ForbiddenException( - 'You cannot create an object with this name.', - ); - } instance.input.workspaceId = workspaceId; return instance; 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 b6b72e49b44a..71556e8dbe64 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 @@ -58,7 +58,7 @@ import { createWorkspaceMigrationsForCustomObjectRelations } from 'src/engine/me import { createWorkspaceMigrationsForRemoteObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/create-workspace-migrations-for-remote-object-relations.util'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; -import { validateObjectMetadataInput } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util'; +import { validateObjectMetadataInputOrThrow } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util'; import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util'; import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; @@ -253,7 +253,7 @@ export class ObjectMetadataService extends TypeOrmQueryService { + validateObjectMetadataInputOrThrow(input.update); + const updatedObject = await super.updateOne(input.id, input.update); await this.workspaceCacheVersionService.incrementVersion(workspaceId); diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/validate-object-metadata-input.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/validate-object-metadata-input.util.spec.ts new file mode 100644 index 000000000000..d2208782aa53 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/validate-object-metadata-input.util.spec.ts @@ -0,0 +1,63 @@ +import { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; +import { validateObjectMetadataInputOrThrow } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util'; + +const validObjectInput: UpdateObjectPayload = { + labelPlural: 'Car', + labelSingular: 'Cars', + nameSingular: 'car', + namePlural: 'cars', +}; + +const reservedKeyword = 'user'; + +describe('validateObjectName', () => { + it('should not throw if names are valid', () => { + expect(() => + validateObjectMetadataInputOrThrow(validObjectInput), + ).not.toThrow(); + }); + + it('should throw is nameSingular has invalid characters', () => { + const invalidObjectInput = { + ...validObjectInput, + nameSingular: 'μ', + }; + + expect(() => + validateObjectMetadataInputOrThrow(invalidObjectInput), + ).toThrow(); + }); + + it('should throw is namePlural has invalid characters', () => { + const invalidObjectInput = { + ...validObjectInput, + namePlural: 'μ', + }; + + expect(() => + validateObjectMetadataInputOrThrow(invalidObjectInput), + ).toThrow(); + }); + + it('should throw if nameSingular is a reserved keyword', async () => { + const invalidObjectInput = { + ...validObjectInput, + nameSingular: reservedKeyword, + }; + + expect(() => + validateObjectMetadataInputOrThrow(invalidObjectInput), + ).toThrow(); + }); + + it('should throw if namePlural is a reserved keyword', async () => { + const invalidObjectInput = { + ...validObjectInput, + namePlural: reservedKeyword, + }; + + expect(() => + validateObjectMetadataInputOrThrow(invalidObjectInput), + ).toThrow(); + }); +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util.ts index e7214456e94c..2a9efe39b7be 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util.ts @@ -1,27 +1,70 @@ -import { BadRequestException } from '@nestjs/common'; +import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { InvalidStringException } from 'src/engine/metadata-modules/errors/InvalidStringException'; import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; import { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; import { validateMetadataName } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils'; -export const validateObjectMetadataInput = < +const coreObjectNames = [ + 'appToken', + 'billingSubscription', + 'billingSubscriptions', + 'billingSubscriptionItem', + 'billingSubscriptionItems', + 'featureFlag', + 'user', + 'users', + 'userWorkspace', + 'userWorkspaces', + 'workspace', + 'workspaces', +]; + +const reservedKeywords = [ + ...coreObjectNames, + 'event', + 'events', + 'field', + 'fields', + 'link', + 'links', + 'currency', + 'currencies', + 'fullName', + 'fullNames', + 'address', + 'addresses', +]; + +export const validateObjectMetadataInputOrThrow = < T extends UpdateObjectPayload | CreateObjectInput, >( objectMetadataInput: T, ): void => { - try { - if (objectMetadataInput.nameSingular) { - validateMetadataName(objectMetadataInput.nameSingular); + validateNameCharactersOrThrow(objectMetadataInput.nameSingular); + validateNameCharactersOrThrow(objectMetadataInput.namePlural); + + validateNameIsNotReservedKeywordOrThrow(objectMetadataInput.nameSingular); + validateNameIsNotReservedKeywordOrThrow(objectMetadataInput.namePlural); +}; + +const validateNameIsNotReservedKeywordOrThrow = (name?: string) => { + if (name) { + if (reservedKeywords.includes(name)) { + throw new ForbiddenException(`The name "${name}" is not available`); } + } +}; - if (objectMetadataInput.namePlural) { - validateMetadataName(objectMetadataInput.namePlural); +const validateNameCharactersOrThrow = (name?: string) => { + try { + if (name) { + validateMetadataName(name); } } catch (error) { if (error instanceof InvalidStringException) { throw new BadRequestException( - `Characters used in name "${objectMetadataInput.nameSingular}" or "${objectMetadataInput.namePlural}" are not supported`, + `Characters used in name "${name}" are not supported`, ); } else { throw error;