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:
+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,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user