Refactor twenty-front metadata api services for v2 (#15360)

# Introduction
Please first review this PR initial base
https://github.com/twentyhq/twenty/pull/15358
In a nutshell refactored the frontend fetchers to display v2 errors
format smoothly
Please note that the v2 now finished the whole validation and does fail
fast anymore ( summary is hardcoded for the moment )
```json
[
  {
    "extensions": {
      "code": "BAD_USER_INPUT",
      "errors": {
        "cronTrigger": [],
        "databaseEventTrigger": [],
        "fieldMetadata": [
          {
            "errors": [
              {
                "code": "INVALID_FIELD_INPUT",
                "message": "Default value should be as quoted string",
                "value": "",
              },
              {
                "code": "INVALID_FIELD_INPUT",
                "message": "Default value "" must be one of the option values",
                "value": "",
              },
            ],
            "flatEntityMinimalInformation": {
              "id": Any<String>,
              "name": "testField",
              "objectMetadataId": Any<String>,
            },
            "status": "fail",
            "type": "create_field",
          },
        ],
        "index": [],
        "objectMetadata": [],
        "routeTrigger": [],
        "serverlessFunction": [],
        "view": [],
        "viewField": [],
        "viewFilter": [],
        "viewGroup": [],
      },
      "message": "Validation failed for 0 object(s) and 0 field(s)",
      "summary": {
        "invalidCronTrigger": 0,
        "invalidDatabaseEventTrigger": 0,
        "invalidFieldMetadata": 0,
        "invalidIndex": 0,
        "invalidObjectMetadata": 0,
        "invalidRouteTrigger": 0,
        "invalidServerlessFunction": 0,
        "invalidView": 0,
        "invalidViewField": 0,
        "invalidViewFilter": 0,
        "invalidViewGroup": 0,
        "totalErrors": 0,
      },
      "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
    },
    "message": "Multiple validation errors occurred while creating fields",
    "name": "GraphQLError",
  },
]
```

## What's done
- `usePersistView` tool ( CRUD )
- renamed `usePersistViewX` tools accordingly ( no more records or core
)
- Now catching a lot of before unhandled exceptions
- refactored each services to handle their own exception handlers and
return either the response or the error within a discriminated union
record

## Result
### Primary entity error 
When performing an metadata operation on a given metadata, if validation
errors occurs we will display each of them in a toast
Here while creating an object metadata.
<img width="700" height="327" alt="image"
src="https://github.com/user-attachments/assets/0c33d13c-c66c-4749-af36-b253abd3449b"
/>

### Related entity error
Still while creating an object
<img width="700" height="327" alt="image"
src="https://github.com/user-attachments/assets/52607788-c4e9-470c-ac8c-23437345ee5c"
/>

### Translated
<img width="700" height="327" alt="image"
src="https://github.com/user-attachments/assets/a7198c20-ae82-47a6-910c-761de9594672"
/>


## Conclusion
This PR is an extract of https://github.com/twentyhq/twenty/pull/15331
close https://github.com/twentyhq/core-team-issues/issues/1776
## Notes
- Not refactor around triggers services as they're not consumed directly
by any frontend services
This commit is contained in:
Paul Rastoin
2025-10-28 10:50:41 +01:00
committed by GitHub
parent 08e1fc8bca
commit 503a5029da
56 changed files with 2073 additions and 1136 deletions
@@ -0,0 +1,82 @@
import { type ApolloError } from '@apollo/client';
import {
type AllMetadataName,
type MetadataValidationErrorResponse,
WorkspaceMigrationV2ExceptionCode,
} from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
export type MetadataErrorClassification =
| { type: 'v1'; error: ApolloError }
| {
type: 'v2-validation';
extensions: MetadataValidationErrorResponse;
primaryMetadataName: AllMetadataName;
relatedFailingMetadataNames: AllMetadataName[];
}
| {
type: 'v2-internal';
code: WorkspaceMigrationV2ExceptionCode;
message: string;
};
const isMetadataValidationError = (
extensions: Record<string, unknown>,
): extensions is MetadataValidationErrorResponse =>
extensions.code === 'METADATA_VALIDATION_FAILED';
const isMetadataInternalError = (
extensions: Record<string, unknown>,
): boolean => {
return (
isDefined(extensions) &&
isDefined(extensions.subCode) &&
(extensions.subCode ===
WorkspaceMigrationV2ExceptionCode.BUILDER_INTERNAL_SERVER_ERROR ||
extensions.subCode ===
WorkspaceMigrationV2ExceptionCode.RUNNER_INTERNAL_SERVER_ERROR)
);
};
type ClassifyMetadataErrorArgs = {
error: ApolloError;
primaryMetadataName: AllMetadataName;
};
export const classifyMetadataError = ({
error,
primaryMetadataName,
}: ClassifyMetadataErrorArgs): MetadataErrorClassification => {
const extensions = error.graphQLErrors?.[0]?.extensions;
if (!isDefined(extensions)) {
return { type: 'v1', error };
}
if (isMetadataValidationError(extensions)) {
const failingMetadataNames = Object.keys(extensions.errors) as [
keyof MetadataValidationErrorResponse['errors'],
];
const relatedFailingMetadataNames = failingMetadataNames.filter(
(metadataName) =>
extensions.errors[metadataName].length > 0 &&
metadataName !== primaryMetadataName,
);
return {
type: 'v2-validation',
extensions,
primaryMetadataName,
relatedFailingMetadataNames,
};
}
if (isMetadataInternalError(extensions)) {
return {
type: 'v2-internal',
code: extensions.subCode as WorkspaceMigrationV2ExceptionCode,
message: (extensions.userFriendlyMessage as string) || error.message,
};
}
return { type: 'v1', error };
};