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
@@ -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`,
});
}
}
@@ -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;
@@ -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;