diff --git a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts index dc72141a38..a5ec16eae6 100644 --- a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts +++ b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts @@ -416,7 +416,7 @@ export const fieldPhonesMock = getMockFieldMetadataEntity({ updatedAt: new Date(), }); -export const fields = [ +const FIELDS_MOCK = [ fieldUuidMock, fieldTextMock, fieldPhonesMock, @@ -457,21 +457,21 @@ export const objectMetadataItemMock: ObjectMetadataEntity = { isSystem: false, isAuditLogged: true, isSearchable: true, - fields, + fields: FIELDS_MOCK, createdAt: new Date(), updatedAt: new Date(), } as ObjectMetadataEntity; export const objectMetadataMapItemMock: ObjectMetadataItemWithFieldMaps = { ...objectMetadataItemMock, - fieldsById: fields.reduce( + fieldsById: FIELDS_MOCK.reduce( (acc, field) => ({ ...acc, [field.id]: field, }), {}, ), - fieldIdByName: fields.reduce( + fieldIdByName: FIELDS_MOCK.reduce( (acc, field) => ({ ...acc, [field.name]: field.id, diff --git a/packages/twenty-server/src/engine/core-modules/error/multiple-metadata-validation-errors.ts b/packages/twenty-server/src/engine/core-modules/error/multiple-metadata-validation-errors.ts deleted file mode 100644 index 170121ab5c..0000000000 --- a/packages/twenty-server/src/engine/core-modules/error/multiple-metadata-validation-errors.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class MultipleMetadataValidationErrors extends Error { - constructor( - public readonly errors: Error[], - message = 'Multiple errors occurred', - ) { - super(message); - this.name = 'AggregateError'; - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.exception.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.exception.ts index 72bd3f94a1..314bc65ac6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.exception.ts @@ -19,4 +19,11 @@ export const FieldMetadataExceptionCode = appendCommonExceptionCode({ 'LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND', UNCOVERED_FIELD_METADATA_TYPE_VALIDATION: 'UNCOVERED_FIELD_METADATA_TYPE_VALIDATION', + RESERVED_KEYWORD: 'RESERVED_KEYWORD', + NOT_AVAILABLE: 'NOT_AVAILABLE', + NAME_NOT_SYNCED_WITH_LABEL: 'NAME_NOT_SYNCED_WITH_LABEL', } as const); + +// eslint-disable-next-line no-redeclare +export type FieldMetadataExceptionCode = + (typeof FieldMetadataExceptionCode)[keyof typeof FieldMetadataExceptionCode]; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts index 12db58b8b4..8805fc08b2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts @@ -34,8 +34,9 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; -import { ViewModule } from 'src/modules/view/view.module'; import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module'; +import { ViewModule } from 'src/modules/view/view.module'; +import { WorkspaceMigrationBuilderExceptionV2Interceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2.interceptor'; import { FieldMetadataEntity } from './field-metadata.entity'; @@ -100,7 +101,10 @@ import { FieldMetadataService } from './services/field-metadata.service'; }, delete: { disabled: true }, guards: [WorkspaceAuthGuard], - interceptors: [FieldMetadataGraphqlApiExceptionInterceptor], + interceptors: [ + WorkspaceMigrationBuilderExceptionV2Interceptor, + FieldMetadataGraphqlApiExceptionInterceptor, + ], }, ], }), 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 b19f555331..4cee4d6467 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 @@ -26,6 +26,7 @@ import { deleteFieldFromFlatObjectMetadataMapsOrThrow } from 'src/engine/metadat import { getSubFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/get-sub-flat-object-metadata-maps-or-throw.util'; import { replaceFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/replace-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; +import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service'; @Injectable() @@ -98,19 +99,25 @@ export class FieldMetadataServiceV2 { flatObjectMetadataMapsWithImpactedObject, ); - await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( - { - buildOptions: { - isSystemBuild: false, - inferDeletionFromMissingObjectFieldIndex: true, + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + buildOptions: { + isSystemBuild: false, + inferDeletionFromMissingObjectFieldIndex: true, + }, + fromFlatObjectMetadataMaps: flatObjectMetadataMapsWithImpactedObject, + toFlatObjectMetadataMaps, + workspaceId, }, - fromFlatObjectMetadataMaps: flatObjectMetadataMapsWithImpactedObject, - toFlatObjectMetadataMaps, - workspaceId, - errorMessage: - 'Multiple validation errors occurred while deleting field', - }, - ); + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderExceptionV2( + validateAndBuildResult, + 'Multiple validation errors occurred while deleting field', + ); + } return fromFlatFieldMetadataToFieldMetadataDto( flatFieldMetadatasToDelete[0], @@ -153,19 +160,25 @@ export class FieldMetadataServiceV2 { flatFieldMetadata: optimisticiallyUpdatedFlatFieldMetadata, }); - await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( - { - fromFlatObjectMetadataMaps, - toFlatObjectMetadataMaps, - buildOptions: { - isSystemBuild: false, - inferDeletionFromMissingObjectFieldIndex: false, + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + fromFlatObjectMetadataMaps, + toFlatObjectMetadataMaps, + buildOptions: { + isSystemBuild: false, + inferDeletionFromMissingObjectFieldIndex: false, + }, + workspaceId, }, - workspaceId, - errorMessage: - 'Multiple validation errors occurred while updating field', - }, - ); + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderExceptionV2( + validateAndBuildResult, + 'Multiple validation errors occurred while updating field', + ); + } return this.fieldMetadataRepository.findOneOrFail({ where: { @@ -236,19 +249,25 @@ export class FieldMetadataServiceV2 { }), ); - await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( - { - fromFlatObjectMetadataMaps, - toFlatObjectMetadataMaps, - buildOptions: { - isSystemBuild: false, - inferDeletionFromMissingObjectFieldIndex: false, + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + fromFlatObjectMetadataMaps, + toFlatObjectMetadataMaps, + buildOptions: { + isSystemBuild: false, + inferDeletionFromMissingObjectFieldIndex: false, + }, + workspaceId, }, - workspaceId, - errorMessage: - 'Multiple validation errors occurred while creating fields', - }, - ); + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderExceptionV2( + validateAndBuildResult, + 'Multiple validation errors occurred while creating fields', + ); + } return this.fieldMetadataRepository.find({ where: { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util.ts index c50187de6b..4e876b809e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util.ts @@ -33,6 +33,9 @@ export const fieldMetadataGraphqlApiExceptionHandler = (error: Error) => { case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED: case FieldMetadataExceptionCode.UNCOVERED_FIELD_METADATA_TYPE_VALIDATION: case FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND: + case FieldMetadataExceptionCode.RESERVED_KEYWORD: + case FieldMetadataExceptionCode.NOT_AVAILABLE: + case FieldMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL: throw error; default: { return assertUnreachable(error.code); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts index e8f0d54ca5..5867cddc86 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { t } from '@lingui/core/macro'; import { isDefined } from 'class-validator'; import { FieldMetadataType } from 'twenty-shared/types'; @@ -10,8 +11,8 @@ import { FieldMetadataExceptionCode, } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { ValidateOneFieldMetadataArgs } 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 FlatFieldMetadataTypeValidator } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type'; +import { FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; import { isEnumValidateOneFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-validate-one-field-metadata-args.util'; import { validateEnumSelectFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util'; import { validateRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-flat-field-metadata.util'; @@ -60,10 +61,11 @@ export class FlatFieldMetadataTypeValidatorService { if (!isMorphRelationEnabled) { return [ - new FieldMetadataException( - 'Morph relation feature is disabled', - FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, - ), + { + code: FieldMetadataExceptionCode.UNCOVERED_FIELD_METADATA_TYPE_VALIDATION, + message: 'Morph relation feature flag is disabled', + userFriendlyMessage: t`Morph relation fields are disabled for your workspace`, + }, ]; } @@ -72,7 +74,7 @@ export class FlatFieldMetadataTypeValidatorService { MULTI_SELECT: (args) => { if (!isEnumValidateOneFieldMetadata(args)) { throw new FieldMetadataException( - 'Should never occur, invaliad enum field metadata type', + 'Should never occur, invalid enum field metadata type', FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, ); } @@ -94,7 +96,7 @@ export class FlatFieldMetadataTypeValidatorService { RATING: (args) => { if (!isEnumValidateOneFieldMetadata(args)) { throw new FieldMetadataException( - 'Should never occur, invaliad enum field metadata type', + 'Should never occur, invalid enum field metadata type', FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, ); } @@ -114,7 +116,7 @@ export class FlatFieldMetadataTypeValidatorService { SELECT: (args) => { if (!isEnumValidateOneFieldMetadata(args)) { throw new FieldMetadataException( - 'Should never occur, invaliad enum field metadata type', + 'Should never occur, invalid enum field metadata type', FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, ); } @@ -140,7 +142,7 @@ export class FlatFieldMetadataTypeValidatorService { workspaceId, otherFlatObjectMetadataMapsToValidate, }: ValidateOneFieldMetadataArgs): Promise< - FailedFlatFieldMetadataValidationExceptions[] + FlatFieldMetadataValidationError[] > { const fieldMetadataTypeValidator = this.FIELD_METADATA_TYPE_VALIDATOR_HASHMAP[ @@ -149,10 +151,12 @@ export class FlatFieldMetadataTypeValidatorService { if (!isDefined(fieldMetadataTypeValidator)) { return [ - new FieldMetadataException( - 'Unsupported field metadata type', - FieldMetadataExceptionCode.UNCOVERED_FIELD_METADATA_TYPE_VALIDATION, - ), + { + code: FieldMetadataExceptionCode.UNCOVERED_FIELD_METADATA_TYPE_VALIDATION, + message: `Unsupported field metadata type ${flatFieldMetadataToValidate.type}`, + value: flatFieldMetadataToValidate.type, + userFriendlyMessage: t`Unsupported field metadata type ${flatFieldMetadataToValidate.type}`, + }, ]; } 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 aee28483ef..48c8f89d09 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 @@ -4,13 +4,10 @@ import { t } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { - FieldMetadataException, - FieldMetadataExceptionCode, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant'; import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service'; -import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; import { FlatFieldMetadataRelationPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-relation-properties-to-compare.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { compareTwoFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util'; @@ -21,14 +18,6 @@ import { validateFlatFieldMetadataNameAvailability } from 'src/engine/metadata-m import { validateFlatFieldMetadataName } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name.util'; 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 { - ObjectMetadataException, - ObjectMetadataExceptionCode, -} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; -import { - InvalidMetadataException, - InvalidMetadataExceptionCode, -} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util'; export type ValidateOneFieldMetadataArgs< @@ -50,34 +39,41 @@ export class FlatFieldMetadataValidatorService { existingFlatObjectMetadataMaps, flatFieldMetadataToValidate: updatedFlatFieldMetadata, workspaceId, - }: ValidateOneFieldMetadataArgs): Promise< - FailedFlatFieldMetadataValidationExceptions[] - > { - const errors: FailedFlatFieldMetadataValidationExceptions[] = []; + }: ValidateOneFieldMetadataArgs): Promise { + const validationResult: FailedFlatFieldMetadataValidation = { + type: 'update_field', + errors: [], + fieldMinimalInformation: { + id: updatedFlatFieldMetadata.id, + name: updatedFlatFieldMetadata.name, + objectMetadataId: updatedFlatFieldMetadata.objectMetadataId, + }, + }; const flatObjectMetadataWithFlatFieldMaps = existingFlatObjectMetadataMaps.byId[ updatedFlatFieldMetadata.objectMetadataId ]; if (!isDefined(flatObjectMetadataWithFlatFieldMaps)) { - return [ - new FieldMetadataException( - 'field metadata to update object metadata not found', - FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ), - ]; + validationResult.errors.push({ + code: FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, + message: 'field metadata to update object metadata not found', + userFriendlyMessage: t`Object related to field to update not found`, + }); + + return validationResult; } + if ( !isDefined( flatObjectMetadataWithFlatFieldMaps.labelIdentifierFieldMetadataId, ) ) { - errors.push( - new FieldMetadataException( - 'Label identifier field metadata id does not exist', - FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND, - ), - ); + validationResult.errors.push({ + code: FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND, + message: 'Label identifier field metadata id does not exist', + userFriendlyMessage: t`Object related to updated field does not have a label identifier`, + }); } const existingFlatFieldMetadataToUpdate = @@ -86,14 +82,13 @@ export class FlatFieldMetadataValidatorService { ]; if (!isDefined(existingFlatFieldMetadataToUpdate)) { - errors.push( - new FieldMetadataException( - 'field metadata to update not found', - FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, - ), - ); + validationResult.errors.push({ + code: FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, + message: 'field metadata to update not found', + userFriendlyMessage: t`Field to update not found`, + }); - return errors; + return validationResult; } const updates = compareTwoFlatFieldMetadata({ @@ -121,12 +116,11 @@ export class FlatFieldMetadataValidatorService { ); if (relationNonEditableUpdatedProperties.length > 0) { - errors.push( - new FieldMetadataException( - `Forbidden updated properties for relation field metadata: ${relationNonEditableUpdatedProperties.join(', ')}`, - FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED, - ), - ); + validationResult.errors.push({ + code: FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED, + message: `Forbidden updated properties for relation field metadata: ${relationNonEditableUpdatedProperties.join(', ')}`, + userFriendlyMessage: t`Forbidden updated properties for relation field metadata`, + }); } } @@ -136,31 +130,24 @@ export class FlatFieldMetadataValidatorService { ); if (updates.some((update) => update.property === 'name')) { - errors.push( + validationResult.errors.push( ...validateFlatFieldMetadataName(updatedFlatFieldMetadata.name), - ); - - const nameAvailabilityValidationError = - validateFlatFieldMetadataNameAvailability({ + ...validateFlatFieldMetadataNameAvailability({ name: updatedFlatFieldMetadata.name, flatObjectMetadata: flatObjectMetadata, - }); - - if (isDefined(nameAvailabilityValidationError)) { - errors.push(nameAvailabilityValidationError); - } + }), + ); } if ( updatedFlatFieldMetadata.isLabelSyncedWithName && !isFlatFieldMetadataNameSyncedWithLabel(updatedFlatFieldMetadata) ) { - errors.push( - new InvalidMetadataException( - t`Name is not synced with label.`, - InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL, - ), - ); + validationResult.errors.push({ + code: FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED, + message: `Name is not synced with label.`, + userFriendlyMessage: t`Updated field name is not synced with label`, + }); } const fieldMetadataTypeValidationErrors = @@ -173,10 +160,10 @@ export class FlatFieldMetadataValidatorService { ); if (fieldMetadataTypeValidationErrors.length > 0) { - errors.push(...fieldMetadataTypeValidationErrors); + validationResult.errors.push(...fieldMetadataTypeValidationErrors); } - return errors; + return validationResult; } validateFlatFieldMetadataDeletion({ @@ -185,8 +172,16 @@ export class FlatFieldMetadataValidatorService { }: { flatFieldMetadataToDelete: FlatFieldMetadata; existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; - }): FailedFlatFieldMetadataValidationExceptions[] { - const errors: FailedFlatFieldMetadataValidationExceptions[] = []; + }): FailedFlatFieldMetadataValidation { + const validationResult: FailedFlatFieldMetadataValidation = { + type: 'delete_field', + errors: [], + fieldMinimalInformation: { + id: flatFieldMetadataToDelete.id, + name: flatFieldMetadataToDelete.name, + objectMetadataId: flatFieldMetadataToDelete.objectMetadataId, + }, + }; const flatObjectMetadataWithFieldMaps = existingFlatObjectMetadataMaps.byId[ @@ -194,26 +189,22 @@ export class FlatFieldMetadataValidatorService { ]; if (!isDefined(flatObjectMetadataWithFieldMaps)) { - errors.push( - new FieldMetadataException( - 'field to delete object metadata not found', - FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ), - ); + validationResult.errors.push({ + code: FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, + message: 'field to delete object metadata not found', + userFriendlyMessage: t`Object related to field to delete not found`, + }); } else { if ( flatObjectMetadataWithFieldMaps.labelIdentifierFieldMetadataId === flatFieldMetadataToDelete.id ) { - errors.push( - new FieldMetadataException( + validationResult.errors.push({ + code: FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED, + message: 'Cannot delete, please update the label identifier field first', - FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED, - { - userFriendlyMessage: t`Cannot delete, please update the label identifier field first`, - }, - ), - ); + userFriendlyMessage: t`Cannot delete, please update the label identifier field first`, + }); } } @@ -229,27 +220,25 @@ export class FlatFieldMetadataValidatorService { isStandardMetadata(flatFieldMetadataToDelete) && !isRelationFieldAndRelationTargetObjectMetadataHasBeenDeleted ) { - errors.push( - new FieldMetadataException( - "Standard Fields can't be deleted", - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ), - ); + validationResult.errors.push({ + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: "Standard Fields can't be deleted", + userFriendlyMessage: t`Standard fields cannot be deleted`, + }); } if ( flatFieldMetadataToDelete.isActive && !isRelationFieldAndRelationTargetObjectMetadataHasBeenDeleted ) { - errors.push( - new FieldMetadataException( - "Active fields can't be deleted", - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ), - ); + validationResult.errors.push({ + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: "Active fields can't be deleted", + userFriendlyMessage: t`Active fields cannot be deleted`, + }); } - return errors; + return validationResult; } async validateFlatFieldMetadataCreation< @@ -259,10 +248,16 @@ export class FlatFieldMetadataValidatorService { flatFieldMetadataToValidate, otherFlatObjectMetadataMapsToValidate, workspaceId, - }: ValidateOneFieldMetadataArgs): Promise< - FailedFlatFieldMetadataValidationExceptions[] - > { - const errors: FailedFlatFieldMetadataValidationExceptions[] = []; + }: ValidateOneFieldMetadataArgs): Promise { + const validationResult: FailedFlatFieldMetadataValidation = { + errors: [], + fieldMinimalInformation: { + id: flatFieldMetadataToValidate.id, + name: flatFieldMetadataToValidate.name, + objectMetadataId: flatFieldMetadataToValidate.objectMetadataId, + }, + type: 'create_field', + }; const parentFlatObjectMetadata = otherFlatObjectMetadataMapsToValidate?.byId[ @@ -273,65 +268,59 @@ export class FlatFieldMetadataValidatorService { ]; if (!isDefined(parentFlatObjectMetadata)) { - errors.push( - new FieldMetadataException( - isDefined(otherFlatObjectMetadataMapsToValidate) - ? 'Object metadata not found in both existing and about to be created object metadatas' - : 'Object metadata not found', - FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ), - ); + validationResult.errors.push({ + code: FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, + message: isDefined(otherFlatObjectMetadataMapsToValidate) + ? 'Object metadata not found in both existing and about to be created object metadatas' + : 'Object metadata not found', + userFriendlyMessage: t`Field to create related object not found`, + }); } else { if ( isDefined( parentFlatObjectMetadata.fieldsById[flatFieldMetadataToValidate.id], ) ) { - errors.push( - new ObjectMetadataException( - 'Field with same id already exists in object', - ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED, - ), - ); + validationResult.errors.push({ + code: FieldMetadataExceptionCode.FIELD_ALREADY_EXISTS, + message: 'Field with same id already exists in object', + userFriendlyMessage: t`Field already exists`, + }); } if (parentFlatObjectMetadata.isRemote === true) { - errors.push( - new ObjectMetadataException( - 'Remote objects are read-only', - ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED, - ), - ); + validationResult.errors.push({ + code: FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED, + message: 'Remote objects are read-only', + userFriendlyMessage: t`Remote objects are not production ready yet`, + }); } - const failedNameAvailabilityValidation = - validateFlatFieldMetadataNameAvailability({ + validationResult.errors.push( + ...validateFlatFieldMetadataNameAvailability({ name: flatFieldMetadataToValidate.name, flatObjectMetadata: parentFlatObjectMetadata, - }); - - if (isDefined(failedNameAvailabilityValidation)) { - errors.push(failedNameAvailabilityValidation); - } + }), + ); } if ( flatFieldMetadataToValidate.isLabelSyncedWithName && !isFlatFieldMetadataNameSyncedWithLabel(flatFieldMetadataToValidate) ) { - errors.push( - new InvalidMetadataException( - t`Name is not synced with label.`, - InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL, - ), - ); + validationResult.errors.push({ + code: FieldMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL, + message: `Name is not synced with label`, + userFriendlyMessage: t`Field name is not synced with field label`, + value: flatFieldMetadataToValidate.label, + }); } - errors.push( + validationResult.errors.push( ...validateFlatFieldMetadataName(flatFieldMetadataToValidate.name), ); - errors.push( + validationResult.errors.push( ...(await this.flatFieldMetadataTypeValidatorService.validateFlatFieldMetadataTypeSpecificities( { existingFlatObjectMetadataMaps, @@ -342,6 +331,6 @@ export class FlatFieldMetadataValidatorService { )), ); - return errors; + return validationResult; } } 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 d04723a7ec..1a2fcbeb97 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,22 +1,9 @@ -import { isDefined } from 'twenty-shared/utils'; +import { type FieldMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-field-metadata/types/field-metadata-minimal-information.type'; +import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; +import { type WorkspaceMigrationFieldActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2'; -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 = - | FieldMetadataException - | ObjectMetadataException - | InvalidMetadataException; - -export const runFlatFieldMetadataValidators = ( - elementToValidate: T, - validators: FlatMetadataValidator[], -): FailedFlatFieldMetadataValidationExceptions[] => - validators - .map((validator) => - runFlatFieldMetadataValidator(elementToValidate, validator), - ) - .filter(isDefined); +export type FailedFlatFieldMetadataValidation = { + type: WorkspaceMigrationFieldActionTypeV2; + errors: FlatFieldMetadataValidationError[]; + fieldMinimalInformation: Partial; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type.ts index 1d42c575bd..083bed827d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type.ts @@ -1,8 +1,8 @@ -import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; export type FailedFieldInputTranspilation = { status: 'fail'; - error: FailedFlatFieldMetadataValidationExceptions; + error: FlatFieldMetadataValidationError; }; export type SuccessfulFieldInputTranspilation = { status: 'success'; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/field-metadata-minimal-information.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/field-metadata-minimal-information.type.ts new file mode 100644 index 0000000000..660b556e13 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/field-metadata-minimal-information.type.ts @@ -0,0 +1,6 @@ +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; + +export type FieldMetadataMinimalInformation = Pick< + FlatFieldMetadata, + 'id' | 'objectMetadataId' | 'name' +>; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type.ts index be226086b8..3a76742401 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type.ts @@ -1,12 +1,12 @@ import { type FieldMetadataType } from 'twenty-shared/types'; import { type ValidateOneFieldMetadataArgs } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service'; -import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; export type FlatFieldMetadataTypeValidator = { [P in FieldMetadataType]: ( args: ValidateOneFieldMetadataArgs

, ) => - | FailedFlatFieldMetadataValidationExceptions[] - | Promise; + | FlatFieldMetadataValidationError[] + | Promise; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type.ts new file mode 100644 index 0000000000..f090cc02bd --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type.ts @@ -0,0 +1,8 @@ +import { type FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; + +export type FlatFieldMetadataValidationError = { + code: FieldMetadataExceptionCode; + message: string; + userFriendlyMessage?: string; + value?: unknown; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util.ts index 46ceacfb8d..4d298198a3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util.ts @@ -1,3 +1,4 @@ +import { t } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; import { assertUnreachable, @@ -10,10 +11,7 @@ import { type FieldMetadataOptions } from 'src/engine/metadata-modules/field-met import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; -import { - FieldMetadataException, - FieldMetadataExceptionCode, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { generateRatingOptions } from 'src/engine/metadata-modules/field-metadata/utils/generate-rating-optionts.util'; import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; @@ -37,10 +35,10 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({ if (rawCreateFieldInput.isRemoteCreation) { return { status: 'fail', - error: new FieldMetadataException( - "Remote fields aren't supported", - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ), + error: { + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: "Remote fields aren't supported", + }, }; } const createFieldInput = @@ -54,14 +52,11 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({ if (!isDefined(parentFlatObjectMetadata)) { return { status: 'fail', - error: new FieldMetadataException( - 'Provided object metadata id does not exist', - FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - { - userFriendlyMessage: - 'Created field metadata, parent object metadata not found', - }, - ), + error: { + code: FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, + message: 'Provided object metadata id does not exist', + userFriendlyMessage: t`Created field metadata, parent object metadata not found`, + }, }; } @@ -74,7 +69,6 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({ switch (createFieldInput.type) { case FieldMetadataType.MORPH_RELATION: { - // TODO prastoin throw new UserInputError( 'Morph relation feature is not migrated to workspace migration v2 yet', ); @@ -128,10 +122,10 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({ case FieldMetadataType.TS_VECTOR: { return { status: 'fail', - error: new FieldMetadataException( - 'TS Vector is not supported for field creation', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ), + error: { + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: 'TS Vector is not supported for field creation', + }, }; } case FieldMetadataType.UUID: diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadata.util.ts index 288590a992..c3a8a6f9c1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadata.util.ts @@ -1,3 +1,4 @@ +import { t } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined, @@ -61,10 +62,12 @@ export const fromRelationCreateFieldInputToFlatFieldMetadata = async ({ if (!isDefined(rawCreationPayload)) { return { status: 'fail', - error: new FieldMetadataException( - `Relation creation payload is required`, - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ), + error: { + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: `Relation creation payload is required`, + userFriendlyMessage: t`Relation creation payload is required`, + value: rawCreationPayload, + }, }; } @@ -80,7 +83,12 @@ export const fromRelationCreateFieldInputToFlatFieldMetadata = async ({ if (error instanceof FieldMetadataException) { return { status: 'fail', - error, + error: { + code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, + message: `Relation creation payload is invalid ${JSON.stringify(relationCreationPayload)}`, + userFriendlyMessage: t`Invalid relation creation payload`, + value: relationCreationPayload, + }, }; } else { throw error; @@ -95,10 +103,12 @@ export const fromRelationCreateFieldInputToFlatFieldMetadata = async ({ if (!isDefined(targetParentFlatObjectMetadata)) { return { status: 'fail', - error: new FieldMetadataException( - `Object metadata relation target not found for relation creation payload`, - FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, - ), + error: { + code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, + message: `Object metadata relation target not found for relation creation payload`, + userFriendlyMessage: t`Object targeted by field to create not found`, + value: relationCreationPayload, + }, }; } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts index 6e0b375a47..f2c50355a5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts @@ -1,15 +1,12 @@ +import { t } from '@lingui/core/macro'; import { extractAndSanitizeObjectStringFields, isDefined, - trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, } from 'twenty-shared/utils'; import { FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/field-metadata/constants/field-metadata-standard-overrides-properties.constant'; import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; -import { - FieldMetadataException, - FieldMetadataExceptionCode, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { type FieldMetadataStandardOverridesProperties } from 'src/engine/metadata-modules/field-metadata/types/field-metadata-standard-overrides-properties.type'; import { FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant'; import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type'; @@ -18,10 +15,6 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m import {} from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; import { findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-with-field-id-only.util'; -import { - ObjectMetadataException, - ObjectMetadataExceptionCode, -} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util'; const fieldMetadataEditableProperties = @@ -43,10 +36,10 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ updateFieldInput: rawUpdateFieldInput, }: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult => { const updateFieldInputInformalProperties = - trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( - rawUpdateFieldInput, - ['objectMetadataId', 'id'], - ); + extractAndSanitizeObjectStringFields(rawUpdateFieldInput, [ + 'objectMetadataId', + 'id', + ]); const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields( rawUpdateFieldInput, fieldMetadataEditableProperties, @@ -61,10 +54,11 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ if (!isDefined(relatedFlatFieldMetadata)) { return { status: 'fail', - error: new FieldMetadataException( - 'Field metadata to update not found', - FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, - ), + error: { + code: FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, + message: 'Field metadata to update not found', + userFriendlyMessage: t`Field metadata to update not found`, + }, }; } @@ -76,20 +70,21 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ if (!isDefined(flatObjectMetadataWithFlatFieldMaps)) { return { status: 'fail', - error: new FieldMetadataException( - 'Field metadata to update object metadata not found', - FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, - ), + error: { + code: FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, + message: 'Field metadata to update object metadata not found', + userFriendlyMessage: t`Field metadata to update object metadata not found`, + }, }; } if (flatObjectMetadataWithFlatFieldMaps.isRemote) { return { status: 'fail', - error: new ObjectMetadataException( - 'Remote objects are read-only', - ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED, - ), + error: { + code: FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED, + message: 'Remote objects are read-only', + }, }; } @@ -105,10 +100,11 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ if (invalidUpdatedProperties.length > 0) { return { status: 'fail', - error: new FieldMetadataException( - `Cannot edit standard field metadata properties: ${invalidUpdatedProperties.join(', ')}`, - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ), + error: { + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: `Cannot update standard field metadata properties: ${invalidUpdatedProperties.join(', ')}`, + userFriendlyMessage: t`Cannot update standard field properties: ${invalidUpdatedProperties.join(', ')}`, + }, }; } 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 deleted file mode 100644 index 6e80fd131c..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/run-flat-field-metadata-validator.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -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 index 25bfa2dd18..ef27084572 100644 --- 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 @@ -1,15 +1,25 @@ -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 { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; 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); +export const runFlatFieldMetadataValidators = ({ + elementToValidate, + validators, +}: { + elementToValidate: T; + validators: FlatMetadataValidator[]; +}): FlatFieldMetadataValidationError[] => { + return validators.flatMap(({ validator, message }) => { + const isInvalid = validator(elementToValidate); + + if (isInvalid) { + return { + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message, + value: elementToValidate, + }; + } + + return []; + }); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/throw-on-field-input-transpilations-error.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/throw-on-field-input-transpilations-error.util.ts index 5b5bde8b3f..c1f2193e29 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/throw-on-field-input-transpilations-error.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/throw-on-field-input-transpilations-error.util.ts @@ -1,9 +1,10 @@ -import { MultipleMetadataValidationErrors } from 'src/engine/core-modules/error/multiple-metadata-validation-errors'; import { type FieldInputTranspilationResult, type SuccessfulFieldInputTranspilation, } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type'; +import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; +// This could be improved by still running the build and validate with available valid inputs type ThrowOnFieldInputTranspilationsErrorArgs = ( inputTranspilationResults: FieldInputTranspilationResult[], errorLabel: string, @@ -19,8 +20,18 @@ export const throwOnFieldInputTranspilationsError: ThrowOnFieldInputTranspilatio ); if (failedInputTranspilationErrors.length > 0) { - throw new MultipleMetadataValidationErrors( - failedInputTranspilationErrors, + // We should create a dedicated exceptions instead of hacking through the WorkspaceMigrationBuilderExceptionV2 + throw new WorkspaceMigrationBuilderExceptionV2( + { + errors: [ + { + errors: failedInputTranspilationErrors, + type: 'create_field', + fieldMinimalInformation: {}, + }, + ], + status: 'fail', + }, errorLabel, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util.ts index ca6fb0932c..c03bb01f46 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util.ts @@ -15,16 +15,11 @@ import { type FieldMetadataComplexOption, type FieldMetadataDefaultOption, } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -import { - FieldMetadataException, - FieldMetadataExceptionCode, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { FieldMetadataExceptionCode } 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 { - runFlatFieldMetadataValidators, - type FailedFlatFieldMetadataValidationExceptions, -} from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { runFlatFieldMetadataValidators } from 'src/engine/metadata-modules/flat-field-metadata/utils/run-flat-field-metadata-validators.util'; import { type FlatMetadataValidator } from 'src/engine/metadata-modules/types/flat-metadata-validator.type'; import { beneathDatabaseIdentifierMinimumLength, @@ -44,7 +39,10 @@ const validateMetadataOptionId = (sanitizedId?: string) => { }, ]; - return runFlatFieldMetadataValidators(sanitizedId, validators); + return runFlatFieldMetadataValidators({ + elementToValidate: sanitizedId, + validators, + }); }; const validateMetadataOptionLabel = (sanitizedLabel: string) => { @@ -71,7 +69,10 @@ const validateMetadataOptionLabel = (sanitizedLabel: string) => { }, ]; - return runFlatFieldMetadataValidators(sanitizedLabel, validators); + return runFlatFieldMetadataValidators({ + elementToValidate: sanitizedLabel, + validators, + }); }; const validateMetadataOptionValue = (sanitizedValue: string) => { @@ -94,7 +95,10 @@ const validateMetadataOptionValue = (sanitizedValue: string) => { }, ]; - return runFlatFieldMetadataValidators(sanitizedValue, validators); + return runFlatFieldMetadataValidators({ + elementToValidate: sanitizedValue, + validators, + }); }; const validateDuplicates = ( @@ -118,20 +122,25 @@ const validateDuplicates = ( new Set(options.map((option) => option[field])).size !== options.length, })); - return runFlatFieldMetadataValidators(options, duplicatedValidators); + return runFlatFieldMetadataValidators({ + elementToValidate: options, + validators: duplicatedValidators, + }); }; const validateFieldMetadataInputOptions = ( flatFieldMetadata: FlatFieldMetadata, -) => { +): FlatFieldMetadataValidationError[] => { const { options } = flatFieldMetadata; if (!isDefined(options) || options.length === 0) { return [ - new FieldMetadataException( - 'Options are required for enum fields', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ), + { + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: 'Options are required for enum fields', + userFriendlyMessage: t`Options are required for enum fields`, + value: options, + }, ]; } @@ -148,16 +157,21 @@ const validateFieldMetadataInputOptions = ( return [...optionsValidationErrors, ...duplicatedValidationErrors]; }; -const validateSelectDefaultValue = ( - options: FieldMetadataOptions, - defaultValue: string | string[], -): FailedFlatFieldMetadataValidationExceptions[] => { +const validateSelectDefaultValue = ({ + defaultValue, + options, +}: { + options: FieldMetadataOptions; + defaultValue: string | string[]; +}): FlatFieldMetadataValidationError[] => { if (typeof defaultValue !== 'string') { return [ - new FieldMetadataException( - 'Default value for select must be a string', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ), + { + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: `Default value for select must be a string got ${defaultValue}`, + userFriendlyMessage: t`Default value must be a string`, + value: defaultValue, + }, ]; } @@ -175,19 +189,27 @@ const validateSelectDefaultValue = ( }, ]; - return runFlatFieldMetadataValidators(defaultValue, validators); + return runFlatFieldMetadataValidators({ + elementToValidate: defaultValue, + validators, + }); }; -const validateMultiSelectDefaultValue = ( - options: FieldMetadataOptions, - multiSelectDefaultValue: unknown, -): FailedFlatFieldMetadataValidationExceptions[] => { +const validateMultiSelectDefaultValue = ({ + multiSelectDefaultValue, + options, +}: { + options: FieldMetadataOptions; + multiSelectDefaultValue: unknown; +}): FlatFieldMetadataValidationError[] => { if (!Array.isArray(multiSelectDefaultValue)) { return [ - new FieldMetadataException( - 'Default value for multi-select must be an array', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ), + { + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + userFriendlyMessage: t`Multi-select field default value must be an array`, + message: `Default value for multi-select must be an array got ${multiSelectDefaultValue}`, + value: multiSelectDefaultValue, + }, ]; } @@ -203,37 +225,42 @@ const validateMultiSelectDefaultValue = ( ]; return [ - runFlatFieldMetadataValidators(multiSelectDefaultValue, validators), - multiSelectDefaultValue.flatMap((value) => - validateSelectDefaultValue(options, value), + runFlatFieldMetadataValidators({ + elementToValidate: multiSelectDefaultValue, + validators, + }), + multiSelectDefaultValue.flatMap((defaultValue) => + validateSelectDefaultValue({ + defaultValue, + options, + }), ), ].flat(); }; -const validateFieldMetadataDefaultValue = ( - flatFieldMetadata: Omit< - FlatFieldMetadata, - 'defaultValue' - > & - NonNullableRequired< - Pick, 'defaultValue'> - >, -) => { - switch (flatFieldMetadata.type) { +const validateFieldMetadataDefaultValue = ({ + defaultValue, + options, + type, +}: Omit, 'defaultValue'> & + NonNullableRequired< + Pick, 'defaultValue'> + >) => { + switch (type) { case FieldMetadataType.SELECT: case FieldMetadataType.RATING: - return validateSelectDefaultValue( - flatFieldMetadata.options, - flatFieldMetadata.defaultValue, - ); + return validateSelectDefaultValue({ + defaultValue, + options, + }); case FieldMetadataType.MULTI_SELECT: - return validateMultiSelectDefaultValue( - flatFieldMetadata.options, - flatFieldMetadata.defaultValue, - ); + return validateMultiSelectDefaultValue({ + multiSelectDefaultValue: defaultValue, + options, + }); default: { assertUnreachable( - flatFieldMetadata.type, + type, 'Should never occur, unknown field metadata enum type', ); } @@ -242,7 +269,7 @@ const validateFieldMetadataDefaultValue = ( export const validateEnumSelectFlatFieldMetadata = ({ flatFieldMetadataToValidate, -}: ValidateOneFieldMetadataArgs): FailedFlatFieldMetadataValidationExceptions[] => { +}: ValidateOneFieldMetadataArgs): FlatFieldMetadataValidationError[] => { const optionsValidationErrors = validateFieldMetadataInputOptions( flatFieldMetadataToValidate, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util.ts index ea1446e755..86ae2763f3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util.ts @@ -1,16 +1,13 @@ import { t } from '@lingui/core/macro'; -import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { - InvalidMetadataException, - InvalidMetadataExceptionCode, -} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; +import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; const getReservedCompositeFieldNames = ( flatObjectMetadata: FlatObjectMetadata, @@ -41,7 +38,8 @@ export const validateFlatFieldMetadataNameAvailability = ({ }: { name: string; flatObjectMetadata: FlatObjectMetadata; -}): FailedFlatFieldMetadataValidationExceptions | undefined => { +}): FlatFieldMetadataValidationError[] => { + const errors: FlatFieldMetadataValidationError[] = []; const reservedCompositeFieldsNames = getReservedCompositeFieldNames(flatObjectMetadata); @@ -49,28 +47,25 @@ export const validateFlatFieldMetadataNameAvailability = ({ flatObjectMetadata.flatFieldMetadatas.some( (field) => field.name === name || - (field.type === FieldMetadataType.RELATION && // Question: Should we also look for MORPH_RELATION field types ? - `${field.name}Id` === name), + (isRelationFieldMetadataType(field.type) && `${field.name}Id` === name), ) ) { - return new InvalidMetadataException( - `Name "${name}" is not available as it is already used by another field`, - InvalidMetadataExceptionCode.NOT_AVAILABLE, - { - userFriendlyMessage: t`This name is not available as it is already used by another field`, - }, - ); + errors.push({ + code: FieldMetadataExceptionCode.NOT_AVAILABLE, + value: name, + message: `Name "${name}" is not available as it is already used by another field`, + userFriendlyMessage: t`Name "${name}" is not available as it is already used by another field`, + }); } if (reservedCompositeFieldsNames.includes(name)) { - return new InvalidMetadataException( - `Name "${name}" is not available`, - InvalidMetadataExceptionCode.RESERVED_KEYWORD, - { - userFriendlyMessage: t`This name is not available.`, - }, - ); + errors.push({ + code: FieldMetadataExceptionCode.RESERVED_KEYWORD, + message: `Name "${name}" is reserved composite field name`, + value: name, + userFriendlyMessage: t`Name "${name}" is not available`, + }); } - return undefined; + return errors; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name.util.ts index d8b8fc9b71..ebc576acb9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name.util.ts @@ -1,8 +1,21 @@ -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 { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; 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); +): FlatFieldMetadataValidationError[] => + METADATA_NAME_VALIDATORS.flatMap(({ validator, message }) => { + const isInvalid = validator(name); + + if (isInvalid) { + return { + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message, + userFriendlyMessage: message, + value: name, + }; + } + + return []; + }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-flat-field-metadata.util.ts index b9540fc50a..ea55c1ffcc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-flat-field-metadata.util.ts @@ -1,40 +1,40 @@ +import { t } from '@lingui/core/macro'; import { type FieldMetadataType } from 'twenty-shared/types'; import { isDefined, isValidUuid } from 'twenty-shared/utils'; -import { - FieldMetadataException, - FieldMetadataExceptionCode, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { FieldMetadataExceptionCode } 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 } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; export const validateRelationFlatFieldMetadata = async ({ existingFlatObjectMetadataMaps, - flatFieldMetadataToValidate: { - relationTargetFieldMetadataId, - relationTargetObjectMetadataId, - }, + flatFieldMetadataToValidate, otherFlatObjectMetadataMapsToValidate, }: ValidateOneFieldMetadataArgs): Promise< - FailedFlatFieldMetadataValidationExceptions[] + FlatFieldMetadataValidationError[] > => { + const { relationTargetFieldMetadataId, relationTargetObjectMetadataId } = + flatFieldMetadataToValidate; + const uuidsValidation = [ relationTargetObjectMetadataId, relationTargetFieldMetadataId, - ].flatMap((id) => + ].flatMap((id) => isValidUuid(id) ? [] - : new FieldMetadataException( - `Invalid uuid ${id}`, - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ), + : { + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: `Invalid uuid ${id}`, + userFriendlyMessage: t`Invalid uuid ${id}`, + value: id, + }, ); if (uuidsValidation.length > 0) { return uuidsValidation; } - const errors: FailedFlatFieldMetadataValidationExceptions[] = []; + const errors: FlatFieldMetadataValidationError[] = []; const targetRelationFlatObjectMetadata = otherFlatObjectMetadataMapsToValidate?.byId[ @@ -42,14 +42,14 @@ export const validateRelationFlatFieldMetadata = async ({ ] ?? existingFlatObjectMetadataMaps.byId[relationTargetObjectMetadataId]; if (!isDefined(targetRelationFlatObjectMetadata)) { - errors.push( - new FieldMetadataException( - isDefined(otherFlatObjectMetadataMapsToValidate) - ? 'Relation target object metadata not found in both existing and about to be created object metadatas' - : 'Relation target object metadata not found', - FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ), - ); + errors.push({ + code: FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, + message: isDefined(otherFlatObjectMetadataMapsToValidate) + ? 'Relation target object metadata not found in both existing and about to be created object metadatas' + : 'Relation target object metadata not found', + userFriendlyMessage: t`Object targeted by the relation not found`, + value: relationTargetObjectMetadataId, + }); } const targetRelationFlatFieldMetadata = @@ -59,14 +59,14 @@ export const validateRelationFlatFieldMetadata = async ({ isDefined(targetRelationFlatObjectMetadata) && !isDefined(targetRelationFlatFieldMetadata) ) { - errors.push( - new FieldMetadataException( - isDefined(otherFlatObjectMetadataMapsToValidate) - ? 'Relation field target metadata not found in both existing and about to be created field metadatas' - : 'Relation field target metadata not found', - FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, - ), - ); + errors.push({ + code: FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, + message: isDefined(otherFlatObjectMetadataMapsToValidate) + ? 'Relation field target metadata not found in both existing and about to be created field metadatas' + : 'Relation field target metadata not found', + userFriendlyMessage: t`Relation field target metadata not found`, + value: relationTargetFieldMetadataId, + }); } return errors; 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 index 8b0aca7217..84b3ea70ff 100644 --- 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 @@ -4,23 +4,22 @@ import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; 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 { FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; import { isRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-relation-flat-field-metadata.util'; 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 { findFlatObjectMetadataInFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps.util'; -import { FailedFlatObjectMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; +import { FailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; +import { FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type'; import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { areFlatObjectMetadataNamesSyncedWithLabels } from 'src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util'; import { computeRelationTargetFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/compute-relation-target-flat-object-metadata-maps.util'; +import { getDefaultFailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-default-failed-flat-object-metadata-validation.type'; import { validateFlatObjectMetadataIdentifiers } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util'; import { validateFlatObjectMetadataLabel } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util'; import { validateFlatObjectMetadataNames } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util'; -import { - ObjectMetadataException, - ObjectMetadataExceptionCode, -} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util'; import { doesOtherObjectWithSameNameExists } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util'; import { WorkspaceMigrationV2BuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service'; @@ -37,32 +36,44 @@ export class FlatObjectMetadataValidatorService { }: { existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; updatedFlatObjectMetadata: FlatObjectMetadata; - }) { + }): FailedFlatObjectMetadataValidation { + const validationResult = getDefaultFailedFlatObjectMetadataValidation({ + type: 'update_object', + objectMinimalInformation: { + id: updatedFlatObjectMetadata.id, + }, + }); + const existingFlatObjectMetadata = existingFlatObjectMetadataMaps.byId[updatedFlatObjectMetadata.id]; if (!isDefined(existingFlatObjectMetadata)) { - return [ - new ObjectMetadataException( - t`Object to update not found`, - ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ), - ]; - } - const errors: FailedFlatObjectMetadataValidationExceptions[] = []; + validationResult.objectLevelErrors.push({ + code: ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, + message: t`Object to update not found`, + userFriendlyMessage: t`Object to update not found`, + }); - errors.push( + return validationResult; + } + validationResult.objectMinimalInformation = { + id: existingFlatObjectMetadata.id, + namePlural: existingFlatObjectMetadata.namePlural, + nameSingular: existingFlatObjectMetadata.nameSingular, + }; + + validationResult.objectLevelErrors.push( ...this.validateFlatObjectMetadataNameAndLabels({ existingFlatObjectMetadataMaps, flatObjectMetadataToValidate: updatedFlatObjectMetadata, }), ); - errors.push( + validationResult.objectLevelErrors.push( ...validateFlatObjectMetadataIdentifiers(existingFlatObjectMetadata), ); - return errors; + return validationResult; } public validateFlatObjectMetadataDeletion({ @@ -73,52 +84,60 @@ export class FlatObjectMetadataValidatorService { existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; objectMetadataToDeleteId: string; buildOptions: WorkspaceMigrationV2BuilderOptions; - }) { - const errors: FailedFlatObjectMetadataValidationExceptions[] = []; + }): FailedFlatObjectMetadataValidation { + const validationResult = getDefaultFailedFlatObjectMetadataValidation({ + type: 'delete_object', + objectMinimalInformation: { + id: objectMetadataToDeleteId, + }, + }); const flatObjectMetadataToDelete = existingFlatObjectMetadataMaps.byId[objectMetadataToDeleteId]; if (!isDefined(flatObjectMetadataToDelete)) { - errors.push( - new ObjectMetadataException( - t`Object to delete not found`, - ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ), - ); + validationResult.objectLevelErrors.push({ + code: ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, + message: t`Object to delete not found`, + userFriendlyMessage: t`Object to delete not found`, + value: objectMetadataToDeleteId, + }); } else { + validationResult.objectMinimalInformation = { + id: flatObjectMetadataToDelete.id, + namePlural: flatObjectMetadataToDelete.namePlural, + nameSingular: flatObjectMetadataToDelete.nameSingular, + }; + if (flatObjectMetadataToDelete.isRemote) { - errors.push( - new ObjectMetadataException( - t`Remote objects are not supported yet`, - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ), - ); + validationResult.objectLevelErrors.push({ + code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + message: t`Remote objects are not supported yet`, + userFriendlyMessage: t`Remote objects are not supported yet`, + }); } if ( !buildOptions.isSystemBuild && isStandardMetadata(flatObjectMetadataToDelete) ) { - errors.push( - new ObjectMetadataException( - t`Standard objects cannot be deleted`, - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ), - ); + validationResult.objectLevelErrors.push({ + code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + message: t`Standard objects cannot be deleted`, + userFriendlyMessage: t`Standard objects cannot be deleted`, + }); } if (!buildOptions.isSystemBuild && flatObjectMetadataToDelete.isActive) { - errors.push( - new ObjectMetadataException( - t`Active objects cannot be deleted`, - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ), - ); + validationResult.objectLevelErrors.push({ + code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + message: t`Active objects cannot be deleted`, + userFriendlyMessage: t`Active objects cannot be deleted`, + }); } } - return errors; + return validationResult; } public async validateFlatObjectMetadataCreation({ @@ -129,8 +148,15 @@ export class FlatObjectMetadataValidatorService { existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; flatObjectMetadataToValidate: FlatObjectMetadata; otherFlatObjectMetadataMapsToValidate?: FlatObjectMetadataMaps; - }) { - const errors: FailedFlatObjectMetadataValidationExceptions[] = []; + }): Promise { + const validationResult = getDefaultFailedFlatObjectMetadataValidation({ + type: 'create_object', + objectMinimalInformation: { + id: flatObjectMetadataToValidate.id, + namePlural: flatObjectMetadataToValidate.namePlural, + nameSingular: flatObjectMetadataToValidate.nameSingular, + }, + }); if ( isDefined( @@ -140,31 +166,29 @@ export class FlatObjectMetadataValidatorService { }), ) ) { - errors.push( - new ObjectMetadataException( - t`Object with same id already exists`, - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ), - ); + validationResult.objectLevelErrors.push({ + code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + message: t`Object with same id already exists`, + userFriendlyMessage: t`Object with same id already exists`, + }); } if (flatObjectMetadataToValidate.isRemote) { - errors.push( - new ObjectMetadataException( - t`Remote objects are not supported yet`, - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ), - ); + validationResult.objectLevelErrors.push({ + code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + message: t`Remote objects are not supported yet`, + userFriendlyMessage: t`Remote objects are not supported yet`, + }); } - errors.push( + validationResult.objectLevelErrors.push( ...this.validateFlatObjectMetadataNameAndLabels({ existingFlatObjectMetadataMaps, flatObjectMetadataToValidate, }), ); - const allFlatFieldMetadatasValidationErrors: FailedFlatFieldMetadataValidationExceptions[] = + const allFlatFieldMetadatasValidationErrors: FailedFlatFieldMetadataValidation[] = []; let optimisticFlatObjectMetadataMaps = addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow({ @@ -195,8 +219,8 @@ export class FlatObjectMetadataValidatorService { }, ); - if (flatFieldValidatorErrors.length > 0) { - allFlatFieldMetadatasValidationErrors.push(...flatFieldValidatorErrors); + if (flatFieldValidatorErrors.errors.length > 0) { + allFlatFieldMetadatasValidationErrors.push(flatFieldValidatorErrors); continue; } @@ -208,10 +232,12 @@ export class FlatObjectMetadataValidatorService { } if (allFlatFieldMetadatasValidationErrors.length > 0) { - errors.push(...allFlatFieldMetadatasValidationErrors); + validationResult.fieldLevelErrors.push( + ...allFlatFieldMetadatasValidationErrors, + ); } - return errors; + return validationResult; } private validateFlatObjectMetadataNameAndLabels({ @@ -220,8 +246,8 @@ export class FlatObjectMetadataValidatorService { }: { flatObjectMetadataToValidate: FlatObjectMetadata; existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; - }) { - const errors: FailedFlatObjectMetadataValidationExceptions[] = []; + }): FlatObjectMetadataValidationError[] { + const errors: FlatObjectMetadataValidationError[] = []; errors.push( ...validateFlatObjectMetadataNames({ @@ -241,12 +267,11 @@ export class FlatObjectMetadataValidatorService { flatObjectMetadataToValidate.isLabelSyncedWithName && !areFlatObjectMetadataNamesSyncedWithLabels(flatObjectMetadataToValidate) ) { - errors.push( - new ObjectMetadataException( - t`Names are not synced with labels`, - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ), - ); + errors.push({ + code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + message: t`Names are not synced with labels`, + userFriendlyMessage: t`Names are not synced with labels`, + }); } if ( @@ -257,15 +282,11 @@ export class FlatObjectMetadataValidatorService { existingObjectMetadataId: flatObjectMetadataToValidate.id, }) ) { - errors.push( - new ObjectMetadataException( - 'Object already exists', - ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS, - { - userFriendlyMessage: t`Object already exists`, - }, - ), - ); + errors.push({ + code: ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS, + message: 'Object already exists', + userFriendlyMessage: t`Object already exists`, + }); } 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 index bb38c91049..7933c32138 100644 --- 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 @@ -1,8 +1,11 @@ -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'; +import { type FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type'; +import { type ObjectMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-object-metadata/types/object-metadata-minimal-information.type'; +import { type WorkspaceMigrationObjectActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2'; -export type FailedFlatObjectMetadataValidationExceptions = - | FieldMetadataException - | ObjectMetadataException - | InvalidMetadataException; +export type FailedFlatObjectMetadataValidation = { + type: WorkspaceMigrationObjectActionTypeV2; + objectLevelErrors: FlatObjectMetadataValidationError[]; + fieldLevelErrors: FailedFlatFieldMetadataValidation[]; + objectMinimalInformation: Partial; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type.ts new file mode 100644 index 0000000000..bdb876d2b9 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type.ts @@ -0,0 +1,8 @@ +import { type ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; + +export type FlatObjectMetadataValidationError = { + code: ObjectMetadataExceptionCode; + message: string; + userFriendlyMessage?: string; + value?: unknown; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/object-metadata-minimal-information.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/object-metadata-minimal-information.type.ts new file mode 100644 index 0000000000..f186e64377 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/object-metadata-minimal-information.type.ts @@ -0,0 +1,5 @@ +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +export type ObjectMetadataMinimalInformation = Partial< + Pick +>; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/get-default-failed-flat-object-metadata-validation.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/get-default-failed-flat-object-metadata-validation.type.ts new file mode 100644 index 0000000000..07c1ecf8de --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/get-default-failed-flat-object-metadata-validation.type.ts @@ -0,0 +1,11 @@ +import { type FailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; + +export const getDefaultFailedFlatObjectMetadataValidation = ( + overrides: Partial> & + Pick, +): FailedFlatObjectMetadataValidation => ({ + fieldLevelErrors: [], + objectLevelErrors: [], + objectMinimalInformation: {}, + ...overrides, +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/is-failed-flat-object-metadata-validation.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/is-failed-flat-object-metadata-validation.util.ts new file mode 100644 index 0000000000..4df5cecedd --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/is-failed-flat-object-metadata-validation.util.ts @@ -0,0 +1,16 @@ +import { type FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { type FailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; +import { + WORKSPACE_MIGRATION_OBJECT_ACTION_TYPES, + type WorkspaceMigrationObjectActionTypeV2, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2'; + +export const isFailedFlatObjectMetadataValidation = ( + failedValidation: + | FailedFlatObjectMetadataValidation + | FailedFlatFieldMetadataValidation, +): failedValidation is FailedFlatObjectMetadataValidation => { + return WORKSPACE_MIGRATION_OBJECT_ACTION_TYPES.includes( + failedValidation.type as WorkspaceMigrationObjectActionTypeV2, + ); +}; 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 deleted file mode 100644 index e33b6f6c23..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/run-flat-object-metadata-validator.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -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 index 791dd13ae4..6b3d4fa378 100644 --- 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 @@ -1,15 +1,25 @@ -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 FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type'; +import { 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 runFlatObjectMetadataValidators = ( - elementToValidate: T, - validators: FlatMetadataValidator[], -): FailedFlatObjectMetadataValidationExceptions[] => - validators - .map((validator) => - runFlatObjectMetadataValidator(elementToValidate, validator), - ) - .filter(isDefined); +export const runFlatObjectMetadataValidators = ({ + elementToValidate, + validators, +}: { + elementToValidate: T; + validators: FlatMetadataValidator[]; +}): FlatObjectMetadataValidationError[] => { + return validators.flatMap(({ validator, message }) => { + const isInvalid = validator(elementToValidate); + + if (isInvalid) { + return { + code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + message, + value: elementToValidate, + }; + } + + return []; + }); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util.ts index a507da091c..5f82e1627a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util.ts @@ -4,16 +4,14 @@ import { isLabelIdentifierFieldMetadataTypes, } from 'twenty-shared/utils'; +import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { - ObjectMetadataException, - ObjectMetadataExceptionCode, -} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; export const validateFlatObjectMetadataIdentifiers = ( flatObjectMetadata: FlatObjectMetadata, ) => { - const errors: ObjectMetadataException[] = []; + const errors: FlatObjectMetadataValidationError[] = []; const { labelIdentifierFieldMetadataId, imageIdentifierFieldMetadataId } = flatObjectMetadata; @@ -25,25 +23,19 @@ export const validateFlatObjectMetadataIdentifiers = ( ); if (!isDefined(flatFieldMetadata)) { - errors.push( - new ObjectMetadataException( + errors.push({ + code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + message: 'labelIdentifierFieldMetadataId validation failed: related field metadata not found', - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - { - userFriendlyMessage: t`Field declared as label identifier not found`, - }, - ), - ); + userFriendlyMessage: t`Field declared as label identifier not found`, + }); } else if (!isLabelIdentifierFieldMetadataTypes(flatFieldMetadata.type)) { - errors.push( - new ObjectMetadataException( + errors.push({ + code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + message: 'labelIdentifierFieldMetadataId validation failed: field type not compatible', - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - { - userFriendlyMessage: t`Field cannot be used as label identifier`, - }, - ), - ); + userFriendlyMessage: t`Field cannot be used as label identifier`, + }); } } @@ -54,15 +46,12 @@ export const validateFlatObjectMetadataIdentifiers = ( ); if (!isDefined(relatedFlatFieldMetadata)) { - errors.push( - new ObjectMetadataException( + errors.push({ + code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + message: 'imageIdentifierFieldMetadataId validation failed: related field metadata not found', - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - { - userFriendlyMessage: t`Field declared as image identifier not found`, - }, - ), - ); + userFriendlyMessage: t`Field declared as image identifier not found`, + }); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util.ts index 94ef0d3f38..2027d25f8f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util.ts @@ -1,12 +1,10 @@ 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 FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { type ObjectMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-object-metadata/types/object-metadata-minimal-information.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 { 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, @@ -16,11 +14,9 @@ import { export const validateFlatObjectMetadataLabel = ({ labelPlural, labelSingular, -}: Pick< - FlatObjectMetadata, - 'labelPlural' | 'labelSingular' ->): FailedFlatObjectMetadataValidationExceptions[] => { - const errors: FailedFlatObjectMetadataValidationExceptions[] = []; +}: Pick & + ObjectMetadataMinimalInformation): FlatObjectMetadataValidationError[] => { + const errors: FlatObjectMetadataValidationError[] = []; const validators: FlatMetadataValidator[] = [ { validator: (label) => beneathDatabaseIdentifierMinimumLength(label), @@ -34,7 +30,10 @@ export const validateFlatObjectMetadataLabel = ({ errors.push( ...[labelSingular, labelPlural].flatMap((label) => - runFlatObjectMetadataValidators(label, validators), + runFlatObjectMetadataValidators({ + elementToValidate: label, + validators, + }), ), ); @@ -42,12 +41,12 @@ export const validateFlatObjectMetadataLabel = ({ 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, - ), - ); + errors.push({ + code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + userFriendlyMessage: `The singular and plural labels cannot be the same for an object`, + message: t`The singular and plural labels cannot be the same for an object`, + value: labelSingular, + }); } return errors; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util.ts index b7d7eaae96..91c08dbf96 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util.ts @@ -1,23 +1,25 @@ 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 FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { type ObjectMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-object-metadata/types/object-metadata-minimal-information.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 { 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[] = []; +}: Pick & + ObjectMetadataMinimalInformation) => { + const errors: FlatObjectMetadataValidationError[] = []; errors.push( ...[nameSingular, namePlural].flatMap((name) => - runFlatObjectMetadataValidators(name, METADATA_NAME_VALIDATORS), + runFlatObjectMetadataValidators({ + elementToValidate: name, + validators: METADATA_NAME_VALIDATORS, + }), ), ); @@ -25,12 +27,12 @@ export const validateFlatObjectMetadataNames = ({ 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, - ), - ); + errors.push({ + code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + message: `The singular and plural names cannot be the same for an object`, + userFriendlyMessage: t`The singular and plural names cannot be the same for an object`, + value: namePlural, + }); } 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 c4aaa5ec8a..85cc5d7f23 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 @@ -23,6 +23,7 @@ import { 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 { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service'; @Injectable() @@ -62,19 +63,25 @@ export class ObjectMetadataServiceV2 { flatObjectMetadataMaps: fromFlatObjectMetadataMaps, }); - await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( - { - fromFlatObjectMetadataMaps, - toFlatObjectMetadataMaps, - buildOptions: { - isSystemBuild: false, - inferDeletionFromMissingObjectFieldIndex: false, + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + fromFlatObjectMetadataMaps, + toFlatObjectMetadataMaps, + buildOptions: { + isSystemBuild: false, + inferDeletionFromMissingObjectFieldIndex: false, + }, + workspaceId, }, - workspaceId, - errorMessage: - 'Multiple validation errors occurred while updating object', - }, - ); + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderExceptionV2( + validateAndBuildResult, + 'Multiple validation errors occurred while updating object', + ); + } const { flatObjectMetadataMaps: recomputedFlatObjectMetadataMaps } = await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps( @@ -151,19 +158,25 @@ export class ObjectMetadataServiceV2 { }), ); - await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( - { - fromFlatObjectMetadataMaps, - toFlatObjectMetadataMaps, - buildOptions: { - inferDeletionFromMissingObjectFieldIndex: true, - isSystemBuild: false, + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + fromFlatObjectMetadataMaps, + toFlatObjectMetadataMaps, + buildOptions: { + inferDeletionFromMissingObjectFieldIndex: true, + isSystemBuild: false, + }, + workspaceId, }, - workspaceId, - errorMessage: - 'Multiple validation errors occurred while deleting object', - }, - ); + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderExceptionV2( + validateAndBuildResult, + 'Multiple validation errors occurred while deleting object', + ); + } return fromFlatObjectMetadataToObjectMetadataDto( flatObjectMetadataToDelete, @@ -234,19 +247,25 @@ export class ObjectMetadataServiceV2 { ], }); - await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( - { - fromFlatObjectMetadataMaps, - toFlatObjectMetadataMaps, - buildOptions: { - isSystemBuild: false, - inferDeletionFromMissingObjectFieldIndex: false, + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + fromFlatObjectMetadataMaps, + toFlatObjectMetadataMaps, + buildOptions: { + isSystemBuild: false, + inferDeletionFromMissingObjectFieldIndex: false, + }, + workspaceId, }, - workspaceId, - errorMessage: - 'Multiple validation errors occurred while creating object', - }, - ); + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderExceptionV2( + validateAndBuildResult, + 'Multiple validation errors occurred while creating object', + ); + } const { flatObjectMetadataMaps: recomputedFlatObjectMetadataMaps } = await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps( 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 9adfe23415..96aeec54aa 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 @@ -43,6 +43,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; +import { WorkspaceMigrationBuilderExceptionV2Interceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2.interceptor'; import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module'; @Module({ @@ -98,7 +99,10 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa update: { disabled: true }, delete: { disabled: true }, guards: [WorkspaceAuthGuard], - interceptors: [ObjectMetadataGraphqlApiExceptionInterceptor], + interceptors: [ + WorkspaceMigrationBuilderExceptionV2Interceptor, + ObjectMetadataGraphqlApiExceptionInterceptor, + ], filters: [PermissionsGraphqlApiExceptionFilter], }, ], diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2.ts new file mode 100644 index 0000000000..5cb08faf3f --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2.ts @@ -0,0 +1,11 @@ +import { type FailedWorkspaceMigrationBuildResult } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service'; + +export class WorkspaceMigrationBuilderExceptionV2 extends Error { + constructor( + public readonly failedWorkspaceMigrationBuildResult: FailedWorkspaceMigrationBuildResult, + message = 'Workspace migration builder failed', + ) { + super(message); + this.name = 'WorkspaceMigrationBuilderExceptionV2'; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/types/validate-error-response.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/types/validate-error-response.type.ts new file mode 100644 index 0000000000..4b3ecbfebe --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/types/validate-error-response.type.ts @@ -0,0 +1,29 @@ +import { type FieldMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-field-metadata/types/field-metadata-minimal-information.type'; +import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; +import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type'; +import { type ObjectMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-object-metadata/types/object-metadata-minimal-information.type'; +import { type WorkspaceMigrationFieldActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2'; +import { type WorkspaceMigrationObjectActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2'; + +export type ValidationErrorFieldResponse = + Partial & { + operation: WorkspaceMigrationFieldActionTypeV2; + errors: FlatFieldMetadataValidationError[]; + }; +export type ValidationErrorObjectResponse = + Partial & { + operation: WorkspaceMigrationObjectActionTypeV2; + errors: FlatObjectMetadataValidationError[]; + fields: ValidationErrorFieldResponse[]; + }; +export type ValidationErrorResponse = { + summary: { + totalErrors: number; + invalidFields: number; + invalidObjects: number; + }; + errors: { + objectMetadata: ValidationErrorObjectResponse[]; + fieldMetadata: ValidationErrorFieldResponse[]; + }; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-failed-flat-field-metadata-validation-to-validation-error-field-response.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-failed-flat-field-metadata-validation-to-validation-error-field-response.util.ts new file mode 100644 index 0000000000..14fc6ad5d6 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-failed-flat-field-metadata-validation-to-validation-error-field-response.util.ts @@ -0,0 +1,18 @@ +import { type FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { type ValidationErrorFieldResponse } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/types/validate-error-response.type'; + +export const fromFailedFlatFieldMetadataValidationToValidationErrorFieldResponse = + ( + failedFlatFieldMetadataValidation: FailedFlatFieldMetadataValidation, + ): ValidationErrorFieldResponse => { + const { id, name, objectMetadataId } = + failedFlatFieldMetadataValidation.fieldMinimalInformation; + + return { + operation: failedFlatFieldMetadataValidation.type, + id, + name, + errors: failedFlatFieldMetadataValidation.errors, + objectMetadataId, + }; + }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-failed-flat-object-metadata-validation-to-validation-error-object-response.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-failed-flat-object-metadata-validation-to-validation-error-object-response.util.ts new file mode 100644 index 0000000000..3a0ebaf1ab --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-failed-flat-object-metadata-validation-to-validation-error-object-response.util.ts @@ -0,0 +1,24 @@ +import { type FailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; +import { type ValidationErrorObjectResponse } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/types/validate-error-response.type'; +import { fromFailedFlatFieldMetadataValidationToValidationErrorFieldResponse } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-failed-flat-field-metadata-validation-to-validation-error-field-response.util'; + +export const fromFailedFlatObjectMetadataValidationToValidationErrorObjectResponse = + ( + failedFlatObjectMetadataValidation: FailedFlatObjectMetadataValidation, + ): ValidationErrorObjectResponse => { + const { id, namePlural, nameSingular } = + failedFlatObjectMetadataValidation.objectMinimalInformation; + const fields = failedFlatObjectMetadataValidation.fieldLevelErrors.map( + fromFailedFlatFieldMetadataValidationToValidationErrorFieldResponse, + ); + const objectResponseError: ValidationErrorObjectResponse = { + operation: failedFlatObjectMetadataValidation.type, + fields, + id, + namePlural, + nameSingular, + errors: failedFlatObjectMetadataValidation.objectLevelErrors, + }; + + return objectResponseError; + }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-workspace-migration-builder-exception-to-validation-response-error.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-workspace-migration-builder-exception-to-validation-response-error.util.ts new file mode 100644 index 0000000000..7291bf7f40 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-workspace-migration-builder-exception-to-validation-response-error.util.ts @@ -0,0 +1,65 @@ +import { isFailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/utils/is-failed-flat-object-metadata-validation.util'; +import { type WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; +import { type ValidationErrorResponse } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/types/validate-error-response.type'; +import { fromFailedFlatFieldMetadataValidationToValidationErrorFieldResponse } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-failed-flat-field-metadata-validation-to-validation-error-field-response.util'; +import { fromFailedFlatObjectMetadataValidationToValidationErrorObjectResponse } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-failed-flat-object-metadata-validation-to-validation-error-object-response.util'; + +export const fromWorkspaceMigrationBuilderExceptionToValidationResponseError = ( + workspaceMigrationBuilderException: WorkspaceMigrationBuilderExceptionV2, +) => { + const emptyResponseError: ValidationErrorResponse = { + summary: { + invalidFields: 0, + invalidObjects: 0, + totalErrors: 0, + }, + errors: { + fieldMetadata: [], + objectMetadata: [], + }, + }; + + return workspaceMigrationBuilderException.failedWorkspaceMigrationBuildResult.errors.reduce( + ({ errors, summary }, failedValidationError) => { + if (isFailedFlatObjectMetadataValidation(failedValidationError)) { + const errorObjectResponse = + fromFailedFlatObjectMetadataValidationToValidationErrorObjectResponse( + failedValidationError, + ); + + return { + summary: { + ...summary, + invalidFields: + summary.invalidFields + errorObjectResponse.fields.length, + invalidObjects: ++summary.invalidObjects, + totalErrors: + ++summary.totalErrors + errorObjectResponse.fields.length, + }, + errors: { + ...errors, + objectMetadata: [...errors.objectMetadata, errorObjectResponse], + }, + }; + } + + const fieldResponseError = + fromFailedFlatFieldMetadataValidationToValidationErrorFieldResponse( + failedValidationError, + ); + + return { + summary: { + ...summary, + invalidFields: ++summary.invalidFields, + totalErrors: ++summary.totalErrors, + }, + errors: { + ...errors, + fieldMetadata: [...errors.fieldMetadata, fieldResponseError], + }, + }; + }, + emptyResponseError, + ); +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2.interceptor.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2.interceptor.ts new file mode 100644 index 0000000000..2cd06d9384 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2.interceptor.ts @@ -0,0 +1,42 @@ +import { + type CallHandler, + type ExecutionContext, + type NestInterceptor, +} from '@nestjs/common'; + +import { t } from '@lingui/core/macro'; +import { catchError, type Observable } from 'rxjs'; + +import { + BaseGraphQLError, + ErrorCode, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; +import { fromWorkspaceMigrationBuilderExceptionToValidationResponseError } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-workspace-migration-builder-exception-to-validation-response-error.util'; + +export class WorkspaceMigrationBuilderExceptionV2Interceptor + implements NestInterceptor +{ + intercept(_: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + catchError((error) => { + if (!(error instanceof WorkspaceMigrationBuilderExceptionV2)) { + throw error; + } + + const { errors, summary } = + fromWorkspaceMigrationBuilderExceptionToValidationResponseError( + error, + ); + + throw new BaseGraphQLError(error.message, ErrorCode.BAD_USER_INPUT, { + code: 'METADATA_VALIDATION_ERROR', + errors, + summary, + message: `Validation failed for ${summary.invalidObjects} object(s) and ${summary.invalidFields} field(s)`, + userFriendlyMessage: t`Validation failed for ${summary.invalidObjects} object(s) and ${summary.invalidFields} field(s)`, + }); + }), + ); + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service.ts index 00a269f9e5..9075629f4c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; -import { MultipleMetadataValidationErrors } from 'src/engine/core-modules/error/multiple-metadata-validation-errors'; import { + FailedWorkspaceMigrationBuildResult, WorkspaceMigrationBuildArgs, WorkspaceMigrationBuilderV2Service, } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service'; @@ -22,10 +22,9 @@ export class WorkspaceMigrationValidateBuildAndRunService { private readonly workspaceMigrationRunnerV2Service: WorkspaceMigrationRunnerV2Service, ) {} - public async validateBuildAndRunWorkspaceMigration({ - errorMessage, - ...builderArgs - }: WorkspaceMigrationBuildArgs & { errorMessage: string }) { + public async validateBuildAndRunWorkspaceMigration( + builderArgs: WorkspaceMigrationBuildArgs, + ): Promise { const validateAndBuildResult = await this.workspaceMigrationBuilderV2Service .validateAndBuild(builderArgs) .catch((error) => { @@ -37,10 +36,7 @@ export class WorkspaceMigrationValidateBuildAndRunService { }); if (validateAndBuildResult.status === 'fail') { - throw new MultipleMetadataValidationErrors( - validateAndBuildResult.errors, - errorMessage, - ); + return validateAndBuildResult; } await this.workspaceMigrationRunnerV2Service diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service.ts index 1ee9adc301..4493642fc3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service.ts @@ -2,9 +2,9 @@ import { Injectable } from '@nestjs/common'; import { type FromTo } from 'twenty-shared/types'; -import { FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { FailedFlatFieldMetadataValidation } 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 { FailedFlatObjectMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; +import { FailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; import { fromFlatObjectMetadataMapsToFlatObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-maps-to-flat-object-metadatas.util'; import { deletedCreatedUpdatedMatrixDispatcher } from 'src/engine/workspace-manager/workspace-migration-v2/utils/deleted-created-updated-matrix-dispatcher.util'; import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-v2-field-actions-builder.service'; @@ -20,6 +20,19 @@ export type WorkspaceMigrationV2BuilderOptions = { isSystemBuild: boolean; }; +type SuccessfulWorkspaceMigrationBuildResult = { + status: 'success'; + workspaceMigration: WorkspaceMigrationV2; +}; + +export type FailedWorkspaceMigrationBuildResult = { + status: 'fail'; + errors: ( + | FailedFlatObjectMetadataValidation + | FailedFlatFieldMetadataValidation + )[]; +}; + export type WorkspaceMigrationBuildArgs = { workspaceId: string; buildOptions: WorkspaceMigrationV2BuilderOptions; @@ -37,17 +50,8 @@ export class WorkspaceMigrationBuilderV2Service { workspaceId, buildOptions, }: WorkspaceMigrationBuildArgs): Promise< - | { - status: 'fail'; - errors: ( - | FailedFlatObjectMetadataValidationExceptions - | FailedFlatFieldMetadataValidationExceptions - )[]; - } - | { - status: 'success'; - workspaceMigration: WorkspaceMigrationV2; - } + | SuccessfulWorkspaceMigrationBuildResult + | FailedWorkspaceMigrationBuildResult > { const fromFlatObjectMetadatas = fromFlatObjectMetadataMapsToFlatObjectMetadatas( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-v2-field-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-v2-field-actions-builder.service.ts index d1402e8d39..481ff0d910 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-v2-field-actions-builder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-v2-field-actions-builder.service.ts @@ -93,8 +93,8 @@ export class WorkspaceMigrationV2FieldActionsBuilderService { }, ); - if (validationErrors.length > 0) { - validateAndBuildResult.failed.push(...validationErrors); + if (validationErrors.errors.length > 0) { + validateAndBuildResult.failed.push(validationErrors); continue; } @@ -142,8 +142,8 @@ export class WorkspaceMigrationV2FieldActionsBuilderService { }, ); - if (validationErrors.length > 0) { - validateAndBuildResult.failed.push(...validationErrors); + if (validationErrors.errors.length > 0) { + validateAndBuildResult.failed.push(validationErrors); continue; } @@ -174,8 +174,8 @@ export class WorkspaceMigrationV2FieldActionsBuilderService { }, ); - if (validationErrors.length > 0) { - validateAndBuildResult.failed.push(...validationErrors); + if (validationErrors.errors.length > 0) { + validateAndBuildResult.failed.push(validationErrors); continue; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-v2-object-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-v2-object-actions-builder.service.ts index aa58566175..7fc0ee4f96 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-v2-object-actions-builder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-v2-object-actions-builder.service.ts @@ -72,8 +72,11 @@ export class WorkspaceMigrationV2ObjectActionsBuilderService { }, ); - if (validationErrors.length > 0) { - validateAndBuildResult.failed.push(...validationErrors); + if ( + validationErrors.fieldLevelErrors.length > 0 || + validationErrors.objectLevelErrors.length > 0 + ) { + validateAndBuildResult.failed.push(validationErrors); continue; } @@ -113,8 +116,11 @@ export class WorkspaceMigrationV2ObjectActionsBuilderService { }, ); - if (validationErrors.length > 0) { - validateAndBuildResult.failed.push(...validationErrors); + if ( + validationErrors.fieldLevelErrors.length > 0 || + validationErrors.objectLevelErrors.length > 0 + ) { + validateAndBuildResult.failed.push(validationErrors); continue; } @@ -154,8 +160,11 @@ export class WorkspaceMigrationV2ObjectActionsBuilderService { }, ); - if (validationErrors.length > 0) { - validateAndBuildResult.failed.push(...validationErrors); + if ( + validationErrors.fieldLevelErrors.length > 0 || + validationErrors.objectLevelErrors.length > 0 + ) { + validateAndBuildResult.failed.push(validationErrors); continue; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/validate-and-build-metadata-result.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/validate-and-build-metadata-result.type.ts index 843ad5134c..db7c0880df 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/validate-and-build-metadata-result.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/validate-and-build-metadata-result.type.ts @@ -1,14 +1,14 @@ -import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { type FailedFlatFieldMetadataValidation } 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 { type FailedFlatObjectMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; +import { type FailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; import { type WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2'; export type ValidateAndBuildMetadataResult< T extends WorkspaceMigrationActionV2, > = { failed: ( - | FailedFlatFieldMetadataValidationExceptions - | FailedFlatObjectMetadataValidationExceptions + | FailedFlatFieldMetadataValidation + | FailedFlatObjectMetadataValidation )[]; created: T[]; deleted: T[]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2.ts index 7c0af51bbe..fa192e11df 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2.ts @@ -35,3 +35,9 @@ export type WorkspaceMigrationObjectActionV2 = export type WorkspaceMigrationObjectActionTypeV2 = WorkspaceMigrationObjectActionV2['type']; + +export const WORKSPACE_MIGRATION_OBJECT_ACTION_TYPES = [ + 'create_object', + 'delete_object', + 'update_object', +] as const satisfies WorkspaceMigrationObjectActionTypeV2[]; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap new file mode 100644 index 0000000000..813a52b659 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap @@ -0,0 +1,1408 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Object metadata creation should fail v2 when labelPlural contains only whitespace 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "Object label is too short", + "value": "", + }, + ], + "fields": [], + "id": Any, + "namePlural": "listingas", + "nameSingular": "listinga", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 0 field(s)", + "summary": { + "invalidFields": 0, + "invalidObjects": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when labelPlural exceeds maximum length 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "Object label is too long", + "value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + }, + ], + "fields": [], + "id": Any, + "namePlural": "listingas", + "nameSingular": "listinga", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 0 field(s)", + "summary": { + "invalidFields": 0, + "invalidObjects": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when labelPlural is empty 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "Object label is too short", + "value": "", + }, + ], + "fields": [], + "id": Any, + "namePlural": "listingas", + "nameSingular": "listinga", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 0 field(s)", + "summary": { + "invalidFields": 0, + "invalidObjects": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when labelSingular contains only whitespace 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "Object label is too short", + "value": "", + }, + ], + "fields": [], + "id": Any, + "namePlural": "listingas", + "nameSingular": "listinga", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 0 field(s)", + "summary": { + "invalidFields": 0, + "invalidObjects": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when labelSingular exceeds maximum length 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "Object label is too long", + "value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + }, + ], + "fields": [], + "id": Any, + "namePlural": "listingas", + "nameSingular": "listinga", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 0 field(s)", + "summary": { + "invalidFields": 0, + "invalidObjects": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when labelSingular is empty 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "Object label is too short", + "value": "", + }, + ], + "fields": [], + "id": Any, + "namePlural": "listingas", + "nameSingular": "listinga", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 0 field(s)", + "summary": { + "invalidFields": 0, + "invalidObjects": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when labels are identical 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "The singular and plural labels cannot be the same for an object", + "userFriendlyMessage": "The singular and plural labels cannot be the same for an object", + "value": "FooBar", + }, + ], + "fields": [], + "id": Any, + "namePlural": "listingas", + "nameSingular": "listinga", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 0 field(s)", + "summary": { + "invalidFields": 0, + "invalidObjects": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when labels with whitespaces result to be identical 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "The singular and plural labels cannot be the same for an object", + "userFriendlyMessage": "The singular and plural labels cannot be the same for an object", + "value": "FooBar", + }, + ], + "fields": [], + "id": Any, + "namePlural": "listingas", + "nameSingular": "listinga", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 0 field(s)", + "summary": { + "invalidFields": 0, + "invalidObjects": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when name exceeds maximum length 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [ + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is too long", + "userFriendlyMessage": "Name is too long", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + ], + "id": Any, + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is too long", + "userFriendlyMessage": "Name is too long", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + ], + "id": Any, + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is too long", + "userFriendlyMessage": "Name is too long", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + ], + "id": Any, + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is too long", + "userFriendlyMessage": "Name is too long", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + ], + "id": Any, + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is too long", + "userFriendlyMessage": "Name is too long", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + ], + "id": Any, + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "objectMetadataId": Any, + "operation": "create_field", + }, + ], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "Name is too long", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + ], + "fields": [], + "id": Any, + "namePlural": "listingas", + "nameSingular": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 5 field(s)", + "summary": { + "invalidFields": 5, + "invalidObjects": 1, + "totalErrors": 6, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 5 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when namePlural has invalid characters 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "μ", + }, + ], + "fields": [], + "id": Any, + "namePlural": "μ", + "nameSingular": "listinga", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 0 field(s)", + "summary": { + "invalidFields": 0, + "invalidObjects": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when namePlural is a reserved keyword 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "The name is not available", + "value": "users", + }, + ], + "fields": [], + "id": Any, + "namePlural": "users", + "nameSingular": "listinga", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 0 field(s)", + "summary": { + "invalidFields": 0, + "invalidObjects": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when namePlural is an empty string 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "Name is too short", + "value": "", + }, + { + "code": "INVALID_OBJECT_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "", + }, + ], + "fields": [], + "id": Any, + "namePlural": "", + "nameSingular": "listinga", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 0 field(s)", + "summary": { + "invalidFields": 0, + "invalidObjects": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when namePlural is not camelCased 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "Name should be in camelCase", + "value": "Not_Camel_Case", + }, + { + "code": "INVALID_OBJECT_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "Not_Camel_Case", + }, + ], + "fields": [], + "id": Any, + "namePlural": "Not_Camel_Case", + "nameSingular": "listinga", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 0 field(s)", + "summary": { + "invalidFields": 0, + "invalidObjects": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when nameSingular contains only one char and whitespaces 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [ + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name should be in camelCase", + "userFriendlyMessage": "Name should be in camelCase", + "value": "a a", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "a a", + }, + ], + "id": Any, + "name": "a a", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name should be in camelCase", + "userFriendlyMessage": "Name should be in camelCase", + "value": "a a", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "a a", + }, + ], + "id": Any, + "name": "a a", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name should be in camelCase", + "userFriendlyMessage": "Name should be in camelCase", + "value": "a a", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "a a", + }, + ], + "id": Any, + "name": "a a", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name should be in camelCase", + "userFriendlyMessage": "Name should be in camelCase", + "value": "a a", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "a a", + }, + ], + "id": Any, + "name": "a a", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name should be in camelCase", + "userFriendlyMessage": "Name should be in camelCase", + "value": "a a", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "a a", + }, + ], + "id": Any, + "name": "a a", + "objectMetadataId": Any, + "operation": "create_field", + }, + ], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "Name should be in camelCase", + "value": "a a", + }, + { + "code": "INVALID_OBJECT_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "a a", + }, + ], + "fields": [], + "id": Any, + "namePlural": "listingas", + "nameSingular": "a a", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 5 field(s)", + "summary": { + "invalidFields": 5, + "invalidObjects": 1, + "totalErrors": 6, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 5 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when nameSingular contains only whitespaces 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [ + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is too short", + "userFriendlyMessage": "Name is too short", + "value": "", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "", + }, + ], + "id": Any, + "name": "", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is too short", + "userFriendlyMessage": "Name is too short", + "value": "", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "", + }, + ], + "id": Any, + "name": "", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is too short", + "userFriendlyMessage": "Name is too short", + "value": "", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "", + }, + ], + "id": Any, + "name": "", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is too short", + "userFriendlyMessage": "Name is too short", + "value": "", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "", + }, + ], + "id": Any, + "name": "", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is too short", + "userFriendlyMessage": "Name is too short", + "value": "", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "", + }, + ], + "id": Any, + "name": "", + "objectMetadataId": Any, + "operation": "create_field", + }, + ], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "Name is too short", + "value": "", + }, + { + "code": "INVALID_OBJECT_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "", + }, + ], + "fields": [], + "id": Any, + "namePlural": "listingas", + "nameSingular": "", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 5 field(s)", + "summary": { + "invalidFields": 5, + "invalidObjects": 1, + "totalErrors": 6, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 5 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when nameSingular has invalid characters 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [ + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "μ", + }, + ], + "id": Any, + "name": "μ", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "μ", + }, + ], + "id": Any, + "name": "μ", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "μ", + }, + ], + "id": Any, + "name": "μ", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "μ", + }, + ], + "id": Any, + "name": "μ", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "μ", + }, + ], + "id": Any, + "name": "μ", + "objectMetadataId": Any, + "operation": "create_field", + }, + ], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "μ", + }, + ], + "fields": [], + "id": Any, + "namePlural": "listingas", + "nameSingular": "μ", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 5 field(s)", + "summary": { + "invalidFields": 5, + "invalidObjects": 1, + "totalErrors": 6, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 5 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when nameSingular is a reserved keyword 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [ + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "The name is not available", + "userFriendlyMessage": "The name is not available", + "value": "user", + }, + ], + "id": Any, + "name": "user", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "The name is not available", + "userFriendlyMessage": "The name is not available", + "value": "user", + }, + ], + "id": Any, + "name": "user", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "The name is not available", + "userFriendlyMessage": "The name is not available", + "value": "user", + }, + ], + "id": Any, + "name": "user", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "The name is not available", + "userFriendlyMessage": "The name is not available", + "value": "user", + }, + ], + "id": Any, + "name": "user", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "The name is not available", + "userFriendlyMessage": "The name is not available", + "value": "user", + }, + ], + "id": Any, + "name": "user", + "objectMetadataId": Any, + "operation": "create_field", + }, + ], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "The name is not available", + "value": "user", + }, + ], + "fields": [], + "id": Any, + "namePlural": "listingas", + "nameSingular": "user", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 5 field(s)", + "summary": { + "invalidFields": 5, + "invalidObjects": 1, + "totalErrors": 6, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 5 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when nameSingular is an empty string 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [ + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is too short", + "userFriendlyMessage": "Name is too short", + "value": "", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "", + }, + ], + "id": Any, + "name": "", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is too short", + "userFriendlyMessage": "Name is too short", + "value": "", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "", + }, + ], + "id": Any, + "name": "", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is too short", + "userFriendlyMessage": "Name is too short", + "value": "", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "", + }, + ], + "id": Any, + "name": "", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is too short", + "userFriendlyMessage": "Name is too short", + "value": "", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "", + }, + ], + "id": Any, + "name": "", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is too short", + "userFriendlyMessage": "Name is too short", + "value": "", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "", + }, + ], + "id": Any, + "name": "", + "objectMetadataId": Any, + "operation": "create_field", + }, + ], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "Name is too short", + "value": "", + }, + { + "code": "INVALID_OBJECT_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "", + }, + ], + "fields": [], + "id": Any, + "namePlural": "listingas", + "nameSingular": "", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 5 field(s)", + "summary": { + "invalidFields": 5, + "invalidObjects": 1, + "totalErrors": 6, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 5 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when nameSingular is not camelCased 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [ + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name should be in camelCase", + "userFriendlyMessage": "Name should be in camelCase", + "value": "Not_Camel_Case", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "Not_Camel_Case", + }, + ], + "id": Any, + "name": "Not_Camel_Case", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name should be in camelCase", + "userFriendlyMessage": "Name should be in camelCase", + "value": "Not_Camel_Case", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "Not_Camel_Case", + }, + ], + "id": Any, + "name": "Not_Camel_Case", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name should be in camelCase", + "userFriendlyMessage": "Name should be in camelCase", + "value": "Not_Camel_Case", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "Not_Camel_Case", + }, + ], + "id": Any, + "name": "Not_Camel_Case", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name should be in camelCase", + "userFriendlyMessage": "Name should be in camelCase", + "value": "Not_Camel_Case", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "Not_Camel_Case", + }, + ], + "id": Any, + "name": "Not_Camel_Case", + "objectMetadataId": Any, + "operation": "create_field", + }, + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Name should be in camelCase", + "userFriendlyMessage": "Name should be in camelCase", + "value": "Not_Camel_Case", + }, + { + "code": "INVALID_FIELD_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "Not_Camel_Case", + }, + ], + "id": Any, + "name": "Not_Camel_Case", + "objectMetadataId": Any, + "operation": "create_field", + }, + ], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "Name should be in camelCase", + "value": "Not_Camel_Case", + }, + { + "code": "INVALID_OBJECT_INPUT", + "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", + "value": "Not_Camel_Case", + }, + ], + "fields": [], + "id": Any, + "namePlural": "listingas", + "nameSingular": "Not_Camel_Case", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 5 field(s)", + "summary": { + "invalidFields": 5, + "invalidObjects": 1, + "totalErrors": 6, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 5 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when names are identical 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "The singular and plural names cannot be the same for an object", + "userFriendlyMessage": "The singular and plural names cannot be the same for an object", + "value": "fooBar", + }, + ], + "fields": [], + "id": Any, + "namePlural": "fooBar", + "nameSingular": "fooBar", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 0 field(s)", + "summary": { + "invalidFields": 0, + "invalidObjects": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; + +exports[`Object metadata creation should fail v2 when names with whitespaces result to be identical 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "errors": { + "fieldMetadata": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_OBJECT_INPUT", + "message": "The singular and plural names cannot be the same for an object", + "userFriendlyMessage": "The singular and plural names cannot be the same for an object", + "value": "fooBar", + }, + ], + "fields": [], + "id": Any, + "namePlural": "fooBar", + "nameSingular": "fooBar", + "operation": "create_object", + }, + ], + }, + "message": "Validation failed for 1 object(s) and 0 field(s)", + "summary": { + "invalidFields": 0, + "invalidObjects": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Validation failed for 1 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating object", + "name": "GraphQLError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/common/object-metadata-label-failing-tests-cases.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/common/object-metadata-label-failing-tests-cases.ts new file mode 100644 index 0000000000..60ccc1723c --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/common/object-metadata-label-failing-tests-cases.ts @@ -0,0 +1,46 @@ +import { type EachTestingContext } from 'twenty-shared/testing'; + +import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; + +export const OBJECT_METADATA_LABEL_FAILING_TEST_CASES: EachTestingContext< + Partial> +>[] = [ + { + title: 'when labelSingular is empty', + context: { labelSingular: '' }, + }, + { + title: 'when labelPlural is empty', + context: { labelPlural: '' }, + }, + { + title: 'when labelSingular exceeds maximum length', + context: { labelSingular: 'A'.repeat(64) }, + }, + { + title: 'when labelPlural exceeds maximum length', + context: { labelPlural: 'A'.repeat(64) }, + }, + { + title: 'when labelSingular contains only whitespace', + context: { labelSingular: ' ' }, + }, + { + title: 'when labelPlural contains only whitespace', + context: { labelPlural: ' ' }, + }, + { + title: 'when labels are identical', + context: { + labelPlural: 'fooBar', + labelSingular: 'fooBar', + }, + }, + { + title: 'when labels with whitespaces result to be identical', + context: { + labelPlural: ' fooBar ', + labelSingular: 'fooBar', + }, + }, +]; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/common/object-metadata-names-failing-tests-cases.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/common/object-metadata-names-failing-tests-cases.ts new file mode 100644 index 0000000000..73fe511d51 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/common/object-metadata-names-failing-tests-cases.ts @@ -0,0 +1,66 @@ +import { type EachTestingContext } from 'twenty-shared/testing'; + +import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; + +export const OBJECT_METADATA_NAMES_FAILING_TEST_CASES: EachTestingContext< + Partial> +>[] = [ + { + title: 'when nameSingular has invalid characters', + context: { nameSingular: 'μ' }, + }, + { + title: 'when namePlural has invalid characters', + context: { namePlural: 'μ' }, + }, + { + title: 'when nameSingular is a reserved keyword', + context: { nameSingular: 'user' }, + }, + { + title: 'when namePlural is a reserved keyword', + context: { namePlural: 'users' }, + }, + { + title: 'when nameSingular is not camelCased', + context: { nameSingular: 'Not_Camel_Case' }, + }, + { + title: 'when namePlural is not camelCased', + context: { namePlural: 'Not_Camel_Case' }, + }, + { + title: 'when namePlural is an empty string', + context: { namePlural: '' }, + }, + { + title: 'when nameSingular is an empty string', + context: { nameSingular: '' }, + }, + { + title: 'when nameSingular contains only whitespaces', + context: { nameSingular: ' ' }, + }, + { + title: 'when nameSingular contains only one char and whitespaces', + context: { nameSingular: ' a a ' }, + }, + { + title: 'when name exceeds maximum length', + context: { nameSingular: 'a'.repeat(64) }, + }, + { + title: 'when names are identical', + context: { + nameSingular: 'fooBar', + namePlural: 'fooBar', + }, + }, + { + title: 'when names with whitespaces result to be identical', + context: { + nameSingular: ' fooBar ', + namePlural: 'fooBar', + }, + }, +]; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-create-one-object-metadata-v2.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-create-one-object-metadata-v2.integration-spec.ts new file mode 100644 index 0000000000..876645e2af --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-create-one-object-metadata-v2.integration-spec.ts @@ -0,0 +1,52 @@ +import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util'; +import { OBJECT_METADATA_LABEL_FAILING_TEST_CASES } from 'test/integration/metadata/suites/object-metadata/common/object-metadata-label-failing-tests-cases'; +import { OBJECT_METADATA_NAMES_FAILING_TEST_CASES } from 'test/integration/metadata/suites/object-metadata/common/object-metadata-names-failing-tests-cases'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; +import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { eachTestingContextFilter } from 'twenty-shared/testing'; + +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util'; + +const allTestsUseCases = [ + ...OBJECT_METADATA_NAMES_FAILING_TEST_CASES, + ...OBJECT_METADATA_LABEL_FAILING_TEST_CASES, +]; + +describe('Object metadata creation should fail v2', () => { + beforeAll(async () => { + const enableWorkspaceMigrationV2 = updateFeatureFlagFactory( + SEED_APPLE_WORKSPACE_ID, + FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + true, + ); + + await makeGraphqlAPIRequest(enableWorkspaceMigrationV2); + }); + + afterAll(async () => { + const enablePermissionsQuery = updateFeatureFlagFactory( + SEED_APPLE_WORKSPACE_ID, + FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + false, + ); + + await makeGraphqlAPIRequest(enablePermissionsQuery); + }); + it.each(eachTestingContextFilter(allTestsUseCases))( + '$title', + async ({ context }) => { + const { errors } = await createOneObjectMetadata({ + input: getMockCreateObjectInput(context), + expectToFail: true, + }); + + expect(errors.length).toBe(1); + expect(errors[0]).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny(errors[0]), + ); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-create-one-object-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-create-one-object-metadata.integration-spec.ts index 177e78f7c9..251071dd59 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-create-one-object-metadata.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-create-one-object-metadata.integration-spec.ts @@ -1,125 +1,13 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input'; -import { type EachTestingContext } from 'twenty-shared/testing'; +import { OBJECT_METADATA_LABEL_FAILING_TEST_CASES } from 'test/integration/metadata/suites/object-metadata/common/object-metadata-label-failing-tests-cases'; +import { OBJECT_METADATA_NAMES_FAILING_TEST_CASES } from 'test/integration/metadata/suites/object-metadata/common/object-metadata-names-failing-tests-cases'; import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; - -type CreateObjectInputPayload = Omit< - CreateObjectInput, - 'workspaceId' | 'dataSourceId' ->; - -type CreateOneObjectMetadataItemTestingContext = EachTestingContext< - Partial ->[]; -const failingNamesCreationTestsUseCase: CreateOneObjectMetadataItemTestingContext = - [ - { - title: 'when nameSingular has invalid characters', - context: { nameSingular: 'μ' }, - }, - { - title: 'when namePlural has invalid characters', - context: { namePlural: 'μ' }, - }, - { - title: 'when nameSingular is a reserved keyword', - context: { nameSingular: 'user' }, - }, - { - title: 'when namePlural is a reserved keyword', - context: { namePlural: 'users' }, - }, - { - title: 'when nameSingular is not camelCased', - context: { nameSingular: 'Not_Camel_Case' }, - }, - { - title: 'when namePlural is not camelCased', - context: { namePlural: 'Not_Camel_Case' }, - }, - { - title: 'when namePlural is an empty string', - context: { namePlural: '' }, - }, - { - title: 'when nameSingular is an empty string', - context: { nameSingular: '' }, - }, - { - title: 'when nameSingular contains only whitespaces', - context: { nameSingular: ' ' }, - }, - { - title: 'when nameSingular contains only one char and whitespaces', - context: { nameSingular: ' a a ' }, - }, - { - title: 'when name exceeds maximum length', - context: { nameSingular: 'a'.repeat(64) }, - }, - { - title: 'when names are identical', - context: { - nameSingular: 'fooBar', - namePlural: 'fooBar', - }, - }, - { - title: 'when names with whitespaces result to be identical', - context: { - nameSingular: ' fooBar ', - namePlural: 'fooBar', - }, - }, - ]; - -const failingLabelsCreationTestsUseCase: CreateOneObjectMetadataItemTestingContext = - [ - { - title: 'when labelSingular is empty', - context: { labelSingular: '' }, - }, - { - title: 'when labelPlural is empty', - context: { labelPlural: '' }, - }, - { - title: 'when labelSingular exceeds maximum length', - context: { labelSingular: 'A'.repeat(64) }, - }, - { - title: 'when labelPlural exceeds maximum length', - context: { labelPlural: 'A'.repeat(64) }, - }, - { - title: 'when labelSingular contains only whitespace', - context: { labelSingular: ' ' }, - }, - { - title: 'when labelPlural contains only whitespace', - context: { labelPlural: ' ' }, - }, - { - title: 'when labels are identical', - context: { - labelPlural: 'fooBar', - labelSingular: 'fooBar', - }, - }, - { - title: 'when labels with whitespaces result to be identical', - context: { - labelPlural: ' fooBar ', - labelSingular: 'fooBar', - }, - }, - ]; const allTestsUseCases = [ - ...failingNamesCreationTestsUseCase, - ...failingLabelsCreationTestsUseCase, + ...OBJECT_METADATA_NAMES_FAILING_TEST_CASES, + ...OBJECT_METADATA_LABEL_FAILING_TEST_CASES, ]; describe('Object metadata creation should fail', () => {