From 362d540aaccdec6c63d1505dbe57c96f1e6d8478 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Fri, 23 May 2025 15:36:02 +0200 Subject: [PATCH] Misc. of sentry improvements (#12233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR mixes various initiatives to improve visibility on sentry **1. Catch errors on workflow jobs** commit [catch workflowTriggerExceptions in job handle](https://github.com/twentyhq/twenty/commit/1dbba8c9e27fed8fe01c5ac91d492720c45ba7a5) @thomtrp **2. Fix type in messagingImportExceptionHandler** commit [fix type issue on messagingImportExceptionHandler](https://github.com/twentyhq/twenty/commit/919bb3844c2e11f929a01dff0539e0a2ec7e4b00) @guillim **3. Catch invalid uuid errors thrown by Postgres by rightfully typing expected id as uuid** commits [use UUIDFilter instead of IDFilter to get graphqlError in case of malformed id](https://github.com/twentyhq/twenty/commit/57cc315efeb0365b1cd589f7487dbfc886338d26), [use UUIDFilter (2)](https://github.com/twentyhq/twenty/commit/304553d770426362ee8bfcd3c2e1f0066abe2c04), [fix ids typed as UUID instead of ID](https://github.com/twentyhq/twenty/commit/f95d6319cf9290a5ae1d59eb634617a511411633) @Weiko ⚠️⚠️⚠️ when we deploy this PR we need to flush the schema types from redis as this PR changes them ⚠️⚠️⚠️ **4. Do not group UNKNOWN errors together** commit [do not group unknown errors together](https://github.com/twentyhq/twenty/commit/c299b39c8fdcc4d6656dc5ce58437e4365cd60b0) Some CustomException classes have introduced UNKNOWN error codes as a default fallback error code. We use CustomException codes to group issues together, but we don't want to do it with UNKNOWN error as they may not have anything in common. For exemple [this sentry for UNKNOWN code](https://twenty-v7.sentry.io/issues/6605750776/events/a72272d8941b4fa2add9b1f39c196d3f/?environment=prod&environment=prod-eu&project=4507072499810304&query=Unknown&referrer=next-event&stream_index=0) groups together "Unknown error importing calendar events for calendar channel...", "Insufficent permissions...", to name a few. **5. Improve postgres error grouping** commit [group together postgres errors](https://github.com/twentyhq/twenty/commit/567c25495e37cdccfc9010f346145f7d5cc46a54) Postgres error are thrown by typeORM as QueryFailedError. we have a lot of them on sentry where they are badly grouped They are currently grouped on sentry according to the stack trace, which leads them to sometimes be grouped even if they don't have anything in common : for exemple [this sentry for QueryFailedError](https://twenty-v7.sentry.io/issues/6563624590/events/2d636821e27a448595b647b4b5a7d6a8/?environment=prod&environment=prod-eu&project=4507072499810304&query=is%3Aunresolved%20%21issue.type%3A%5Bperformance_consecutive_db_queries%2Cperformance_consecutive_http%2Cperformance_file_io_main_thread%2Cperformance_db_main_thread%2Cperformance_n_plus_one_db_queries%2Cperformance_n_plus_one_api_calls%2Cperformance_p95_endpoint_regression%2Cperformance_slow_db_query%2Cperformance_render_blocking_asset_span%2Cperformance_uncompressed_assets%2Cperformance_http_overhead%2Cperformance_large_http_payload%5D%20timesSeen%3A%3E10&referrer=previous-event&sort=date&stream_index=0) groups together "user mapping not found for "postgres" and "invalide type for uuid: 'fallback-id'" to name a few. I attempted to improve the grouping by grouping them with a new custom fingerPrint composed of the [code returned by Postgres](https://www.postgresql.org/docs/current/errcodes-appendix.html) + the truncated operation name (Find, Aggregate, Check...). This is still not ideal as postgres code are quite broad - we could have the same error code for two Find operations with different causes. let's give this a try ! --- .../react-state-management-guidelines.md | 2 +- .../lib/requests/delete-workflow.ts | 2 +- .../lib/requests/destroy-workflow.ts | 2 +- .../twenty-front/src/generated/graphql.tsx | 24 +- .../hooks/__tests__/useCompleteTask.test.tsx | 2 +- .../useEmailThreadInCommandMenu.test.tsx | 2 +- .../favorites/hooks/__mocks__/useFavorites.ts | 4 +- .../hooks/__mocks__/useDeleteOneRecord.ts | 2 +- .../__mocks__/useFindDuplicateRecords.ts | 2 +- .../hooks/__mocks__/useFindOneRecord.ts | 2 +- .../hooks/__mocks__/useUpdateOneRecord.ts | 2 +- .../useDeleteOneRecordMutation.test.tsx | 2 +- .../useFindDuplicateRecordsQuery.test.tsx | 2 +- .../__tests__/useFindOneRecordQuery.test.tsx | 2 +- .../useUpdateOneRecordMutation.test.tsx | 2 +- .../hooks/useDeleteOneRecordMutation.ts | 4 +- .../hooks/useDestroyOneRecordMutation.ts | 4 +- .../hooks/useFindDuplicatesRecordsQuery.ts | 2 +- .../hooks/useFindOneRecordQuery.ts | 2 +- .../hooks/useUpdateOneRecordMutation.ts | 4 +- .../hooks/__tests__/usePersistField.test.tsx | 2 +- .../__tests__/useToggleEditOnlyInput.test.tsx | 2 +- .../postgres-error-codes.constants.ts | 263 ++++++++++++++++++ .../utils/postgres-exception.ts | 7 + ...nner-graphql-api-exception-handler.util.ts | 13 +- .../input/uuid-filter.input-type.ts | 18 ++ .../services/type-mapper.service.ts | 6 +- .../utils/__tests__/get-resolver-args.spec.ts | 7 +- .../utils/get-resolver-args.util.ts | 13 +- .../drivers/sentry.driver.ts | 20 +- .../search/dtos/object-record-filter-input.ts | 29 +- ...ow-trigger-graphql-api-exception.filter.ts | 42 +-- ...saging-import-exception-handler.service.ts | 100 ++++--- .../jobs/workflow-trigger.job.ts | 3 +- .../delete-one-operation-factory.util.ts | 2 +- .../destroy-one-operation-factory.util.ts | 2 +- .../restore-one-operation-factory.util.ts | 2 +- .../update-one-operation-factory.util.ts | 2 +- 38 files changed, 470 insertions(+), 133 deletions(-) create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/constants/postgres-error-codes.constants.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/postgres-exception.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/uuid-filter.input-type.ts diff --git a/.cursor/rules/react-state-management-guidelines.md b/.cursor/rules/react-state-management-guidelines.md index cb9909b688..5eb97b24a2 100644 --- a/.cursor/rules/react-state-management-guidelines.md +++ b/.cursor/rules/react-state-management-guidelines.md @@ -84,7 +84,7 @@ Twenty uses a combination of Recoil for global state and Apollo Client for serve `; export const GET_USER = gql` - query GetUser($id: ID!) { + query GetUser($id: UUID!) { user(id: $id) { ...UserFields } diff --git a/packages/twenty-e2e-testing/lib/requests/delete-workflow.ts b/packages/twenty-e2e-testing/lib/requests/delete-workflow.ts index 299768d672..a6fdab1aaa 100644 --- a/packages/twenty-e2e-testing/lib/requests/delete-workflow.ts +++ b/packages/twenty-e2e-testing/lib/requests/delete-workflow.ts @@ -19,7 +19,7 @@ export const deleteWorkflow = async ({ operationName: 'DeleteOneWorkflow', variables: { idToDelete: workflowId }, query: - 'mutation DeleteOneWorkflow($idToDelete: ID!) {\n deleteWorkflow(id: $idToDelete) {\n __typename\n deletedAt\n id\n }\n}', + 'mutation DeleteOneWorkflow($idToDelete: UUID!) {\n deleteWorkflow(id: $idToDelete) {\n __typename\n deletedAt\n id\n }\n}', }, }); }; diff --git a/packages/twenty-e2e-testing/lib/requests/destroy-workflow.ts b/packages/twenty-e2e-testing/lib/requests/destroy-workflow.ts index 2191a3e3dc..6e7b8c67f0 100644 --- a/packages/twenty-e2e-testing/lib/requests/destroy-workflow.ts +++ b/packages/twenty-e2e-testing/lib/requests/destroy-workflow.ts @@ -19,7 +19,7 @@ export const destroyWorkflow = async ({ operationName: 'DestroyOneWorkflow', variables: { idToDestroy: workflowId }, query: - 'mutation DestroyOneWorkflow($idToDestroy: ID!) {\n destroyWorkflow(id: $idToDestroy) {\n id\n __typename\n }\n}', + 'mutation DestroyOneWorkflow($idToDestroy: UUID!) {\n destroyWorkflow(id: $idToDestroy) {\n id\n __typename\n }\n}', }, }); }; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 3e458bdfd0..e663c087cb 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -736,17 +736,6 @@ export enum HealthIndicatorId { worker = 'worker' } -export type IdFilter = { - eq?: InputMaybe; - gt?: InputMaybe; - gte?: InputMaybe; - in?: InputMaybe>; - is?: InputMaybe; - lt?: InputMaybe; - lte?: InputMaybe; - neq?: InputMaybe; -}; - export enum IdentityProviderType { OIDC = 'OIDC', SAML = 'SAML' @@ -1414,7 +1403,7 @@ export type ObjectRecordFilterInput = { and?: InputMaybe>; createdAt?: InputMaybe; deletedAt?: InputMaybe; - id?: InputMaybe; + id?: InputMaybe; not?: InputMaybe; or?: InputMaybe>; updatedAt?: InputMaybe; @@ -2084,6 +2073,17 @@ export type TransientToken = { transientToken: AuthToken; }; +export type UuidFilter = { + eq?: InputMaybe; + gt?: InputMaybe; + gte?: InputMaybe; + in?: InputMaybe>; + is?: InputMaybe; + lt?: InputMaybe; + lte?: InputMaybe; + neq?: InputMaybe; +}; + export type UuidFilterComparison = { eq?: InputMaybe; gt?: InputMaybe; diff --git a/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx b/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx index af43385f55..0feeefc0d4 100644 --- a/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx @@ -27,7 +27,7 @@ const mocks: MockedResponse[] = [ { request: { query: gql` - mutation UpdateOneTask($idToUpdate: ID!, $input: TaskUpdateInput!) { + mutation UpdateOneTask($idToUpdate: UUID!, $input: TaskUpdateInput!) { updateTask(id: $idToUpdate, data: $input) { __typename assignee { diff --git a/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/__tests__/useEmailThreadInCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/__tests__/useEmailThreadInCommandMenu.test.tsx index 6052a4390b..5f491fc335 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/__tests__/useEmailThreadInCommandMenu.test.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/__tests__/useEmailThreadInCommandMenu.test.tsx @@ -12,7 +12,7 @@ const mocks = [ { request: { query: gql` - query FindOneMessageThread($objectRecordId: ID!) { + query FindOneMessageThread($objectRecordId: UUID!) { messageThread(filter: { id: { eq: $objectRecordId } }) { __typename id diff --git a/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts b/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts index 190fcfc12b..286c18226c 100644 --- a/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts +++ b/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts @@ -101,7 +101,7 @@ export const sortedFavorites = [ ]; const UPDATE_ONE_FAVORITE_MUTATION = gql` - mutation UpdateOneFavorite($idToUpdate: ID!, $input: FavoriteUpdateInput!) { + mutation UpdateOneFavorite($idToUpdate: UUID!, $input: FavoriteUpdateInput!) { updateFavorite(id: $idToUpdate, data: $input) { __typename company { @@ -859,7 +859,7 @@ export const mocks = [ { request: { query: gql` - mutation DeleteOneFavorite($idToDelete: ID!) { + mutation DeleteOneFavorite($idToDelete: UUID!) { deleteFavorite(id: $idToDelete) { __typename deletedAt diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useDeleteOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useDeleteOneRecord.ts index 8c042b8488..19314a79ca 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useDeleteOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useDeleteOneRecord.ts @@ -1,7 +1,7 @@ import { gql } from '@apollo/client'; export const query = gql` - mutation DeleteOnePerson($idToDelete: ID!) { + mutation DeleteOnePerson($idToDelete: UUID!) { deletePerson(id: $idToDelete) { __typename deletedAt diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindDuplicateRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindDuplicateRecords.ts index 929c016a38..ed071dcd49 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindDuplicateRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindDuplicateRecords.ts @@ -5,7 +5,7 @@ import { getPeopleRecordConnectionMock } from '~/testing/mock-data/people'; const peopleMock = getPeopleRecordConnectionMock(); export const query = gql` - query FindDuplicatePerson($ids: [ID!]!) { + query FindDuplicatePerson($ids: [UUID!]!) { personDuplicates(ids: $ids) { edges { node { diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindOneRecord.ts index 075dabb052..2178e1c3cf 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindOneRecord.ts @@ -4,7 +4,7 @@ import { PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS } from '@/object-record/hooks/ import { responseData as person } from './useUpdateOneRecord'; export const query = gql` - query FindOnePerson($objectRecordId: ID!) { + query FindOnePerson($objectRecordId: UUID!) { person(filter: { id: { eq: $objectRecordId } }) { ${PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS} } diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useUpdateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useUpdateOneRecord.ts index 3f1747cdfa..f902881d63 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useUpdateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useUpdateOneRecord.ts @@ -2,7 +2,7 @@ import { PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS } from '@/object-record/hooks/ import { gql } from '@apollo/client'; export const query = gql` - mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) { + mutation UpdateOnePerson($idToUpdate: UUID!, $input: PersonUpdateInput!) { updatePerson(id: $idToUpdate, data: $input) { ${PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS} } diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecordMutation.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecordMutation.test.tsx index 859355818a..d6709bfffd 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecordMutation.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecordMutation.test.tsx @@ -5,7 +5,7 @@ import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRe import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const expectedQueryTemplate = ` - mutation DeleteOnePerson($idToDelete: ID!) { + mutation DeleteOnePerson($idToDelete: UUID!) { deletePerson(id: $idToDelete) { __typename deletedAt diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindDuplicateRecordsQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindDuplicateRecordsQuery.test.tsx index 10cb78a400..9b26f399dc 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindDuplicateRecordsQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindDuplicateRecordsQuery.test.tsx @@ -6,7 +6,7 @@ import { useFindDuplicateRecordsQuery } from '@/object-record/hooks/useFindDupli import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const expectedQueryTemplate = ` - query FindDuplicatePerson($ids: [ID!]!) { + query FindDuplicatePerson($ids: [UUID!]!) { personDuplicates(ids: $ids) { edges { node { diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindOneRecordQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindOneRecordQuery.test.tsx index 32b2a16913..7f0349c0d4 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindOneRecordQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindOneRecordQuery.test.tsx @@ -6,7 +6,7 @@ import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQue import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const expectedQueryTemplate = ` -query FindOnePerson($objectRecordId: ID!) { +query FindOnePerson($objectRecordId: UUID!) { person(filter: { id: { eq: $objectRecordId } }) { ${PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS} } diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecordMutation.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecordMutation.test.tsx index be862743e9..7d7cb8ab94 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecordMutation.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecordMutation.test.tsx @@ -7,7 +7,7 @@ import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMeta import { normalizeGQLQuery } from '~/utils/normalizeGQLQuery'; const expectedQueryTemplate = ` -mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) { +mutation UpdateOnePerson($idToUpdate: UUID!, $input: PersonUpdateInput!) { updatePerson(id: $idToUpdate, data: $input) { ${PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS} } diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecordMutation.ts index 661334bbe5..c3d81e9cf3 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecordMutation.ts @@ -4,8 +4,8 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { mapSoftDeleteFieldsToGraphQLQuery } from '@/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery'; import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation'; import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField'; -import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { capitalize } from 'twenty-shared/utils'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useDeleteOneRecordMutation = ({ objectNameSingular, @@ -27,7 +27,7 @@ export const useDeleteOneRecordMutation = ({ ); const deleteOneRecordMutation = gql` - mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) { + mutation DeleteOne${capitalizedObjectName}($idToDelete: UUID!) { ${mutationResponseField}(id: $idToDelete) ${mapSoftDeleteFieldsToGraphQLQuery(objectMetadataItem)} } diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecordMutation.ts index a5637779ff..8e68d75fb4 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecordMutation.ts @@ -3,8 +3,8 @@ import gql from 'graphql-tag'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation'; import { getDestroyOneRecordMutationResponseField } from '@/object-record/utils/getDestroyOneRecordMutationResponseField'; -import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { capitalize } from 'twenty-shared/utils'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useDestroyOneRecordMutation = ({ objectNameSingular, @@ -26,7 +26,7 @@ export const useDestroyOneRecordMutation = ({ ); const destroyOneRecordMutation = gql` - mutation DestroyOne${capitalizedObjectName}($idToDestroy: ID!) { + mutation DestroyOne${capitalizedObjectName}($idToDestroy: UUID!) { ${mutationResponseField}(id: $idToDestroy) { id } diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts index 4664ef1263..61db5cd0f9 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts @@ -22,7 +22,7 @@ export const useFindDuplicateRecordsQuery = ({ const findDuplicateRecordsQuery = gql` query FindDuplicate${capitalize( objectMetadataItem.nameSingular, - )}($ids: [ID!]!) { + )}($ids: [UUID!]!) { ${getFindDuplicateRecordsQueryResponseField( objectMetadataItem.nameSingular, )}(ids: $ids) { diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecordQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecordQuery.ts index b4d65810e3..d4c052342b 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecordQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecordQuery.ts @@ -25,7 +25,7 @@ export const useFindOneRecordQuery = ({ const findOneRecordQuery = gql` query FindOne${capitalize( objectMetadataItem.nameSingular, - )}($objectRecordId: ID!) { + )}($objectRecordId: UUID!) { ${objectMetadataItem.nameSingular}(filter: { ${ withSoftDeleted diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordMutation.ts index e812c40239..dd829f6a29 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordMutation.ts @@ -8,8 +8,8 @@ import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField'; -import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { capitalize } from 'twenty-shared/utils'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useUpdateOneRecordMutation = ({ objectNameSingular, @@ -43,7 +43,7 @@ export const useUpdateOneRecordMutation = ({ ); const updateOneRecordMutation = gql` - mutation UpdateOne${capitalizedObjectName}($idToUpdate: ID!, $input: ${capitalizedObjectName}UpdateInput!) { + mutation UpdateOne${capitalizedObjectName}($idToUpdate: UUID!, $input: ${capitalizedObjectName}UpdateInput!) { ${mutationResponseField}(id: $idToUpdate, data: $input) ${mapObjectMetadataToGraphQLQuery( { objectMetadataItems, diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx index 07cfcf9bb7..6fde4fedc5 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx @@ -23,7 +23,7 @@ import { recordStoreFamilySelector } from '@/object-record/record-store/states/s import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const query = gql` - mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) { + mutation UpdateOnePerson($idToUpdate: UUID!, $input: PersonUpdateInput!) { updatePerson(id: $idToUpdate, data: $input) { ${PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS} } diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx index 2ea2a15350..318d80888e 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx @@ -22,7 +22,7 @@ const mocks: MockedResponse[] = [ request: { query: gql` mutation UpdateOneCompany( - $idToUpdate: ID! + $idToUpdate: UUID! $input: CompanyUpdateInput! ) { updateCompany(id: $idToUpdate, data: $input) { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/constants/postgres-error-codes.constants.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/constants/postgres-error-codes.constants.ts new file mode 100644 index 0000000000..af0e2fc0ef --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/constants/postgres-error-codes.constants.ts @@ -0,0 +1,263 @@ +// From https://www.postgresql.org/docs/current/errcodes-appendix.html +export const POSTGRESQL_ERROR_CODES = [ + '00000', // successful_completion + '01000', // warning + '0100C', // dynamic_result_sets_returned + '01008', // implicit_zero_bit_padding + '01003', // null_value_eliminated_in_set_function + '01007', // privilege_not_granted + '01006', // privilege_not_revoked + '01004', // string_data_right_truncation + '01P01', // deprecated_feature + '02000', // no_data + '02001', // no_additional_dynamic_result_sets_returned + '03000', // sql_statement_not_yet_complete + '08000', // connection_exception + '08003', // connection_does_not_exist + '08006', // connection_failure + '08001', // sqlclient_unable_to_establish_sqlconnection + '08004', // sqlserver_rejected_establishment_of_sqlconnection + '08007', // transaction_resolution_unknown + '08P01', // protocol_violation + '09000', // triggered_action_exception + '0A000', // feature_not_supported + '0B000', // invalid_transaction_initiation + '0F000', // locator_exception + '0F001', // invalid_locator_specification + '0L000', // invalid_grantor + '0LP01', // invalid_grant_operation + '0P000', // invalid_role_specification + '0Z000', // diagnostics_exception + '0Z002', // stacked_diagnostics_accessed_without_active_handler + '20000', // case_not_found + '21000', // cardinality_violation + '22000', // data_exception + '2202E', // array_subscript_error + '22021', // character_not_in_repertoire + '22008', // datetime_field_overflow + '22012', // division_by_zero + '22005', // error_in_assignment + '2200B', // escape_character_conflict + '22022', // indicator_overflow + '22015', // interval_field_overflow + '2201E', // invalid_argument_for_logarithm + '22014', // invalid_argument_for_ntile_function + '22016', // invalid_argument_for_nth_value_function + '2201F', // invalid_argument_for_power_function + '2201G', // invalid_argument_for_width_bucket_function + '22018', // invalid_character_value_for_cast + '22007', // invalid_datetime_format + '22019', // invalid_escape_character + '2200D', // invalid_escape_octet + '22025', // invalid_escape_sequence + '22P06', // nonstandard_use_of_escape_character + '22010', // invalid_indicator_parameter_value + '22023', // invalid_parameter_value + '22013', // invalid_preceding_or_following_size + '2201B', // invalid_regular_expression + '2201W', // invalid_row_count_in_limit_clause + '2201X', // invalid_row_count_in_result_offset_clause + '2202H', // invalid_tablesample_argument + '2202G', // invalid_tablesample_repeat + '22009', // invalid_time_zone_displacement_value + '2200C', // invalid_use_of_escape_character + '2200G', // most_specific_type_mismatch + '22004', // null_value_not_allowed + '22002', // null_value_no_indicator_parameter + '22003', // numeric_value_out_of_range + '2200H', // sequence_generator_limit_exceeded + '22026', // string_data_length_mismatch + '22001', // string_data_right_truncation + '22011', // substring_error + '22027', // trim_error + '22024', // unterminated_c_string + '2200F', // zero_length_character_string + '22P01', // floating_point_exception + '22P02', // invalid_text_representation + '22P03', // invalid_binary_representation + '22P04', // bad_copy_file_format + '22P05', // untranslatable_character + '2200L', // not_an_xml_document + '2200M', // invalid_xml_document + '2200N', // invalid_xml_content + '2200S', // invalid_xml_comment + '2200T', // invalid_xml_processing_instruction + '22030', // duplicate_json_object_key_value + '22031', // invalid_argument_for_sql_json_datetime_function + '22032', // invalid_json_text + '22033', // invalid_sql_json_subscript + '22034', // more_than_one_sql_json_item + '22035', // no_sql_json_item + '22036', // non_numeric_sql_json_item + '22037', // non_unique_keys_in_a_json_object + '22038', // singleton_sql_json_item_required + '22039', // sql_json_array_not_found + '2203A', // sql_json_member_not_found + '2203B', // sql_json_number_not_found + '2203C', // sql_json_object_not_found + '2203D', // too_many_json_array_elements + '2203E', // too_many_json_object_members + '2203F', // sql_json_scalar_required + '2203G', // sql_json_item_cannot_be_cast_to_target_type + '23000', // integrity_constraint_violation + '23001', // restrict_violation + '23502', // not_null_violation + '23503', // foreign_key_violation + '23505', // unique_violation + '23514', // check_violation + '23P01', // exclusion_violation + '24000', // invalid_cursor_state + '25000', // invalid_transaction_state + '25001', // active_sql_transaction + '25002', // branch_transaction_already_active + '25008', // held_cursor_requires_same_isolation_level + '25003', // inappropriate_access_mode_for_branch_transaction + '25004', // inappropriate_isolation_level_for_branch_transaction + '25005', // no_active_sql_transaction_for_branch_transaction + '25006', // read_only_sql_transaction + '25007', // schema_and_data_statement_mixing_not_supported + '25P01', // no_active_sql_transaction + '25P02', // in_failed_sql_transaction + '25P03', // idle_in_transaction_session_timeout + '25P04', // transaction_timeout + '26000', // invalid_sql_statement_name + '27000', // triggered_data_change_violation + '28000', // invalid_authorization_specification + '28P01', // invalid_password + '2B000', // dependent_privilege_descriptors_still_exist + '2BP01', // dependent_objects_still_exist + '2D000', // invalid_transaction_termination + '2F000', // sql_routine_exception + '2F005', // function_executed_no_return_statement + '2F002', // modifying_sql_data_not_permitted + '2F003', // prohibited_sql_statement_attempted + '2F004', // reading_sql_data_not_permitted + '34000', // invalid_cursor_name + '38000', // external_routine_exception + '38001', // containing_sql_not_permitted + '38002', // modifying_sql_data_not_permitted + '38003', // prohibited_sql_statement_attempted + '38004', // reading_sql_data_not_permitted + '39000', // external_routine_invocation_exception + '39001', // invalid_sqlstate_returned + '39004', // null_value_not_allowed + '39P01', // trigger_protocol_violated + '39P02', // srf_protocol_violated + '39P03', // event_trigger_protocol_violated + '3B000', // savepoint_exception + '3B001', // invalid_savepoint_specification + '3D000', // invalid_catalog_name + '3F000', // invalid_schema_name + '40000', // transaction_rollback + '40002', // transaction_integrity_constraint_violation + '40001', // serialization_failure + '40003', // statement_completion_unknown + '40P01', // deadlock_detected + '42000', // syntax_error_or_access_rule_violation + '42601', // syntax_error + '42501', // insufficient_privilege + '42846', // cannot_coerce + '42803', // grouping_error + '42P20', // windowing_error + '42P19', // invalid_recursion + '42830', // invalid_foreign_key + '42602', // invalid_name + '42622', // name_too_long + '42939', // reserved_name + '42804', // datatype_mismatch + '42P18', // indeterminate_datatype + '42P21', // collation_mismatch + '42P22', // indeterminate_collation + '42809', // wrong_object_type + '428C9', // generated_always + '42703', // undefined_column + '42883', // undefined_function + '42P01', // undefined_table + '42P02', // undefined_parameter + '42704', // undefined_object + '42701', // duplicate_column + '42P03', // duplicate_cursor + '42P04', // duplicate_database + '42723', // duplicate_function + '42P05', // duplicate_prepared_statement + '42P06', // duplicate_schema + '42P07', // duplicate_table + '42712', // duplicate_alias + '42710', // duplicate_object + '42702', // ambiguous_column + '42725', // ambiguous_function + '42P08', // ambiguous_parameter + '42P09', // ambiguous_alias + '42P10', // invalid_column_reference + '42611', // invalid_column_definition + '42P11', // invalid_cursor_definition + '42P12', // invalid_database_definition + '42P13', // invalid_function_definition + '42P14', // invalid_prepared_statement_definition + '42P15', // invalid_schema_definition + '42P16', // invalid_table_definition + '42P17', // invalid_object_definition + '44000', // with_check_option_violation + '53000', // insufficient_resources + '53100', // disk_full + '53200', // out_of_memory + '53300', // too_many_connections + '53400', // configuration_limit_exceeded + '54000', // program_limit_exceeded + '54001', // statement_too_complex + '54011', // too_many_columns + '54023', // too_many_arguments + '55000', // object_not_in_prerequisite_state + '55006', // object_in_use + '55P02', // cant_change_runtime_param + '55P03', // lock_not_available + '55P04', // unsafe_new_enum_value_usage + '57000', // operator_intervention + '57014', // query_canceled + '57P01', // admin_shutdown + '57P02', // crash_shutdown + '57P03', // cannot_connect_now + '57P04', // database_dropped + '57P05', // idle_session_timeout + '58000', // system_error + '58030', // io_error + '58P01', // undefined_file + '58P02', // duplicate_file + 'F0000', // config_file_error + 'F0001', // lock_file_exists + 'HV000', // fdw_error + 'HV005', // fdw_column_name_not_found + 'HV002', // fdw_dynamic_parameter_value_needed + 'HV010', // fdw_function_sequence_error + 'HV021', // fdw_inconsistent_descriptor_information + 'HV024', // fdw_invalid_attribute_value + 'HV007', // fdw_invalid_column_name + 'HV008', // fdw_invalid_column_number + 'HV004', // fdw_invalid_data_type + 'HV006', // fdw_invalid_data_type_descriptors + 'HV091', // fdw_invalid_descriptor_field_identifier + 'HV00B', // fdw_invalid_handle + 'HV00C', // fdw_invalid_option_index + 'HV00D', // fdw_invalid_option_name + 'HV090', // fdw_invalid_string_length_or_buffer_length + 'HV00A', // fdw_invalid_string_format + 'HV009', // fdw_invalid_use_of_null_pointer + 'HV014', // fdw_too_many_handles + 'HV001', // fdw_out_of_memory + 'HV00P', // fdw_no_schemas + 'HV00J', // fdw_option_name_not_found + 'HV00K', // fdw_reply_handle + 'HV00Q', // fdw_schema_not_found + 'HV00R', // fdw_table_not_found + 'HV00L', // fdw_unable_to_create_execution + 'HV00M', // fdw_unable_to_create_reply + 'HV00N', // fdw_unable_to_establish_connection + 'P0000', // plpgsql_error + 'P0001', // raise_exception + 'P0002', // no_data_found + 'P0003', // too_many_rows + 'P0004', // assert_failure + 'XX000', // internal_error + 'XX001', // data_corrupted + 'XX002', // index_corrupted +]; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/postgres-exception.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/postgres-exception.ts new file mode 100644 index 0000000000..2e53a237b8 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/postgres-exception.ts @@ -0,0 +1,7 @@ +export class PostgresException extends Error { + readonly code: string; + constructor(message: string, code: string) { + super(message); + this.code = code; + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts index 606a7f48d0..703ded2015 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts @@ -3,8 +3,10 @@ import { QueryFailedError } from 'typeorm'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +import { POSTGRESQL_ERROR_CODES } from 'src/engine/api/graphql/workspace-query-runner/constants/postgres-error-codes.constants'; import { graphqlQueryRunnerExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/graphql-query-runner-exception-handler.util'; import { handleDuplicateKeyError } from 'src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util'; +import { PostgresException } from 'src/engine/api/graphql/workspace-query-runner/utils/postgres-exception'; import { workspaceExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-exception-handler.util'; import { WorkspaceQueryRunnerException } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; import { RecordTransformerException } from 'src/engine/core-modules/record-transformer/record-transformer.exception'; @@ -12,8 +14,12 @@ import { recordTransformerGraphqlApiExceptionHandler } from 'src/engine/core-mod import { PermissionsException } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { permissionGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util'; +interface QueryFailedErrorWithCode extends QueryFailedError { + code: string; +} + export const workspaceQueryRunnerGraphqlApiExceptionHandler = ( - error: Error, + error: QueryFailedErrorWithCode, context: WorkspaceQueryRunnerOptions, ) => { switch (true) { @@ -23,6 +29,11 @@ export const workspaceQueryRunnerGraphqlApiExceptionHandler = ( ) { return handleDuplicateKeyError(error, context); } + const errorCode = (error as QueryFailedErrorWithCode).code; + + if (POSTGRESQL_ERROR_CODES.includes(errorCode)) { + throw new PostgresException(error.message, errorCode); + } throw error; } case error instanceof RecordTransformerException: diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/uuid-filter.input-type.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/uuid-filter.input-type.ts new file mode 100644 index 0000000000..93fb62916d --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/uuid-filter.input-type.ts @@ -0,0 +1,18 @@ +import { GraphQLInputObjectType, GraphQLList } from 'graphql'; + +import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type'; +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +export const UUIDFilterType = new GraphQLInputObjectType({ + name: 'UUIDFilter', + fields: { + eq: { type: UUIDScalarType }, + gt: { type: UUIDScalarType }, + gte: { type: UUIDScalarType }, + in: { type: new GraphQLList(UUIDScalarType) }, + lt: { type: UUIDScalarType }, + lte: { type: UUIDScalarType }, + neq: { type: UUIDScalarType }, + is: { type: FilterIs }, + }, +}); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts index e852a272fc..370f2d0387 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts @@ -27,10 +27,10 @@ import { RawJsonFilterType, StringFilterType, } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input'; -import { IDFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/id-filter.input-type'; import { MultiSelectFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/multi-select-filter.input-type'; import { RichTextV2FilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/rich-text.input-type'; import { SelectFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/select-filter.input-type'; +import { UUIDFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/uuid-filter.input-type'; import { BigFloatScalarType, UUIDScalarType, @@ -95,14 +95,14 @@ export class TypeMapperService { isIdField?: boolean, ): GraphQLInputObjectType | GraphQLScalarType | undefined { if (isIdField || fieldMetadataType === FieldMetadataType.RELATION) { - return IDFilterType; + return UUIDFilterType; } const typeFilterMapping = new Map< FieldMetadataType, GraphQLInputObjectType | GraphQLScalarType >([ - [FieldMetadataType.UUID, IDFilterType], + [FieldMetadataType.UUID, UUIDFilterType], [FieldMetadataType.TEXT, StringFilterType], [FieldMetadataType.DATE_TIME, DateFilterType], [FieldMetadataType.DATE, DateFilterType], diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/__tests__/get-resolver-args.spec.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/__tests__/get-resolver-args.spec.ts index 3f47f10797..89e07c1c5b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/__tests__/get-resolver-args.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/__tests__/get-resolver-args.spec.ts @@ -1,8 +1,9 @@ -import { GraphQLBoolean, GraphQLID, GraphQLInt, GraphQLString } from 'graphql'; +import { GraphQLBoolean, GraphQLInt, GraphQLString } from 'graphql'; import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory'; +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util'; describe('getResolverArgs', () => { @@ -44,11 +45,11 @@ describe('getResolverArgs', () => { }, }, updateOne: { - id: { type: GraphQLID, isNullable: false }, + id: { type: UUIDScalarType, isNullable: false }, data: { kind: InputTypeDefinitionKind.Update, isNullable: false }, }, deleteOne: { - id: { type: GraphQLID, isNullable: false }, + id: { type: UUIDScalarType, isNullable: false }, }, restoreMany: { filter: { kind: InputTypeDefinitionKind.Filter, isNullable: false }, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts index 421c68346b..520435b6ae 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts @@ -1,9 +1,10 @@ -import { GraphQLBoolean, GraphQLID, GraphQLInt, GraphQLString } from 'graphql'; +import { GraphQLBoolean, GraphQLInt, GraphQLString } from 'graphql'; import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { ArgMetadata } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/param-metadata.interface'; import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory'; +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; export const getResolverArgs = ( type: WorkspaceResolverBuilderMethodNames, @@ -77,7 +78,7 @@ export const getResolverArgs = ( case 'updateOne': return { id: { - type: GraphQLID, + type: UUIDScalarType, isNullable: false, }, data: { @@ -88,7 +89,7 @@ export const getResolverArgs = ( case 'findDuplicates': return { ids: { - type: GraphQLID, + type: UUIDScalarType, isNullable: true, isArray: true, }, @@ -101,14 +102,14 @@ export const getResolverArgs = ( case 'deleteOne': return { id: { - type: GraphQLID, + type: UUIDScalarType, isNullable: false, }, }; case 'destroyOne': return { id: { - type: GraphQLID, + type: UUIDScalarType, isNullable: false, }, }; @@ -133,7 +134,7 @@ export const getResolverArgs = ( case 'restoreOne': return { id: { - type: GraphQLID, + type: UUIDScalarType, isNullable: false, }, }; diff --git a/packages/twenty-server/src/engine/core-modules/exception-handler/drivers/sentry.driver.ts b/packages/twenty-server/src/engine/core-modules/exception-handler/drivers/sentry.driver.ts index ddb1875e02..e835262743 100644 --- a/packages/twenty-server/src/engine/core-modules/exception-handler/drivers/sentry.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/exception-handler/drivers/sentry.driver.ts @@ -1,7 +1,9 @@ import * as Sentry from '@sentry/node'; +import { isDefined } from 'twenty-shared/utils'; import { ExceptionHandlerOptions } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface'; +import { PostgresException } from 'src/engine/api/graphql/workspace-query-runner/utils/postgres-exception'; import { ExceptionHandlerDriverInterface } from 'src/engine/core-modules/exception-handler/interfaces'; import { CustomException } from 'src/utils/custom-exception'; @@ -55,7 +57,10 @@ export class ExceptionHandlerSentryDriver }); } - if (exception instanceof CustomException) { + if ( + exception instanceof CustomException && + exception.code !== 'UNKNOWN' + ) { scope.setTag('customExceptionCode', exception.code); scope.setFingerprint([exception.code]); exception.name = exception.code @@ -67,6 +72,19 @@ export class ExceptionHandlerSentryDriver .join(' '); } + if (exception instanceof PostgresException) { + scope.setTag('postgresSqlErrorCode', exception.code); + const fingerPrint = [exception.code]; + const genericOperationName = // truncates to first word: FindOnePerson -> Find, AggregateCompanies -> Aggregate, ... + options?.operation?.name?.match(/^[A-Z][a-z]*/)?.[0]; + + if (isDefined(genericOperationName)) { + fingerPrint.push(genericOperationName); + } + scope.setFingerprint(fingerPrint); + exception.name = exception.message; + } + const eventId = Sentry.captureException(exception, { contexts: { GraphQL: { diff --git a/packages/twenty-server/src/engine/core-modules/search/dtos/object-record-filter-input.ts b/packages/twenty-server/src/engine/core-modules/search/dtos/object-record-filter-input.ts index 54ec704718..feb503455b 100644 --- a/packages/twenty-server/src/engine/core-modules/search/dtos/object-record-filter-input.ts +++ b/packages/twenty-server/src/engine/core-modules/search/dtos/object-record-filter-input.ts @@ -1,10 +1,13 @@ -import { Field, ID, InputType, registerEnumType } from '@nestjs/graphql'; +import { Field, InputType, registerEnumType } from '@nestjs/graphql'; import { IsArray, IsOptional } from 'class-validator'; import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { DateScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { + DateScalarType, + UUIDScalarType, +} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @InputType() export class ObjectRecordFilterInput implements Partial { @@ -22,9 +25,9 @@ export class ObjectRecordFilterInput implements Partial { @IsArray() or?: ObjectRecordFilterInput[]; - @Field(() => IDFilterType, { nullable: true }) + @Field(() => UUIDFilterType, { nullable: true }) @IsOptional() - id?: IDFilterType | null; + id?: UUIDFilterType | null; @Field(() => DateFilterType, { nullable: true }) createdAt?: DateFilterType | null; @@ -36,33 +39,33 @@ export class ObjectRecordFilterInput implements Partial { deletedAt?: DateFilterType | null; } -@InputType('IDFilter') -class IDFilterType { - @Field(() => ID, { nullable: true }) +@InputType('UUIDFilter') +class UUIDFilterType { + @Field(() => UUIDScalarType, { nullable: true }) @IsOptional() eq?: string; - @Field(() => ID, { nullable: true }) + @Field(() => UUIDScalarType, { nullable: true }) @IsOptional() gt?: string; - @Field(() => ID, { nullable: true }) + @Field(() => UUIDScalarType, { nullable: true }) @IsOptional() gte?: string; - @Field(() => [ID], { nullable: true }) + @Field(() => [UUIDScalarType], { nullable: true }) @IsOptional() in?: string[]; - @Field(() => ID, { nullable: true }) + @Field(() => UUIDScalarType, { nullable: true }) @IsOptional() lt?: string; - @Field(() => ID, { nullable: true }) + @Field(() => UUIDScalarType, { nullable: true }) @IsOptional() lte?: string; - @Field(() => ID, { nullable: true }) + @Field(() => UUIDScalarType, { nullable: true }) @IsOptional() neq?: string; diff --git a/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter.ts index 508aff9fec..83531aede4 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter.ts @@ -9,28 +9,34 @@ import { WorkflowTriggerExceptionCode, } from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception'; +export const handleWorkflowTriggerException = ( + exception: WorkflowTriggerException, +) => { + switch (exception.code) { + case WorkflowTriggerExceptionCode.INVALID_INPUT: + case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION: + case WorkflowTriggerExceptionCode.INVALID_ACTION_TYPE: + case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER: + case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS: + case WorkflowTriggerExceptionCode.FORBIDDEN: + throw new UserInputError(exception.message); + case WorkflowTriggerExceptionCode.NOT_FOUND: + throw new NotFoundError(exception.message); + case WorkflowTriggerExceptionCode.INTERNAL_ERROR: + throw exception; + default: { + const _exhaustiveCheck: never = exception.code; + + throw exception; + } + } +}; + @Catch(WorkflowTriggerException) export class WorkflowTriggerGraphqlApiExceptionFilter implements ExceptionFilter { catch(exception: WorkflowTriggerException) { - switch (exception.code) { - case WorkflowTriggerExceptionCode.INVALID_INPUT: - case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION: - case WorkflowTriggerExceptionCode.INVALID_ACTION_TYPE: - case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER: - case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS: - case WorkflowTriggerExceptionCode.FORBIDDEN: - throw new UserInputError(exception.message); - case WorkflowTriggerExceptionCode.NOT_FOUND: - throw new NotFoundError(exception.message); - case WorkflowTriggerExceptionCode.INTERNAL_ERROR: - throw exception; - default: { - const _exhaustiveCheck: never = exception.code; - - throw exception; - } - } + handleWorkflowTriggerException(exception); } } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts index 09fb65fa29..2665087152 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts @@ -1,5 +1,7 @@ import { Injectable } from '@nestjs/common'; +import { isDefined } from 'class-validator'; + import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; @@ -35,7 +37,7 @@ export class MessageImportExceptionHandlerService { ) {} public async handleDriverException( - exception: MessageImportDriverException, + exception: MessageImportDriverException | Error, syncStep: MessageImportSyncStep, messageChannel: Pick< MessageChannelWorkspaceEntity, @@ -43,49 +45,53 @@ export class MessageImportExceptionHandlerService { >, workspaceId: string, ): Promise { - switch (exception.code) { - case MessageImportDriverExceptionCode.NOT_FOUND: - await this.handleNotFoundException( - syncStep, - messageChannel, - workspaceId, - ); - break; - case MessageImportDriverExceptionCode.TEMPORARY_ERROR: - case MessageNetworkExceptionCode.ECONNABORTED: - case MessageNetworkExceptionCode.ENOTFOUND: - case MessageNetworkExceptionCode.ECONNRESET: - case MessageNetworkExceptionCode.ETIMEDOUT: - case MessageNetworkExceptionCode.ERR_NETWORK: - await this.handleTemporaryException( - syncStep, - messageChannel, - workspaceId, - exception, - ); - break; - case MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS: - await this.handleInsufficientPermissionsException( - messageChannel, - workspaceId, - ); - break; - case MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR: - await this.handlePermanentException( - exception, - messageChannel, - workspaceId, - ); - break; - case MessageImportDriverExceptionCode.UNKNOWN: - case MessageImportDriverExceptionCode.UNKNOWN_NETWORK_ERROR: - default: - await this.handleUnknownException( - exception, - messageChannel, - workspaceId, - ); - break; + if (exception instanceof MessageImportDriverException) { + switch (exception.code) { + case MessageImportDriverExceptionCode.NOT_FOUND: + await this.handleNotFoundException( + syncStep, + messageChannel, + workspaceId, + ); + break; + case MessageImportDriverExceptionCode.TEMPORARY_ERROR: + case MessageNetworkExceptionCode.ECONNABORTED: + case MessageNetworkExceptionCode.ENOTFOUND: + case MessageNetworkExceptionCode.ECONNRESET: + case MessageNetworkExceptionCode.ETIMEDOUT: + case MessageNetworkExceptionCode.ERR_NETWORK: + await this.handleTemporaryException( + syncStep, + messageChannel, + workspaceId, + exception, + ); + break; + case MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS: + await this.handleInsufficientPermissionsException( + messageChannel, + workspaceId, + ); + break; + case MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR: + await this.handlePermanentException( + exception, + messageChannel, + workspaceId, + ); + break; + case MessageImportDriverExceptionCode.UNKNOWN: + case MessageImportDriverExceptionCode.UNKNOWN_NETWORK_ERROR: + default: + await this.handleUnknownException( + exception, + messageChannel, + workspaceId, + ); + break; + } + } else { + await this.handleUnknownException(exception, messageChannel, workspaceId); } } @@ -172,7 +178,7 @@ export class MessageImportExceptionHandlerService { } private async handleUnknownException( - exception: MessageImportDriverException, + exception: MessageImportDriverException | Error, messageChannel: Pick, workspaceId: string, ): Promise { @@ -183,7 +189,9 @@ export class MessageImportExceptionHandlerService { ); const messageImportException = new MessageImportException( - exception.message, + isDefined(exception.name) + ? `${exception.name}: ${exception.message}` + : exception.message, MessageImportExceptionCode.UNKNOWN, ); diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts index 6d24cd0d3d..8162469313 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts @@ -8,6 +8,7 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; +import { handleWorkflowTriggerException } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter'; import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { @@ -106,7 +107,7 @@ export class WorkflowTriggerJob { jobName: WorkflowTriggerJob.name, jobId: data.workflowId, }); - throw e; + handleWorkflowTriggerException(e); } } } diff --git a/packages/twenty-server/test/integration/graphql/utils/delete-one-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/delete-one-operation-factory.util.ts index 0a7102c76f..ea26bc10c7 100644 --- a/packages/twenty-server/test/integration/graphql/utils/delete-one-operation-factory.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/delete-one-operation-factory.util.ts @@ -13,7 +13,7 @@ export const deleteOneOperationFactory = ({ recordId, }: DeleteOneOperationFactoryParams) => ({ query: gql` - mutation Delete${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: ID!) { + mutation Delete${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: UUID!) { delete${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id) { ${gqlFields} } diff --git a/packages/twenty-server/test/integration/graphql/utils/destroy-one-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/destroy-one-operation-factory.util.ts index 38b448bc94..2fafbaca9b 100644 --- a/packages/twenty-server/test/integration/graphql/utils/destroy-one-operation-factory.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/destroy-one-operation-factory.util.ts @@ -13,7 +13,7 @@ export const destroyOneOperationFactory = ({ recordId, }: DestroyOneOperationFactoryParams) => ({ query: gql` - mutation Destroy${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: ID!) { + mutation Destroy${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: UUID!) { destroy${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id) { ${gqlFields} } diff --git a/packages/twenty-server/test/integration/graphql/utils/restore-one-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/restore-one-operation-factory.util.ts index 60282f45f2..a824e20850 100644 --- a/packages/twenty-server/test/integration/graphql/utils/restore-one-operation-factory.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/restore-one-operation-factory.util.ts @@ -13,7 +13,7 @@ export const restoreOneOperationFactory = ({ recordId, }: RestoreOneOperationFactoryParams) => ({ query: gql` - mutation Restore${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: ID!) { + mutation Restore${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: UUID!) { restore${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id) { ${gqlFields} } diff --git a/packages/twenty-server/test/integration/graphql/utils/update-one-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/update-one-operation-factory.util.ts index bf0f8ea0b4..dffaec05b0 100644 --- a/packages/twenty-server/test/integration/graphql/utils/update-one-operation-factory.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/update-one-operation-factory.util.ts @@ -15,7 +15,7 @@ export const updateOneOperationFactory = ({ recordId, }: UpdateOneOperationFactoryParams) => ({ query: gql` - mutation Update${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: ID, $data: ${capitalize(objectMetadataSingularName)}UpdateInput) { + mutation Update${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: UUID, $data: ${capitalize(objectMetadataSingularName)}UpdateInput) { update${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id, data: $data) { ${gqlFields} }