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',