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:
Paul Rastoin
2025-08-20 19:33:15 +02:00
committed by GitHub
parent 4752970f79
commit e99c67d6b5
53 changed files with 2589 additions and 810 deletions
@@ -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];
@@ -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,
],
},
],
}),
@@ -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: {
@@ -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);