From de0468d1043b792656153bdd118f6e2b91a9674b Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:49:35 +0200 Subject: [PATCH] `ObjectMetadataServiceV2.createOne` (#13849) # Introduction Introducing v2 object metadata service create one handler v2, not overkilling the flatObjectMetadata validation for the moment that will require sequential validation for the import in order to handle relations and morph relations --- .../services/field-metadata.service-v2.ts | 2 +- .../flat-field-metadata-validator.service.ts | 41 +++-- ...led-flat-field-metadata-validation.type.ts | 31 +--- .../run-flat-field-metadata-validator.util.ts | 23 +++ ...run-flat-field-metadata-validators.util.ts | 15 ++ ...date-enum-flat-field-metadata.validator.ts | 18 +- ...date-flat-field-metadata-name.validator.ts | 8 + .../flat-object-metadata-validator.service.ts | 154 ++++++++++++++++++ ...ed-flat-object-metadata-validation.type.ts | 8 + ...ject-input-to-flat-object-metadata.util.ts | 40 ++--- ...run-flat-object-metadata-validator.util.ts | 23 +++ ...un-flat-object-metadata-validators.util.ts | 15 ++ ...te-flat-object-metadata-label.validator.ts | 54 ++++++ ...ate-flat-object-metadata-name.validator.ts | 37 +++++ .../object-metadata-v2.service.ts | 123 ++++++++------ .../object-metadata.exception.ts | 1 + .../object-metadata/object-metadata.module.ts | 21 ++- .../object-metadata.resolver.ts | 2 +- .../object-metadata.service.ts | 41 ++++- ...data-graphql-api-exception-handler.util.ts | 1 + .../types/flat-metadata-validator.type.ts | 4 + ...name-flat-metadata-validators.constants.ts | 36 ++++ ...e-metadata-name-is-not-reserved-keyword.ts | 6 +- ...er-and-contain-digits-nor-letters.utils.ts | 2 +- .../utils/validate-metadata-name.utils.ts | 21 --- ...ect-with-same-name-exists-or-throw.util.ts | 22 ++- 26 files changed, 573 insertions(+), 176 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/run-flat-field-metadata-validator.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/run-flat-field-metadata-validators.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name.validator.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/run-flat-object-metadata-validator.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/run-flat-object-metadata-validators.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-label.validator.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-name.validator.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/types/flat-metadata-validator.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/utils/constants/metadata-name-flat-metadata-validators.constants.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name.utils.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts index 4f7e5b4f22..f0e873e6b1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts @@ -326,7 +326,7 @@ export class FieldMetadataServiceV2 { }); const validationErrors = - await this.flatFieldMetadataValidatorService.validateOneFlatFieldMetadataCreation( + await this.flatFieldMetadataValidatorService.validateFlatFieldMetadataCreation( { existingFlatObjectMetadataMaps: optimisticFlatObjectMetadataMaps, flatFieldMetadataToValidate: flatFieldMetadataToCreate, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service.ts index 611827869b..4b61b44ba9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service.ts @@ -17,6 +17,7 @@ import { } from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util'; import { isFlatFieldMetadataEntityOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; import { validateFlatFieldMetadataNameAvailability } from 'src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name-availability.validator'; +import { validateFlatFieldMetadataName } from 'src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name.validator'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; import { fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-with-flat-field-maps-to-flat-object-metadatas.util'; import { @@ -27,7 +28,6 @@ import { InvalidMetadataException, InvalidMetadataExceptionCode, } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; -import { validateMetadataName } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils'; import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; export type ValidateOneFieldMetadataArgs< @@ -141,14 +141,18 @@ export class FlatFieldMetadataValidatorService { if (updates.some((update) => update.property === 'name')) { errors.push( - ...[ - validateMetadataName(updatedFlatFieldMetadata.name), - validateFlatFieldMetadataNameAvailability({ - name: updatedFlatFieldMetadata.name, - flatObjectMetadata: flatObjectMetadata, - }), - ].filter(isDefined), + ...validateFlatFieldMetadataName(updatedFlatFieldMetadata.name), ); + + const nameAvailabilityValidationError = + validateFlatFieldMetadataNameAvailability({ + name: updatedFlatFieldMetadata.name, + flatObjectMetadata: flatObjectMetadata, + }); + + if (isDefined(nameAvailabilityValidationError)) { + errors.push(nameAvailabilityValidationError); + } } if (updatedFlatFieldMetadata.isLabelSyncedWithName) { @@ -241,7 +245,7 @@ export class FlatFieldMetadataValidatorService { return errors; } - async validateOneFlatFieldMetadataCreation< + async validateFlatFieldMetadataCreation< T extends FieldMetadataType = FieldMetadataType, >({ existingFlatObjectMetadataMaps, @@ -306,27 +310,20 @@ export class FlatFieldMetadataValidatorService { } } - const nameValidationResult = validateMetadataName( - flatFieldMetadataToValidate.name, + errors.push( + ...validateFlatFieldMetadataName(flatFieldMetadataToValidate.name), ); - if (isDefined(nameValidationResult)) { - errors.push(nameValidationResult); - } - - const fieldMetadataTypeValidationErrors = - await this.flatFieldMetadataTypeValidatorService.validateFlatFieldMetadataTypeSpecificities( + errors.push( + ...(await this.flatFieldMetadataTypeValidatorService.validateFlatFieldMetadataTypeSpecificities( { existingFlatObjectMetadataMaps, flatFieldMetadataToValidate, workspaceId, otherFlatObjectMetadataMapsToValidate, }, - ); - - if (fieldMetadataTypeValidationErrors.length > 0) { - errors.push(...fieldMetadataTypeValidationErrors); - } + )), + ); return errors; } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type.ts index e795f49c1a..d04723a7ec 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type.ts @@ -1,10 +1,9 @@ import { isDefined } from 'twenty-shared/utils'; -import { - FieldMetadataException, - FieldMetadataExceptionCode, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { type FieldMetadataException } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { runFlatFieldMetadataValidator } from 'src/engine/metadata-modules/flat-field-metadata/utils/run-flat-field-metadata-validator.util'; import { type ObjectMetadataException } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { type FlatMetadataValidator } from 'src/engine/metadata-modules/types/flat-metadata-validator.type'; import { type InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; export type FailedFlatFieldMetadataValidationExceptions = @@ -12,31 +11,9 @@ export type FailedFlatFieldMetadataValidationExceptions = | ObjectMetadataException | InvalidMetadataException; -export type FlatFieldMetadataValidator = { - validator: (value: T) => boolean; - message: string; -}; - -export const runFlatFieldMetadataValidator = ( - elementToValidate: T, - { message, validator }: FlatFieldMetadataValidator, -): FailedFlatFieldMetadataValidationExceptions | undefined => { - const validationFailed = validator(elementToValidate); - - if (validationFailed) { - return new FieldMetadataException( - message, - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - { - userFriendlyMessage: message, - }, - ); - } -}; - export const runFlatFieldMetadataValidators = ( elementToValidate: T, - validators: FlatFieldMetadataValidator[], + validators: FlatMetadataValidator[], ): FailedFlatFieldMetadataValidationExceptions[] => validators .map((validator) => diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/run-flat-field-metadata-validator.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/run-flat-field-metadata-validator.util.ts new file mode 100644 index 0000000000..6e80fd131c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/run-flat-field-metadata-validator.util.ts @@ -0,0 +1,23 @@ +import { + FieldMetadataException, + FieldMetadataExceptionCode, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { type FlatMetadataValidator } from 'src/engine/metadata-modules/types/flat-metadata-validator.type'; + +export const runFlatFieldMetadataValidator = ( + elementToValidate: T, + { message, validator }: FlatMetadataValidator, +): FailedFlatFieldMetadataValidationExceptions | undefined => { + const isInvalid = validator(elementToValidate); + + if (isInvalid) { + return new FieldMetadataException( + message, + FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + { + userFriendlyMessage: message, + }, + ); + } +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/run-flat-field-metadata-validators.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/run-flat-field-metadata-validators.util.ts new file mode 100644 index 0000000000..25bfa2dd18 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/run-flat-field-metadata-validators.util.ts @@ -0,0 +1,15 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { runFlatFieldMetadataValidator } from 'src/engine/metadata-modules/flat-field-metadata/utils/run-flat-field-metadata-validator.util'; +import { type FlatMetadataValidator } from 'src/engine/metadata-modules/types/flat-metadata-validator.type'; + +export const runFlatFieldMetadataValidators = ( + elementToValidate: T, + validators: FlatMetadataValidator[], +): FailedFlatFieldMetadataValidationExceptions[] => + validators + .map((validator) => + runFlatFieldMetadataValidator(elementToValidate, validator), + ) + .filter(isDefined); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-enum-flat-field-metadata.validator.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-enum-flat-field-metadata.validator.ts index af78fc2e9f..ca6fb0932c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-enum-flat-field-metadata.validator.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-enum-flat-field-metadata.validator.ts @@ -2,8 +2,8 @@ import { t } from '@lingui/core/macro'; import { isNonEmptyString } from '@sniptt/guards'; import { QUOTED_STRING_REGEX } from 'twenty-shared/constants'; import { - type EnumFieldMetadataType, FieldMetadataType, + type EnumFieldMetadataType, type NonNullableRequired, } from 'twenty-shared/types'; import { assertUnreachable, isDefined } from 'twenty-shared/utils'; @@ -21,11 +21,11 @@ import { } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { type ValidateOneFieldMetadataArgs } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service'; import { - type FailedFlatFieldMetadataValidationExceptions, - type FlatFieldMetadataValidator, runFlatFieldMetadataValidators, + type FailedFlatFieldMetadataValidationExceptions, } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatMetadataValidator } from 'src/engine/metadata-modules/types/flat-metadata-validator.type'; import { beneathDatabaseIdentifierMinimumLength, exceedsDatabaseIdentifierMaximumLength, @@ -33,7 +33,7 @@ import { import { isSnakeCaseString } from 'src/utils/is-snake-case-string'; const validateMetadataOptionId = (sanitizedId?: string) => { - const validators: FlatFieldMetadataValidator[] = [ + const validators: FlatMetadataValidator[] = [ { validator: (id) => !isDefined(id), message: t`Option id is required`, @@ -48,7 +48,7 @@ const validateMetadataOptionId = (sanitizedId?: string) => { }; const validateMetadataOptionLabel = (sanitizedLabel: string) => { - const validators: FlatFieldMetadataValidator[] = [ + const validators: FlatMetadataValidator[] = [ { validator: (label) => !isDefined(label), message: t`Option label is required`, @@ -75,7 +75,7 @@ const validateMetadataOptionLabel = (sanitizedLabel: string) => { }; const validateMetadataOptionValue = (sanitizedValue: string) => { - const validators: FlatFieldMetadataValidator[] = [ + const validators: FlatMetadataValidator[] = [ { validator: (value) => !isDefined(value), message: t`Option value is required`, @@ -109,7 +109,7 @@ const validateDuplicates = ( | FieldMetadataComplexOption[] )[number])[]; const duplicatedValidators = fieldsToCheckForDuplicates.map< - FlatFieldMetadataValidator< + FlatMetadataValidator< FieldMetadataDefaultOption[] | FieldMetadataComplexOption[] > >((field) => ({ @@ -161,7 +161,7 @@ const validateSelectDefaultValue = ( ]; } - const validators: FlatFieldMetadataValidator[] = [ + const validators: FlatMetadataValidator[] = [ { validator: (value: string) => !QUOTED_STRING_REGEX.test(value), message: 'Default value should be as quoted string', @@ -191,7 +191,7 @@ const validateMultiSelectDefaultValue = ( ]; } - const validators: FlatFieldMetadataValidator[] = [ + const validators: FlatMetadataValidator[] = [ { validator: (values) => values.length === 0, message: 'If defined default value must contain at least one value', diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name.validator.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name.validator.ts new file mode 100644 index 0000000000..d8b8fc9b71 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name.validator.ts @@ -0,0 +1,8 @@ +import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { runFlatFieldMetadataValidators } from 'src/engine/metadata-modules/flat-field-metadata/utils/run-flat-field-metadata-validators.util'; +import { METADATA_NAME_VALIDATORS } from 'src/engine/metadata-modules/utils/constants/metadata-name-flat-metadata-validators.constants'; + +export const validateFlatFieldMetadataName = ( + name: string, +): FailedFlatFieldMetadataValidationExceptions[] => + runFlatFieldMetadataValidators(name, METADATA_NAME_VALIDATORS); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service.ts new file mode 100644 index 0000000000..284db63896 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service.ts @@ -0,0 +1,154 @@ +import { Injectable } from '@nestjs/common'; + +import { t } from '@lingui/core/macro'; + +import { FlatFieldMetadataValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service'; +import { FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; +import { addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util'; +import { addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-object-metadata-to-flat-object-metadata-maps-or-throw.util'; +import { FailedFlatObjectMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { validateFlatObjectMetadataLabel } from 'src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-label.validator'; +import { validateFlatObjectMetadataNames } from 'src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-name.validator'; +import { + ObjectMetadataException, + ObjectMetadataExceptionCode, +} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; +import { doesOtherObjectWithSameNameExists } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util'; + +export type ValidateOneFlatObjectMetadataArgs = { + existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; + flatObjectMetadataToValidate: FlatObjectMetadata; + otherFlatObjectMetadataMapsToValidate?: FlatObjectMetadataMaps; + workspaceId: string; +}; + +@Injectable() +export class FlatObjectMetadataValidatorService { + constructor( + private readonly flatFieldMetadataValidatorService: FlatFieldMetadataValidatorService, + ) {} + + public async validateFlatObjectMetadataCreation({ + existingFlatObjectMetadataMaps, + flatObjectMetadataToValidate, + workspaceId, + }: ValidateOneFlatObjectMetadataArgs) { + const errors: FailedFlatObjectMetadataValidationExceptions[] = []; + + errors.push( + ...validateFlatObjectMetadataNames({ + namePlural: flatObjectMetadataToValidate.namePlural, + nameSingular: flatObjectMetadataToValidate.nameSingular, + }), + ); + + errors.push( + ...validateFlatObjectMetadataLabel({ + labelPlural: flatObjectMetadataToValidate.labelPlural, + labelSingular: flatObjectMetadataToValidate.labelSingular, + }), + ); + + if (flatObjectMetadataToValidate.isRemote) { + errors.push( + new ObjectMetadataException( + t`Remote objects are not supported yet`, + ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + ), + ); + } + + if (flatObjectMetadataToValidate.isLabelSyncedWithName === true) { + const computedNameSingular = computeMetadataNameFromLabel( + flatObjectMetadataToValidate.labelSingular, + ); + + if (computedNameSingular !== flatObjectMetadataToValidate.nameSingular) { + errors.push( + new ObjectMetadataException( + t`Singular name is not synced with singular label`, + ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + ), + ); + } + + const computedNamePlural = computeMetadataNameFromLabel( + flatObjectMetadataToValidate.labelPlural, + ); + + if (computedNamePlural !== flatObjectMetadataToValidate.namePlural) { + errors.push( + new ObjectMetadataException( + t`Plural name is not synced with plural label`, + ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + ), + ); + } + } + + if ( + doesOtherObjectWithSameNameExists({ + objectMetadataNamePlural: flatObjectMetadataToValidate.namePlural, + objectMetadataNameSingular: flatObjectMetadataToValidate.nameSingular, + objectMetadataMaps: existingFlatObjectMetadataMaps, + }) + ) { + errors.push( + new ObjectMetadataException( + 'Object already exists', + ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS, + { + userFriendlyMessage: t`Object already exists`, + }, + ), + ); + } + + const allFlatFieldMetadatasValidationErrors: FailedFlatFieldMetadataValidationExceptions[] = + []; + let existingFlatObjectMetadataMapsWithFlatObjectMetadataToBeCreatedWithoutFields = + addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow({ + flatObjectMetadata: { + ...flatObjectMetadataToValidate, + flatFieldMetadatas: [], + }, + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + }); + + for (const flatFieldMetadataToValidate of flatObjectMetadataToValidate.flatFieldMetadatas) { + const otherFlatObjectMetadataMapsToValidate = undefined; // TODO prastoin when implementing import + + const flatFieldValidatorErrors = + await this.flatFieldMetadataValidatorService.validateFlatFieldMetadataCreation( + { + existingFlatObjectMetadataMaps: + existingFlatObjectMetadataMapsWithFlatObjectMetadataToBeCreatedWithoutFields, + flatFieldMetadataToValidate, + workspaceId, + otherFlatObjectMetadataMapsToValidate, + }, + ); + + if (flatFieldValidatorErrors.length > 0) { + allFlatFieldMetadatasValidationErrors.push(...flatFieldValidatorErrors); + continue; + } + + existingFlatObjectMetadataMapsWithFlatObjectMetadataToBeCreatedWithoutFields = + addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({ + flatFieldMetadata: flatFieldMetadataToValidate, + flatObjectMetadataMaps: + existingFlatObjectMetadataMapsWithFlatObjectMetadataToBeCreatedWithoutFields, + }); + } + + if (allFlatFieldMetadatasValidationErrors.length > 0) { + errors.push(...allFlatFieldMetadatasValidationErrors); + } + + return errors; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type.ts new file mode 100644 index 0000000000..bb38c91049 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type.ts @@ -0,0 +1,8 @@ +import { type FieldMetadataException } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { type ObjectMetadataException } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { type InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; + +export type FailedFlatObjectMetadataValidationExceptions = + | FieldMetadataException + | ObjectMetadataException + | InvalidMetadataException; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata.util.ts index 9580919e20..9ff59e34e0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata.util.ts @@ -1,24 +1,20 @@ -import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils'; +import { + capitalize, + trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, +} from 'twenty-shared/utils'; import { v4 } from 'uuid'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; -import { - ObjectMetadataException, - ObjectMetadataExceptionCode, -} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; import { buildDefaultFlatFieldMetadataForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util'; -export const fromCreateObjectInputToFlatObjectMetadata = ( - rawCreateObjectInput: CreateObjectInput, -): FlatObjectMetadata => { - if (rawCreateObjectInput.isRemote) { - throw new ObjectMetadataException( - 'Remote objects are not supported', - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ); - } - +export const fromCreateObjectInputToFlatObjectMetadata = ({ + objectMetadataInput: rawCreateObjectInput, + workspaceId, +}: { + objectMetadataInput: Omit; + workspaceId: string; +}): FlatObjectMetadata => { const createObjectInput = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( rawCreateObjectInput, @@ -37,7 +33,7 @@ export const fromCreateObjectInputToFlatObjectMetadata = ( const baseCustomFlatFieldMetadatas = buildDefaultFlatFieldMetadataForCustomObject({ objectMetadataId, - workspaceId: createObjectInput.workspaceId, + workspaceId, }); return { @@ -51,19 +47,19 @@ export const fromCreateObjectInputToFlatObjectMetadata = ( isAuditLogged: true, isCustom: true, isLabelSyncedWithName: createObjectInput.isLabelSyncedWithName ?? false, - isRemote: false, + isRemote: createObjectInput.isRemote ?? false, isSearchable: true, isSystem: false, labelIdentifierFieldMetadataId: baseCustomFlatFieldMetadatas.nameField.id, - labelPlural: createObjectInput.labelPlural ?? null, - labelSingular: createObjectInput.labelSingular ?? null, - namePlural: createObjectInput.namePlural ?? null, - nameSingular: createObjectInput.nameSingular ?? null, + labelPlural: capitalize(createObjectInput.labelPlural), + labelSingular: capitalize(createObjectInput.labelSingular), + namePlural: createObjectInput.namePlural, + nameSingular: createObjectInput.nameSingular, shortcut: createObjectInput.shortcut ?? null, standardId: null, standardOverrides: null, uniqueIdentifier: objectMetadataId, targetTableName: 'DEPRECATED', - workspaceId: createObjectInput.workspaceId, + workspaceId, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/run-flat-object-metadata-validator.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/run-flat-object-metadata-validator.util.ts new file mode 100644 index 0000000000..e33b6f6c23 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/run-flat-object-metadata-validator.util.ts @@ -0,0 +1,23 @@ +import { type FailedFlatObjectMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; +import { + ObjectMetadataException, + ObjectMetadataExceptionCode, +} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { type FlatMetadataValidator } from 'src/engine/metadata-modules/types/flat-metadata-validator.type'; + +export const runFlatObjectMetadataValidator = ( + elementToValidate: T, + { message, validator }: FlatMetadataValidator, +): FailedFlatObjectMetadataValidationExceptions | undefined => { + const validationFailed = validator(elementToValidate); + + if (validationFailed) { + return new ObjectMetadataException( + message, + ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + { + userFriendlyMessage: message, + }, + ); + } +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/run-flat-object-metadata-validators.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/run-flat-object-metadata-validators.util.ts new file mode 100644 index 0000000000..791dd13ae4 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/run-flat-object-metadata-validators.util.ts @@ -0,0 +1,15 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type FailedFlatObjectMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; +import { runFlatObjectMetadataValidator } from 'src/engine/metadata-modules/flat-object-metadata/utils/run-flat-object-metadata-validator.util'; +import { type FlatMetadataValidator } from 'src/engine/metadata-modules/types/flat-metadata-validator.type'; + +export const runFlatObjectMetadataValidators = ( + elementToValidate: T, + validators: FlatMetadataValidator[], +): FailedFlatObjectMetadataValidationExceptions[] => + validators + .map((validator) => + runFlatObjectMetadataValidator(elementToValidate, validator), + ) + .filter(isDefined); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-label.validator.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-label.validator.ts new file mode 100644 index 0000000000..94ef0d3f38 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-label.validator.ts @@ -0,0 +1,54 @@ +import { t } from '@lingui/core/macro'; + +import { type FailedFlatObjectMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { runFlatObjectMetadataValidators } from 'src/engine/metadata-modules/flat-object-metadata/utils/run-flat-object-metadata-validators.util'; +import { + ObjectMetadataException, + ObjectMetadataExceptionCode, +} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { type FlatMetadataValidator } from 'src/engine/metadata-modules/types/flat-metadata-validator.type'; +import { + beneathDatabaseIdentifierMinimumLength, + exceedsDatabaseIdentifierMaximumLength, +} from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils'; + +export const validateFlatObjectMetadataLabel = ({ + labelPlural, + labelSingular, +}: Pick< + FlatObjectMetadata, + 'labelPlural' | 'labelSingular' +>): FailedFlatObjectMetadataValidationExceptions[] => { + const errors: FailedFlatObjectMetadataValidationExceptions[] = []; + const validators: FlatMetadataValidator[] = [ + { + validator: (label) => beneathDatabaseIdentifierMinimumLength(label), + message: t`Object label is too short`, + }, + { + validator: (label) => exceedsDatabaseIdentifierMaximumLength(label), + message: t`Object label is too long`, + }, + ]; + + errors.push( + ...[labelSingular, labelPlural].flatMap((label) => + runFlatObjectMetadataValidators(label, validators), + ), + ); + + const labelsAreIdentical = + labelSingular.trim().toLowerCase() === labelPlural.trim().toLowerCase(); + + if (labelsAreIdentical) { + errors.push( + new ObjectMetadataException( + t`The singular and plural labels cannot be the same for an object`, + ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + ), + ); + } + + return errors; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-name.validator.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-name.validator.ts new file mode 100644 index 0000000000..b7d7eaae96 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-name.validator.ts @@ -0,0 +1,37 @@ +import { t } from '@lingui/core/macro'; + +import { type FailedFlatObjectMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { runFlatObjectMetadataValidators } from 'src/engine/metadata-modules/flat-object-metadata/utils/run-flat-object-metadata-validators.util'; +import { + ObjectMetadataException, + ObjectMetadataExceptionCode, +} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { METADATA_NAME_VALIDATORS } from 'src/engine/metadata-modules/utils/constants/metadata-name-flat-metadata-validators.constants'; + +export const validateFlatObjectMetadataNames = ({ + namePlural, + nameSingular, +}: Pick) => { + const errors: FailedFlatObjectMetadataValidationExceptions[] = []; + + errors.push( + ...[nameSingular, namePlural].flatMap((name) => + runFlatObjectMetadataValidators(name, METADATA_NAME_VALIDATORS), + ), + ); + + const namesAreIdentical = + namePlural.trim().toLowerCase() === nameSingular.trim().toLowerCase(); + + if (namesAreIdentical) { + errors.push( + new ObjectMetadataException( + t`The singular and plural names cannot be the same for an object`, + ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + ), + ); + } + + return errors; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts index 9300833001..9637f901ca 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts @@ -1,64 +1,85 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; -import { Repository } from 'typeorm'; +import { MultipleMetadataValidationErrors } from 'src/engine/core-modules/error/multiple-metadata-validation-errors'; +import { EMPTY_FLAT_OBJECT_METADATA_MAPS } from 'src/engine/metadata-modules/flat-object-metadata-maps/constant/empty-flat-object-metadata-maps.constant'; +import { addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-object-metadata-to-flat-object-metadata-maps-or-throw.util'; +import { FlatObjectMetadataValidatorService } from 'src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { fromCreateObjectInputToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata.util'; +import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; +import { + ObjectMetadataException, + ObjectMetadataExceptionCode, +} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service'; import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.service'; -import { ObjectMetadataEntity } from './object-metadata.entity'; - @Injectable() -export class ObjectMetadataServiceV2 extends TypeOrmQueryService { +export class ObjectMetadataServiceV2 { constructor( - @InjectRepository(ObjectMetadataEntity, 'core') - private readonly objectMetadataRepository: Repository, private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, private readonly workspaceMigrationBuilderV2: WorkspaceMigrationBuilderV2Service, private readonly workspaceMigrationRunnerV2Service: WorkspaceMigrationRunnerV2Service, - ) { - super(objectMetadataRepository); + private readonly flatObjectMetadataValidatorService: FlatObjectMetadataValidatorService, + ) {} + + async createOne({ + objectMetadataInput, + workspaceId, + }: { + objectMetadataInput: Omit; + workspaceId: string; + }): Promise { + const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } = + await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps( + { + workspaceId, + }, + ); + + const flatObjectMetadataToCreate = + fromCreateObjectInputToFlatObjectMetadata({ + objectMetadataInput, + workspaceId, + }); + + const validationErrors = + await this.flatObjectMetadataValidatorService.validateFlatObjectMetadataCreation( + { + existingFlatObjectMetadataMaps, + flatObjectMetadataToValidate: flatObjectMetadataToCreate, + workspaceId, + }, + ); + + if (validationErrors.length > 0) { + throw new MultipleMetadataValidationErrors( + validationErrors, + 'Multiple validation errors occurred while creating object', + ); + } + + try { + const workspaceMigration = this.workspaceMigrationBuilderV2.build({ + fromFlatObjectMetadataMaps: EMPTY_FLAT_OBJECT_METADATA_MAPS, + toFlatObjectMetadataMaps: + addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow({ + flatObjectMetadataMaps: EMPTY_FLAT_OBJECT_METADATA_MAPS, + flatObjectMetadata: flatObjectMetadataToCreate, + }), + inferDeletionFromMissingObjectFieldIndex: false, + workspaceId, + }); + + await this.workspaceMigrationRunnerV2Service.run(workspaceMigration); + } catch { + throw new ObjectMetadataException( + 'Workspace migration failed to run', + ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR, + ); + } + + return flatObjectMetadataToCreate; // TODO retrieve from cache } - - // override async createOne( - // objectMetadataInput: CreateObjectInput, - // ): Promise { - // const { flatObjectMetadataMaps } = - // await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps( - // { - // workspaceId: objectMetadataInput.workspaceId, - // }, - // ); - - // const createdRawFlatObjectMetadata = - // fromCreateObjectInputToFlatObjectMetadata(objectMetadataInput); - // const existingFlatObjectMetadatas: FlatObjectMetadataMaps = { - // byId: {}, - // idByNameSingular: {}, - // }; - // // @ts-expect-error TODO implement validateFlatObjectMetadata - // const createdFlatObjectMetadata = validateFlatObjectMetadata({ - // existing: - // // Here we assume that EVERYTHING is in cache and up to date, this is very critical, also race condition prone :thinking: - // fromFlatObjectMetadataMapsToFlatObjectMetadatas(flatObjectMetadataMaps), - // toValidate: [createdRawFlatObjectMetadata], - // }); - - // const workspaceMigration = this.workspaceMigrationBuilderV2.build({ - // objectMetadataFromToInputs: { - // from: fromFlatObjectMetadataMapsToFlatObjectMetadatas( - // existingFlatObjectMetadatas, - // ), - // to: [createdFlatObjectMetadata], - // }, - // inferDeletionFromMissingObjectFieldIndex: false, - // workspaceId: objectMetadataInput.workspaceId, - // }); - - // await this.workspaceMigrationRunnerV2Service.run(workspaceMigration); - - // return createdFlatObjectMetadata; // TODO retrieve from cache - // } } 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 ffaa7e19e9..61aba60c68 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 @@ -9,4 +9,5 @@ export enum ObjectMetadataExceptionCode { OBJECT_ALREADY_EXISTS = 'OBJECT_ALREADY_EXISTS', 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', } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts index 359c266e4f..ba7b5af650 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts @@ -15,10 +15,19 @@ import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service'; +import { FlatFieldMetadataValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service'; +import { FlatObjectMetadataValidatorService } from 'src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service'; import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module'; +import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; +import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; +import { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metadata/object-metadata.resolver'; +import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { ObjectMetadataFieldRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service'; import { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service'; import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service'; @@ -37,13 +46,6 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor import { WorkspaceMigrationBuilderV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module'; import { WorkspaceMigrationRunnerV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.module'; -import { ObjectMetadataEntity } from './object-metadata.entity'; -import { ObjectMetadataService } from './object-metadata.service'; - -import { CreateObjectInput } from './dtos/create-object.input'; -import { ObjectMetadataDTO } from './dtos/object-metadata.dto'; -import { UpdateObjectPayload } from './dtos/update-object.input'; - @Module({ imports: [ NestjsQueryGraphQLModule.forFeature({ @@ -72,6 +74,10 @@ import { UpdateObjectPayload } from './dtos/update-object.input'; ], services: [ ObjectMetadataService, + ObjectMetadataServiceV2, + FlatObjectMetadataValidatorService, + FlatFieldMetadataValidatorService, + FlatFieldMetadataTypeValidatorService, ObjectMetadataMigrationService, ObjectMetadataFieldRelationService, ObjectMetadataRelatedRecordsService, @@ -102,6 +108,7 @@ import { UpdateObjectPayload } from './dtos/update-object.input'; ], providers: [ ObjectMetadataService, + ObjectMetadataServiceV2, ObjectMetadataResolver, BeforeUpdateOneObject, ], diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts index 3fefb23719..72889e9014 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts @@ -21,8 +21,8 @@ import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dto import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; import { - type UpdateObjectPayload, UpdateOneObjectInput, + type UpdateObjectPayload, } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; 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 f73e496b2f..d1ec2a558b 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 @@ -6,13 +6,15 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { FieldMetadataType } from 'twenty-shared/types'; import { capitalize, isDefined } from 'twenty-shared/utils'; import { + In, + Repository, type FindManyOptions, type FindOneOptions, - In, type QueryRunner, - Repository, } from 'typeorm'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service'; @@ -21,6 +23,7 @@ import { type UpdateObjectPayload, type UpdateOneObjectInput, } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { ObjectMetadataException, ObjectMetadataExceptionCode, @@ -70,6 +73,8 @@ export class ObjectMetadataService extends TypeOrmQueryService { + const isWorkspaceMigrationV2Enabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + objectMetadataInput.workspaceId, + ); + + if (isWorkspaceMigrationV2Enabled) { + const flatObjectMetadata = await this.objectMetadataServiceV2.createOne({ + objectMetadataInput, + workspaceId: objectMetadataInput.workspaceId, + }); + + // Since V2 returns FlatObjectMetadata, we need to fetch the created entity + const createdObjectMetadata = await this.objectMetadataRepository.findOne( + { + where: { + id: flatObjectMetadata.id, + workspaceId: objectMetadataInput.workspaceId, + }, + }, + ); + + if (!isDefined(createdObjectMetadata)) { + throw new ObjectMetadataException( + 'Created object metadata not found', + ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, + ); + } + + return createdObjectMetadata; + } + const mainDataSource = await this.workspaceDataSourceService.connectToMainDataSource(); const queryRunner = mainDataSource.createQueryRunner(); 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 fcda156ce3..a1013b2609 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 @@ -28,6 +28,7 @@ export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => { throw new ForbiddenError(error); case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS: throw new ConflictError(error); + case ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR: case ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT: throw new InternalServerError(error); case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD: diff --git a/packages/twenty-server/src/engine/metadata-modules/types/flat-metadata-validator.type.ts b/packages/twenty-server/src/engine/metadata-modules/types/flat-metadata-validator.type.ts new file mode 100644 index 0000000000..494e211d6c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/types/flat-metadata-validator.type.ts @@ -0,0 +1,4 @@ +export type FlatMetadataValidator = { + validator: (value: T) => boolean; + message: string; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/constants/metadata-name-flat-metadata-validators.constants.ts b/packages/twenty-server/src/engine/metadata-modules/utils/constants/metadata-name-flat-metadata-validators.constants.ts new file mode 100644 index 0000000000..1d5e12fc0b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/utils/constants/metadata-name-flat-metadata-validators.constants.ts @@ -0,0 +1,36 @@ +import { t } from '@lingui/core/macro'; +import camelCase from 'lodash.camelcase'; + +import { type FlatMetadataValidator } from 'src/engine/metadata-modules/types/flat-metadata-validator.type'; +import { + beneathDatabaseIdentifierMinimumLength, + exceedsDatabaseIdentifierMaximumLength, +} from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils'; +import { RESERVED_METADATA_NAME_KEYWORDS } from 'src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword'; +import { STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX } from 'src/engine/metadata-modules/utils/validate-metadata-name-start-with-lowercase-letter-and-contain-digits-nor-letters.utils'; + +export const METADATA_NAME_VALIDATORS: FlatMetadataValidator[] = [ + { + message: t`Name is too long`, + validator: (name) => exceedsDatabaseIdentifierMaximumLength(name), + }, + { + message: t`Name is too short`, + validator: (name) => beneathDatabaseIdentifierMinimumLength(name), + }, + { + message: t`Name should be in camelCase`, + validator: (name) => name !== camelCase(name), + }, + { + message: t`Name is not valid: it must start with lowercase letter and contain only alphanumeric letters`, + validator: (name) => + !name.match( + STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX, + ), + }, + { + message: t`The name is not available`, + validator: (name) => RESERVED_METADATA_NAME_KEYWORDS.includes(name), + }, +]; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts index 56452c240c..5527410edf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts @@ -42,7 +42,7 @@ const coreObjectNames = [ 'userWorkspaceRoles', ]; -const reservedKeywords = [ +export const RESERVED_METADATA_NAME_KEYWORDS = [ ...coreObjectNames, 'plan', 'plans', @@ -64,14 +64,12 @@ const reservedKeywords = [ 'index', 'relation', 'relations', - 'position', - 'positions', ]; export const validateMetadataNameIsNotReservedKeywordOrThrow = ( name: string, ) => { - if (reservedKeywords.includes(name)) { + if (RESERVED_METADATA_NAME_KEYWORDS.includes(name)) { throw new InvalidMetadataException( `The name "${name}" is not available`, InvalidMetadataExceptionCode.RESERVED_KEYWORD, diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-start-with-lowercase-letter-and-contain-digits-nor-letters.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-start-with-lowercase-letter-and-contain-digits-nor-letters.utils.ts index 0e60c5f4fb..9915605947 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-start-with-lowercase-letter-and-contain-digits-nor-letters.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-start-with-lowercase-letter-and-contain-digits-nor-letters.utils.ts @@ -5,7 +5,7 @@ import { InvalidMetadataExceptionCode, } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; -const STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX = +export const STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX = /^[a-z][a-zA-Z0-9]*$/; export const validateMetadataNameStartWithLowercaseLetterAndContainDigitsNorLettersOrThrow = diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name.utils.ts deleted file mode 100644 index 167c06f4cd..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name.utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - FieldMetadataException, - FieldMetadataExceptionCode, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; -import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils'; - -export const validateMetadataName = ( - name: string, -): undefined | FieldMetadataException => { - try { - validateMetadataNameOrThrow(name); - } catch (error) { - return new FieldMetadataException( - error.message, - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - { - userFriendlyMessage: error.userFriendlyMessage, - }, - ); - } -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts index db17e1e282..33330c5f11 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts @@ -1,6 +1,7 @@ import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; +import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; import { ObjectMetadataException, ObjectMetadataExceptionCode, @@ -11,18 +12,18 @@ type ValidateNoOtherObjectWithSameNameExistsOrThrowsParams = { objectMetadataNameSingular: string; objectMetadataNamePlural: string; existingObjectMetadataId?: string; - objectMetadataMaps: ObjectMetadataMaps; + objectMetadataMaps: ObjectMetadataMaps | FlatObjectMetadataMaps; }; -export const validatesNoOtherObjectWithSameNameExistsOrThrows = ({ - objectMetadataNameSingular, - objectMetadataNamePlural, - existingObjectMetadataId, +export const doesOtherObjectWithSameNameExists = ({ objectMetadataMaps, -}: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams) => { - const objectAlreadyExists = Object.values(objectMetadataMaps.byId) + objectMetadataNamePlural, + objectMetadataNameSingular, + existingObjectMetadataId, +}: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams) => + Object.values(objectMetadataMaps.byId) .filter(isDefined) - .find( + .some( (objectMetadata) => (objectMetadata.nameSingular === objectMetadataNameSingular || objectMetadata.namePlural === objectMetadataNamePlural || @@ -31,6 +32,11 @@ export const validatesNoOtherObjectWithSameNameExistsOrThrows = ({ objectMetadata.id !== existingObjectMetadataId, ); +export const validatesNoOtherObjectWithSameNameExistsOrThrows = ( + args: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams, +) => { + const objectAlreadyExists = doesOtherObjectWithSameNameExists(args); + if (objectAlreadyExists) { throw new ObjectMetadataException( 'Object already exists',