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 637447c3f..8a6810b84 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 af8c3cd87..e1874ebb4 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 309345de9..78fc4ea36 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 ef00ed60f..c4eca8e8c 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 2ed051c78..c9d2ad4f4 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 4367e4035..ad17d2748 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 000000000..11caa9df2 --- /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 000000000..8f3e59644 --- /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 000000000..d1c8e76ac --- /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); +};