Fail slow + structured validator error response scoped by object and fields (#13980)
# Introduction
Example of returned response: ( snapshot copy don't mind jest expect any
values )
```json
{
"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<String>,
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"objectMetadataId": Any<String>,
"operation": "create_field",
},
{
"errors": [
{
"code": "INVALID_FIELD_INPUT",
"message": "Name is too long",
"userFriendlyMessage": "Name is too long",
"value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
],
"id": Any<String>,
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"objectMetadataId": Any<String>,
"operation": "create_field",
},
{
"errors": [
{
"code": "INVALID_FIELD_INPUT",
"message": "Name is too long",
"userFriendlyMessage": "Name is too long",
"value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
],
"id": Any<String>,
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"objectMetadataId": Any<String>,
"operation": "create_field",
},
{
"errors": [
{
"code": "INVALID_FIELD_INPUT",
"message": "Name is too long",
"userFriendlyMessage": "Name is too long",
"value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
],
"id": Any<String>,
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"objectMetadataId": Any<String>,
"operation": "create_field",
},
{
"errors": [
{
"code": "INVALID_FIELD_INPUT",
"message": "Name is too long",
"userFriendlyMessage": "Name is too long",
"value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
],
"id": Any<String>,
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"objectMetadataId": Any<String>,
"operation": "create_field",
},
],
"objectMetadata": [
{
"errors": [
{
"code": "INVALID_OBJECT_INPUT",
"message": "Name is too long",
"value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
],
"fields": [],
"id": Any<String>,
"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",
}
```
## Note
We should put in place integrations tests on REST API too, in order to
ensure we receive the same response error
This commit is contained in:
@@ -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,
|
||||
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
export class MultipleMetadataValidationErrors extends Error {
|
||||
constructor(
|
||||
public readonly errors: Error[],
|
||||
message = 'Multiple errors occurred',
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'AggregateError';
|
||||
}
|
||||
}
|
||||
+7
@@ -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];
|
||||
|
||||
+6
-2
@@ -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,
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
+55
-36
@@ -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: {
|
||||
|
||||
+3
@@ -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);
|
||||
|
||||
+17
-13
@@ -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<T>): 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}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+118
-129
@@ -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<T>): Promise<
|
||||
FailedFlatFieldMetadataValidationExceptions[]
|
||||
> {
|
||||
const errors: FailedFlatFieldMetadataValidationExceptions[] = [];
|
||||
}: ValidateOneFieldMetadataArgs<T>): Promise<FailedFlatFieldMetadataValidation> {
|
||||
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<T>): Promise<
|
||||
FailedFlatFieldMetadataValidationExceptions[]
|
||||
> {
|
||||
const errors: FailedFlatFieldMetadataValidationExceptions[] = [];
|
||||
}: ValidateOneFieldMetadataArgs<T>): Promise<FailedFlatFieldMetadataValidation> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-21
@@ -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 = <T>(
|
||||
elementToValidate: T,
|
||||
validators: FlatMetadataValidator<T>[],
|
||||
): FailedFlatFieldMetadataValidationExceptions[] =>
|
||||
validators
|
||||
.map((validator) =>
|
||||
runFlatFieldMetadataValidator(elementToValidate, validator),
|
||||
)
|
||||
.filter(isDefined);
|
||||
export type FailedFlatFieldMetadataValidation = {
|
||||
type: WorkspaceMigrationFieldActionTypeV2;
|
||||
errors: FlatFieldMetadataValidationError[];
|
||||
fieldMinimalInformation: Partial<FieldMetadataMinimalInformation>;
|
||||
};
|
||||
|
||||
+2
-2
@@ -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<T> = {
|
||||
status: 'success';
|
||||
|
||||
+6
@@ -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'
|
||||
>;
|
||||
+3
-3
@@ -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<P>,
|
||||
) =>
|
||||
| FailedFlatFieldMetadataValidationExceptions[]
|
||||
| Promise<FailedFlatFieldMetadataValidationExceptions[]>;
|
||||
| FlatFieldMetadataValidationError[]
|
||||
| Promise<FlatFieldMetadataValidationError[]>;
|
||||
};
|
||||
|
||||
+8
@@ -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;
|
||||
};
|
||||
+15
-21
@@ -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:
|
||||
|
||||
+19
-9
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+25
-29
@@ -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<FlatFieldMetadata> => {
|
||||
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(', ')}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
-23
@@ -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 = <T>(
|
||||
elementToValidate: T,
|
||||
{ message, validator }: FlatMetadataValidator<T>,
|
||||
): FailedFlatFieldMetadataValidationExceptions | undefined => {
|
||||
const isInvalid = validator(elementToValidate);
|
||||
|
||||
if (isInvalid) {
|
||||
return new FieldMetadataException(
|
||||
message,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
{
|
||||
userFriendlyMessage: message,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
+23
-13
@@ -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 = <T>(
|
||||
elementToValidate: T,
|
||||
validators: FlatMetadataValidator<T>[],
|
||||
): FailedFlatFieldMetadataValidationExceptions[] =>
|
||||
validators
|
||||
.map((validator) =>
|
||||
runFlatFieldMetadataValidator(elementToValidate, validator),
|
||||
)
|
||||
.filter(isDefined);
|
||||
export const runFlatFieldMetadataValidators = <T>({
|
||||
elementToValidate,
|
||||
validators,
|
||||
}: {
|
||||
elementToValidate: T;
|
||||
validators: FlatMetadataValidator<T>[];
|
||||
}): FlatFieldMetadataValidationError[] => {
|
||||
return validators.flatMap(({ validator, message }) => {
|
||||
const isInvalid = validator(elementToValidate);
|
||||
|
||||
if (isInvalid) {
|
||||
return {
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message,
|
||||
value: elementToValidate,
|
||||
};
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
+14
-3
@@ -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 = <T>(
|
||||
inputTranspilationResults: FieldInputTranspilationResult<T>[],
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
+84
-57
@@ -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 = <T extends EnumFieldMetadataType>(
|
||||
flatFieldMetadata: FlatFieldMetadata<T>,
|
||||
) => {
|
||||
): 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 = <T extends EnumFieldMetadataType>(
|
||||
return [...optionsValidationErrors, ...duplicatedValidationErrors];
|
||||
};
|
||||
|
||||
const validateSelectDefaultValue = (
|
||||
options: FieldMetadataOptions<EnumFieldMetadataType>,
|
||||
defaultValue: string | string[],
|
||||
): FailedFlatFieldMetadataValidationExceptions[] => {
|
||||
const validateSelectDefaultValue = ({
|
||||
defaultValue,
|
||||
options,
|
||||
}: {
|
||||
options: FieldMetadataOptions<EnumFieldMetadataType>;
|
||||
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<EnumFieldMetadataType>,
|
||||
multiSelectDefaultValue: unknown,
|
||||
): FailedFlatFieldMetadataValidationExceptions[] => {
|
||||
const validateMultiSelectDefaultValue = ({
|
||||
multiSelectDefaultValue,
|
||||
options,
|
||||
}: {
|
||||
options: FieldMetadataOptions<EnumFieldMetadataType>;
|
||||
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<EnumFieldMetadataType>,
|
||||
'defaultValue'
|
||||
> &
|
||||
NonNullableRequired<
|
||||
Pick<FlatFieldMetadata<EnumFieldMetadataType>, 'defaultValue'>
|
||||
>,
|
||||
) => {
|
||||
switch (flatFieldMetadata.type) {
|
||||
const validateFieldMetadataDefaultValue = ({
|
||||
defaultValue,
|
||||
options,
|
||||
type,
|
||||
}: Omit<FlatFieldMetadata<EnumFieldMetadataType>, 'defaultValue'> &
|
||||
NonNullableRequired<
|
||||
Pick<FlatFieldMetadata<EnumFieldMetadataType>, '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<EnumFieldMetadataType>): FailedFlatFieldMetadataValidationExceptions[] => {
|
||||
}: ValidateOneFieldMetadataArgs<EnumFieldMetadataType>): FlatFieldMetadataValidationError[] => {
|
||||
const optionsValidationErrors = validateFieldMetadataInputOptions(
|
||||
flatFieldMetadataToValidate,
|
||||
);
|
||||
|
||||
+19
-24
@@ -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;
|
||||
};
|
||||
|
||||
+17
-4
@@ -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 [];
|
||||
});
|
||||
|
||||
+32
-32
@@ -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<FieldMetadataType.RELATION>): Promise<
|
||||
FailedFlatFieldMetadataValidationExceptions[]
|
||||
FlatFieldMetadataValidationError[]
|
||||
> => {
|
||||
const { relationTargetFieldMetadataId, relationTargetObjectMetadataId } =
|
||||
flatFieldMetadataToValidate;
|
||||
|
||||
const uuidsValidation = [
|
||||
relationTargetObjectMetadataId,
|
||||
relationTargetFieldMetadataId,
|
||||
].flatMap<FieldMetadataException>((id) =>
|
||||
].flatMap<FlatFieldMetadataValidationError>((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;
|
||||
|
||||
+103
-82
@@ -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<FailedFlatObjectMetadataValidation> {
|
||||
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;
|
||||
|
||||
+10
-7
@@ -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<ObjectMetadataMinimalInformation>;
|
||||
};
|
||||
|
||||
+8
@@ -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;
|
||||
};
|
||||
+5
@@ -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<FlatObjectMetadata, 'id' | 'namePlural' | 'nameSingular'>
|
||||
>;
|
||||
+11
@@ -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<Omit<FailedFlatObjectMetadataValidation, 'type'>> &
|
||||
Pick<FailedFlatObjectMetadataValidation, 'type'>,
|
||||
): FailedFlatObjectMetadataValidation => ({
|
||||
fieldLevelErrors: [],
|
||||
objectLevelErrors: [],
|
||||
objectMinimalInformation: {},
|
||||
...overrides,
|
||||
});
|
||||
+16
@@ -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,
|
||||
);
|
||||
};
|
||||
-23
@@ -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 = <T>(
|
||||
elementToValidate: T,
|
||||
{ message, validator }: FlatMetadataValidator<T>,
|
||||
): FailedFlatObjectMetadataValidationExceptions | undefined => {
|
||||
const validationFailed = validator(elementToValidate);
|
||||
|
||||
if (validationFailed) {
|
||||
return new ObjectMetadataException(
|
||||
message,
|
||||
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
{
|
||||
userFriendlyMessage: message,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
+23
-13
@@ -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 = <T>(
|
||||
elementToValidate: T,
|
||||
validators: FlatMetadataValidator<T>[],
|
||||
): FailedFlatObjectMetadataValidationExceptions[] =>
|
||||
validators
|
||||
.map((validator) =>
|
||||
runFlatObjectMetadataValidator(elementToValidate, validator),
|
||||
)
|
||||
.filter(isDefined);
|
||||
export const runFlatObjectMetadataValidators = <T>({
|
||||
elementToValidate,
|
||||
validators,
|
||||
}: {
|
||||
elementToValidate: T;
|
||||
validators: FlatMetadataValidator<T>[];
|
||||
}): FlatObjectMetadataValidationError[] => {
|
||||
return validators.flatMap(({ validator, message }) => {
|
||||
const isInvalid = validator(elementToValidate);
|
||||
|
||||
if (isInvalid) {
|
||||
return {
|
||||
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
message,
|
||||
value: elementToValidate,
|
||||
};
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
+18
-29
@@ -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`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-17
@@ -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<FlatObjectMetadata, 'labelPlural' | 'labelSingular'> &
|
||||
ObjectMetadataMinimalInformation): FlatObjectMetadataValidationError[] => {
|
||||
const errors: FlatObjectMetadataValidationError[] = [];
|
||||
const validators: FlatMetadataValidator<string>[] = [
|
||||
{
|
||||
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;
|
||||
|
||||
+16
-14
@@ -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<FlatObjectMetadata, 'nameSingular' | 'namePlural'>) => {
|
||||
const errors: FailedFlatObjectMetadataValidationExceptions[] = [];
|
||||
}: Pick<FlatObjectMetadata, 'nameSingular' | 'namePlural'> &
|
||||
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;
|
||||
|
||||
+55
-36
@@ -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(
|
||||
|
||||
+5
-1
@@ -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],
|
||||
},
|
||||
],
|
||||
|
||||
+11
@@ -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';
|
||||
}
|
||||
}
|
||||
+29
@@ -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<FieldMetadataMinimalInformation> & {
|
||||
operation: WorkspaceMigrationFieldActionTypeV2;
|
||||
errors: FlatFieldMetadataValidationError[];
|
||||
};
|
||||
export type ValidationErrorObjectResponse =
|
||||
Partial<ObjectMetadataMinimalInformation> & {
|
||||
operation: WorkspaceMigrationObjectActionTypeV2;
|
||||
errors: FlatObjectMetadataValidationError[];
|
||||
fields: ValidationErrorFieldResponse[];
|
||||
};
|
||||
export type ValidationErrorResponse = {
|
||||
summary: {
|
||||
totalErrors: number;
|
||||
invalidFields: number;
|
||||
invalidObjects: number;
|
||||
};
|
||||
errors: {
|
||||
objectMetadata: ValidationErrorObjectResponse[];
|
||||
fieldMetadata: ValidationErrorFieldResponse[];
|
||||
};
|
||||
};
|
||||
+18
@@ -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,
|
||||
};
|
||||
};
|
||||
+24
@@ -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;
|
||||
};
|
||||
+65
@@ -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<ValidationErrorResponse>(
|
||||
({ 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,
|
||||
);
|
||||
};
|
||||
+42
@@ -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<unknown> {
|
||||
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)`,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-9
@@ -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<FailedWorkspaceMigrationBuildResult | undefined> {
|
||||
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
|
||||
|
||||
+17
-13
@@ -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(
|
||||
|
||||
+6
-6
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+15
-6
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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[];
|
||||
|
||||
+6
@@ -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[];
|
||||
|
||||
+1408
File diff suppressed because it is too large
Load Diff
+46
@@ -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<Omit<CreateObjectInput, 'workspaceId' | 'dataSourceId'>>
|
||||
>[] = [
|
||||
{
|
||||
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',
|
||||
},
|
||||
},
|
||||
];
|
||||
+66
@@ -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<Omit<CreateObjectInput, 'workspaceId' | 'dataSourceId'>>
|
||||
>[] = [
|
||||
{
|
||||
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',
|
||||
},
|
||||
},
|
||||
];
|
||||
+52
@@ -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]),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
+4
-116
@@ -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<CreateObjectInputPayload>
|
||||
>[];
|
||||
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', () => {
|
||||
|
||||
Reference in New Issue
Block a user