From 4d0450069c43b7059c4adcd1d1925ab84d7bb1aa Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 11 Mar 2025 18:41:29 +0100 Subject: [PATCH] Fix fieldMetadata sync validation exceptions caught in exception handler (#10789) ## Context Field metadata service was reusing validators from validate-**OBJECT**-metadata-input which were throwing ObjectMetadata exceptions not handled in fieldMetadataGraphqlApiExceptionHandler and were going to Sentry. To solve the issue since this validator is associated with both fields and objects I'm moving the util to the root utils folder of metadata module and throwing a common metadata user input exception --- .../field-metadata/field-metadata.service.ts | 4 +- ...data-graphql-api-exception-handler.util.ts | 5 ++ .../object-metadata.service.ts | 2 +- ...data-graphql-api-exception-handler.util.ts | 5 ++ .../validate-object-metadata-input.util.ts | 46 ------------------- ...data-graphql-api-exception-handler.util.ts | 5 ++ .../compute-metadata-name-from-label.util.ts | 29 ++++++++++++ .../exceptions/invalid-metadata.exception.ts | 5 ++ ...e-name-and-label-are-sync-or-throw.util.ts | 42 +++++++++++++++++ 9 files changed, 94 insertions(+), 49 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/utils/compute-metadata-name-from-label.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts index 637447c3f2..8a6810b848 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts @@ -36,14 +36,15 @@ import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field- import { isSelectFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util'; import { RelationMetadataEntity, RelationMetadataType, } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception'; import { exceedsDatabaseIdentifierMaximumLength } from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils'; import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils'; +import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { @@ -59,7 +60,6 @@ import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { ViewService } from 'src/modules/view/services/view.service'; import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; -import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception'; import { FieldMetadataValidationService } from './field-metadata-validation.service'; import { FieldMetadataEntity } from './field-metadata.entity'; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util.ts index af8c3cd87d..e1874ebb4a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util.ts @@ -9,8 +9,13 @@ import { FieldMetadataException, FieldMetadataExceptionCode, } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; export const fieldMetadataGraphqlApiExceptionHandler = (error: Error) => { + if (error instanceof InvalidMetadataException) { + throw new UserInputError(error.message); + } + if (error instanceof FieldMetadataException) { switch (error.code) { case FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND: 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 309345de91..78fc4ea363 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 @@ -27,12 +27,12 @@ import { ObjectMetadataRelationService } from 'src/engine/metadata-modules/objec import { buildDefaultFieldsForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util'; import { validateLowerCasedAndTrimmedStringsAreDifferentOrThrow, - validateNameAndLabelAreSyncOrThrow, validateObjectMetadataInputLabelsOrThrow, validateObjectMetadataInputNamesOrThrow, } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util'; import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service'; import { SearchService } from 'src/engine/metadata-modules/search/search.service'; +import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; 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 ef00ed60f9..c4eca8e8cf 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 @@ -9,8 +9,13 @@ import { ObjectMetadataException, ObjectMetadataExceptionCode, } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => { + if (error instanceof InvalidMetadataException) { + throw new UserInputError(error.message); + } + if (error instanceof ObjectMetadataException) { switch (error.code) { case ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND: 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 2ed051c788..c9d2ad4f44 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,4 +1,3 @@ -import { slugify } from 'transliteration'; import { isDefined } from 'twenty-shared'; import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; @@ -11,7 +10,6 @@ import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/ import { validateMetadataNameIsNotTooLongOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-long.utils'; import { validateMetadataNameIsNotTooShortOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-short.utils'; import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils'; -import { camelCase } from 'src/utils/camel-case'; export const validateObjectMetadataInputNamesOrThrow = < T extends UpdateObjectPayload | CreateObjectInput, @@ -71,50 +69,6 @@ const validateObjectMetadataInputLabelOrThrow = (name: string): void => { } }; -export const computeMetadataNameFromLabel = (label: string): string => { - if (!isDefined(label)) { - throw new ObjectMetadataException( - 'Label is required', - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ); - } - - const prefixedLabel = /^\d/.test(label) ? `n${label}` : label; - - if (prefixedLabel === '') { - return ''; - } - - const formattedString = slugify(prefixedLabel, { - trim: true, - separator: '_', - allowedChars: 'a-zA-Z0-9', - }); - - if (formattedString === '') { - throw new ObjectMetadataException( - `Invalid label: "${label}"`, - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ); - } - - return camelCase(formattedString); -}; - -export const validateNameAndLabelAreSyncOrThrow = ( - label: string, - name: string, -) => { - const computedName = computeMetadataNameFromLabel(label); - - if (name !== computedName) { - throw new ObjectMetadataException( - `Name is not synced with label. Expected name: "${computedName}", got ${name}`, - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ); - } -}; - type ValidateLowerCasedAndTrimmedStringAreDifferentOrThrowArgs = { inputs: [string, string]; message: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/utils/relation-metadata-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/utils/relation-metadata-graphql-api-exception-handler.util.ts index 4367e40351..ad17d27483 100644 --- a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/utils/relation-metadata-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/utils/relation-metadata-graphql-api-exception-handler.util.ts @@ -8,8 +8,13 @@ import { RelationMetadataException, RelationMetadataExceptionCode, } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.exception'; +import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; export const relationMetadataGraphqlApiExceptionHandler = (error: Error) => { + if (error instanceof InvalidMetadataException) { + throw new UserInputError(error.message); + } + if (error instanceof RelationMetadataException) { switch (error.code) { case RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND: diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/compute-metadata-name-from-label.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/compute-metadata-name-from-label.util.ts new file mode 100644 index 0000000000..11caa9df23 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/utils/compute-metadata-name-from-label.util.ts @@ -0,0 +1,29 @@ +import camelCase from 'lodash.camelcase'; +import { slugify } from 'transliteration'; +import { isDefined } from 'twenty-shared'; + +import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; + +export const computeMetadataNameFromLabel = (label: string): string => { + if (!isDefined(label)) { + throw new InvalidMetadataException('Label is required'); + } + + const prefixedLabel = /^\d/.test(label) ? `n${label}` : label; + + if (prefixedLabel === '') { + return ''; + } + + const formattedString = slugify(prefixedLabel, { + trim: true, + separator: '_', + allowedChars: 'a-zA-Z0-9', + }); + + if (formattedString === '') { + throw new InvalidMetadataException(`Invalid label: "${label}"`); + } + + return camelCase(formattedString); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception.ts b/packages/twenty-server/src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception.ts new file mode 100644 index 0000000000..8f3e596442 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception.ts @@ -0,0 +1,5 @@ +export class InvalidMetadataException extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util.ts new file mode 100644 index 0000000000..d1c8e76aca --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util.ts @@ -0,0 +1,42 @@ +import camelCase from 'lodash.camelcase'; +import { slugify } from 'transliteration'; +import { isDefined } from 'twenty-shared'; + +import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; + +export const validateNameAndLabelAreSyncOrThrow = ( + label: string, + name: string, +) => { + const computedName = computeMetadataNameFromLabel(label); + + if (name !== computedName) { + throw new InvalidMetadataException( + `Name is not synced with label. Expected name: "${computedName}", got ${name}`, + ); + } +}; + +export const computeMetadataNameFromLabel = (label: string): string => { + if (!isDefined(label)) { + throw new InvalidMetadataException('Label is required'); + } + + const prefixedLabel = /^\d/.test(label) ? `n${label}` : label; + + if (prefixedLabel === '') { + return ''; + } + + const formattedString = slugify(prefixedLabel, { + trim: true, + separator: '_', + allowedChars: 'a-zA-Z0-9', + }); + + if (formattedString === '') { + throw new InvalidMetadataException(`Invalid label: "${label}"`); + } + + return camelCase(formattedString); +};