From 19c9f957b12b4807a6cfb9c4b87f42bed1e4a49d Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:08:43 +0100 Subject: [PATCH] Improve userFriendlyMessage devX (#16815) Two challenges with error messages - always provide a useful/meaningful error message for the end user instead of the generic one. eg: show "Wrong password" and not "An error occured" - avoid technical details unless error regards a technical feature. eg: show "An error occured" and not "Invalid post-hook payload."; but do show "Invalid issuer URL." as it occurs while configuring SSO What this PR does - Make userFriendlyMessage mandatory for widely used GraphqlQueryRunnerException and CommonQueryRunnerException, so that developers are forced to ask themselves what the error message should be, and as it contains very wide error codes (eg: "Bad request") which should not be mapped to just one default message - Keep userFriendlyMessage optional for service-specific exceptions (eg: workflowStepExecutorException), but convert the error code to userFriendlyMessage mapper to a switch case function with a typecheck ensuring that all codes are mapped to a message. These default messages are still overridable where they are thrown. --- .../common-selected-fields.handler.ts | 2 + .../data-arg-processor/data-arg.processor.ts | 7 + .../validate-actor-field-or-throw.util.ts | 2 + .../validate-address-field-or-throw.util.ts | 2 + .../validate-array-field-or-throw.util.ts | 6 +- .../validate-boolean-field-or-throw.util.ts | 7 +- .../validate-currency-field-or-throw.util.ts | 2 + ...-date-and-date-time-field-or-throw.util.ts | 6 +- .../validate-emails-field-or-throw.util.ts | 2 + .../validate-full-name-field-or-throw.util.ts | 2 + .../validate-links-field-or-throw.util.ts | 2 + ...lidate-multi-select-field-or-throw.util.ts | 10 +- .../validate-number-field-or-throw.util.ts | 11 +- ...overridden-position-field-or-throw.util.ts | 12 +- .../validate-phones-field-or-throw.util.ts | 2 + ...e-rating-and-select-field-or-throw.util.ts | 10 +- .../validate-raw-json-field-or-throw.util.ts | 7 +- ...lidate-rich-text-v2-field-or-throw.util.ts | 2 + .../validate-text-field-or-throw.util.ts | 9 +- .../validate-uuid-field-or-throw.util.ts | 9 +- .../common-base-query-runner.service.ts | 8 +- ...common-create-many-query-runner.service.ts | 2 + ...common-delete-many-query-runner.service.ts | 2 + .../common-delete-one-query-runner.service.ts | 4 + ...ommon-destroy-many-query-runner.service.ts | 2 + ...common-destroy-one-query-runner.service.ts | 6 + ...on-find-duplicates-query-runner.service.ts | 4 + .../common-find-many-query-runner.service.ts | 7 + .../common-find-one-query-runner.service.ts | 6 + .../common-group-by-query-runner.service.ts | 3 + .../common-merge-many-query-runner.service.ts | 15 ++ ...ommon-restore-many-query-runner.service.ts | 2 + ...common-restore-one-query-runner.service.ts | 4 + ...common-update-many-query-runner.service.ts | 2 + .../common-update-one-query-runner.service.ts | 4 + .../errors/common-query-runner.exception.ts | 28 +--- .../errors/standard-error-message.constant.ts | 3 + .../errors/graphql-query-runner.exception.ts | 31 +--- .../graphql-query-filter-field.parser.ts | 3 + .../graphql-query-order.parser.ts | 2 + .../convert-order-by-to-find-options-order.ts | 2 + .../utils/get-group-by-expression.util.ts | 2 + .../parse-group-by-relation-field.util.ts | 3 + ...e-single-key-for-group-by-or-throw.util.ts | 2 + ...ct-records-to-graphql-connection.helper.ts | 2 + .../process-nested-relations-v2.helper.ts | 4 + .../utils/compute-where-condition-parts.ts | 2 + .../utils/cursors.util.ts | 2 + .../utils/get-target-object-metadata.util.ts | 3 + .../utils/assert-is-valid-uuid.util.ts | 2 + .../workspace-query-hook.explorer.ts | 2 + .../workspace-query-runner.exception.ts | 36 +++-- ...arse-aggregate-fields-rest-request.util.ts | 3 + .../parse-depth-rest-request.util.ts | 2 + .../check-filter-query.util.ts | 3 + .../filter-parser-utils/parse-filter.util.ts | 3 + .../rest-input-request-parser.exception.ts | 35 +++-- ...r-composite-field-where-condition.utils.ts | 3 + .../build-cursor-where-condition.utils.ts | 3 + .../utils/validate-and-get-order-by.utils.ts | 5 + .../created-by.create-many.pre-query-hook.ts | 2 + .../created-by.create-one.pre-query-hook.ts | 2 + .../updated-by.update-many.pre-query-hook.ts | 2 + .../updated-by.update-one.pre-query-hook.ts | 2 + .../api-key/exceptions/api-key.exception.ts | 27 ++-- .../application/application.exception.ts | 32 ++-- .../application-variable.exception.ts | 17 ++- .../approved-access-domain.exception.ts | 35 +++-- .../core-modules/audit/audit.exception.ts | 18 ++- .../core-modules/auth/auth.exception.ts | 113 ++++++++------ .../auth/services/auth.service.ts | 13 +- .../auth/strategies/jwt.auth.strategy.spec.ts | 24 +-- .../core-modules/billing/billing.exception.ts | 87 +++++++---- .../core-modules/captcha/captcha.exception.ts | 15 +- .../email-verification.exception.ts | 36 +++-- .../emailing-domain-driver.exception.ts | 29 ++-- .../feature-flag/feature-flag.exception.ts | 17 ++- .../interfaces/file-storage-exception.ts | 17 ++- .../core-modules/file/file.exception.ts | 24 ++- .../public-domain/public-domain.exception.ts | 24 ++- .../exceptions/record-crud.exception.ts | 41 +++-- .../record-transformer.exception.ts | 35 +++-- .../search/exceptions/search.exception.ts | 18 ++- .../engine/core-modules/sso/sso.exception.ts | 30 ++-- .../throttler/throttler.exception.ts | 17 ++- .../exceptions/send-email-tool.exception.ts | 33 +++-- .../twenty-config/twenty-config.exception.ts | 32 ++-- .../two-factor-authentication.exception.ts | 29 ++-- .../user-workspace.exception.ts | 18 ++- .../core-modules/user/user.exception.ts | 24 +-- .../core-modules/webhook/webhook.exception.ts | 18 ++- .../workspace-invitation.exception.ts | 30 ++-- .../workspace/workspace.exception.ts | 38 +++-- .../ai/ai-agent/agent.exception.ts | 39 +++-- .../exceptions/cron-trigger.exception.ts | 23 ++- .../data-source/data-source.exception.ts | 19 ++- .../database-event-trigger.exception.ts | 26 ++-- .../field-metadata.exception.ts | 55 ++++--- .../exceptions/flat-entity-maps.exception.ts | 25 ++-- .../object-metadata.exception.ts | 39 +++-- .../exceptions/page-layout-tab.exception.ts | 20 ++- .../page-layout-widget.exception.ts | 19 ++- .../exceptions/page-layout.exception.ts | 22 ++- .../permissions/permissions.exception.ts | 140 ++++++++++++------ .../remote-server/remote-server.exception.ts | 30 ++-- .../distant-table/distant-table.exception.ts | 21 ++- .../foreign-table/foreign-table.exception.ts | 21 ++- .../remote-table/remote-table.exception.ts | 32 ++-- .../role/exceptions/role-target.exception.ts | 29 ++-- .../exceptions/route-trigger.exception.ts | 39 +++-- .../serverless-function.exception.ts | 41 +++-- .../exceptions/invalid-metadata.exception.ts | 41 +++-- .../view/exceptions/flat-view.exception.ts | 24 ++- .../workspace-metadata-version.exception.ts | 17 ++- .../workspace-migration.exception.ts | 29 ++-- .../exceptions/relation.exception.ts | 27 ++-- .../exceptions/twenty-orm.exception.ts | 71 ++++++--- .../workspace-schema-manager.exception.ts | 21 ++- .../exceptions/workspace-cache.exception.ts | 23 ++- .../exceptions/workspace-cleaner.exception.ts | 17 ++- .../workspace-migration-runner.exception.ts | 38 +++-- .../calendar-event-import-driver.exception.ts | 38 +++-- .../calendar-event-import.exception.ts | 20 ++- ...nected-account-refresh-tokens.exception.ts | 29 ++-- .../exceptions/dashboard.exception.ts | 22 ++- .../exceptions/workflow-common.exception.ts | 17 ++- .../workflow-query-validation.exception.ts | 17 ++- .../workflow-version-edge.exception.ts | 20 ++- .../workflow-version-step.exception.ts | 26 ++-- .../workflow-step-executor.exception.ts | 27 ++-- .../exceptions/workflow-run.exception.ts | 32 ++-- .../exceptions/workflow-trigger.exception.ts | 39 +++-- ...space-member-delete-many.pre-query.hook.ts | 5 + ...kspace-member-delete-one.pre-query.hook.ts | 5 + ...rs-side-effect-v2.integration-spec.ts.snap | 4 +- 135 files changed, 1672 insertions(+), 845 deletions(-) create mode 100644 packages/twenty-server/src/engine/api/common/common-query-runners/errors/standard-error-message.constant.ts diff --git a/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts b/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts index 2577034a8d..950ba0f364 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts @@ -7,6 +7,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type'; import { getAllSelectableFields } from 'src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util'; import { MAX_DEPTH } from 'src/engine/api/rest/input-request-parsers/constants/max-depth.constant'; @@ -106,6 +107,7 @@ export class CommonSelectedFieldsHandler { throw new CommonQueryRunnerException( `Object metadata relation target not found for relation creation payload`, CommonQueryRunnerExceptionCode.BAD_REQUEST, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } const relationFieldSelectFields = getAllSelectableFields({ diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts index 0ab2dcf9ff..b9c0529906 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { msg } from '@lingui/core/macro'; import { isNull, isUndefined } from '@sniptt/guards'; import { FieldMetadataRelationSettings, @@ -44,6 +45,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; import { transformEmailsValue } from 'src/engine/core-modules/record-transformer/utils/transform-emails-value.util'; @@ -112,6 +114,7 @@ export class DataArgProcessor { throw new CommonQueryRunnerException( `Object ${flatObjectMetadata.nameSingular} doesn't have any "${key}" field.`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } @@ -121,6 +124,7 @@ export class DataArgProcessor { throw new CommonQueryRunnerException( `Field metadata not found for field ${key}`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } @@ -132,6 +136,7 @@ export class DataArgProcessor { throw new CommonQueryRunnerException( `Field ${key} is not nullable and has no default value.`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`A required field is missing.` }, ); } @@ -221,6 +226,7 @@ export class DataArgProcessor { throw new CommonQueryRunnerException( `One-to-many relation ${key} field does not support write operations.`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } @@ -276,6 +282,7 @@ export class DataArgProcessor { throw new CommonQueryRunnerException( `${key} ${fieldMetadata.type}-typed field does not support write operations`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); default: assertUnreachable( diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-actor-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-actor-field-or-throw.util.ts index f8b7b44673..d06aaae398 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-actor-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-actor-field-or-throw.util.ts @@ -1,3 +1,4 @@ +import { msg } from '@lingui/core/macro'; import { isNull } from '@sniptt/guards'; import { FieldActorSource } from 'twenty-shared/types'; @@ -40,6 +41,7 @@ export const validateActorFieldOrThrow = ( throw new CommonQueryRunnerException( `Invalid subfield ${subField} for actor field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value for actor.` }, ); } } diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-address-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-address-field-or-throw.util.ts index bc1fbcb3a1..528fe190cf 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-address-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-address-field-or-throw.util.ts @@ -1,3 +1,4 @@ +import { msg } from '@lingui/core/macro'; import { isNull } from '@sniptt/guards'; import { validateNumericFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-numeric-field-or-throw.util'; @@ -55,6 +56,7 @@ export const validateAddressFieldOrThrow = ( throw new CommonQueryRunnerException( `Invalid subfield ${subField} for address field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value for address.` }, ); } } diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util.ts index d89b59e1d7..a8e1672c06 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util.ts @@ -1,5 +1,6 @@ import { inspect } from 'util'; +import { msg } from '@lingui/core/macro'; import { isNull } from '@sniptt/guards'; import { @@ -16,9 +17,12 @@ export const validateArrayFieldOrThrow = ( if (typeof value === 'string') return value; if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) { + const inspectedValue = inspect(value); + throw new CommonQueryRunnerException( - `Invalid value ${inspect(value)} for field "${fieldName} - Array values need to be string"`, + `Invalid value ${inspectedValue} for field "${fieldName} - Array values need to be string"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value: "${inspectedValue}"` }, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-boolean-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-boolean-field-or-throw.util.ts index 2d091fe2e0..973c43704f 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-boolean-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-boolean-field-or-throw.util.ts @@ -1,5 +1,6 @@ import { inspect } from 'util'; +import { msg } from '@lingui/core/macro'; import { isNull } from '@sniptt/guards'; import { @@ -11,11 +12,15 @@ export const validateBooleanFieldOrThrow = ( value: unknown, fieldName: string, ): boolean | null => { - if (typeof value !== 'boolean' && !isNull(value)) + if (typeof value !== 'boolean' && !isNull(value)) { + const inspectedValue = inspect(value); + throw new CommonQueryRunnerException( `Invalid boolean value ${inspect(value)} for field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value: "${inspectedValue}"` }, ); + } return value; }; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-currency-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-currency-field-or-throw.util.ts index a5f518ba71..910f046af5 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-currency-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-currency-field-or-throw.util.ts @@ -1,3 +1,4 @@ +import { msg } from '@lingui/core/macro'; import { isNull } from '@sniptt/guards'; import { validateNumericFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-numeric-field-or-throw.util'; @@ -32,6 +33,7 @@ export const validateCurrencyFieldOrThrow = ( throw new CommonQueryRunnerException( `Invalid subfield ${subField} for currency field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value for currency.` }, ); } } diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-and-date-time-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-and-date-time-field-or-throw.util.ts index c1884245c7..dbc5e9d6ec 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-and-date-time-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-and-date-time-field-or-throw.util.ts @@ -1,5 +1,6 @@ import { inspect } from 'util'; +import { msg } from '@lingui/core/macro'; import { isDate, isNull, isNumber, isString } from '@sniptt/guards'; import { @@ -20,8 +21,11 @@ export const validateDateAndDateTimeFieldOrThrow = ( if (!isNaN(date.getTime())) return value; } + const inspectedValue = inspect(value); + throw new CommonQueryRunnerException( - `Invalid value ${inspect(value)} for date or date-time field "${fieldName}"`, + `Invalid value ${inspectedValue} for date or date-time field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value for date: "${inspectedValue}"` }, ); }; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-emails-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-emails-field-or-throw.util.ts index 32b9e5597a..f36d70e1cf 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-emails-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-emails-field-or-throw.util.ts @@ -1,3 +1,4 @@ +import { msg } from '@lingui/core/macro'; import { isNull } from '@sniptt/guards'; import { validateArrayFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util'; @@ -31,6 +32,7 @@ export const validateEmailsFieldOrThrow = ( throw new CommonQueryRunnerException( `Invalid subfield ${subField} for emails field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value for emails.` }, ); } } diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-full-name-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-full-name-field-or-throw.util.ts index cc02d87cd3..f4c79115e5 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-full-name-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-full-name-field-or-throw.util.ts @@ -1,3 +1,4 @@ +import { msg } from '@lingui/core/macro'; import { isNull } from '@sniptt/guards'; import { validateRawJsonFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util'; @@ -30,6 +31,7 @@ export const validateFullNameFieldOrThrow = ( throw new CommonQueryRunnerException( `Invalid subfield ${subField} for full name field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value for full name.` }, ); } } diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-links-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-links-field-or-throw.util.ts index e0d7efd684..8862f07248 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-links-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-links-field-or-throw.util.ts @@ -1,3 +1,4 @@ +import { msg } from '@lingui/core/macro'; import { isNull } from '@sniptt/guards'; import { validateRawJsonFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util'; @@ -31,6 +32,7 @@ export const validateLinksFieldOrThrow = ( throw new CommonQueryRunnerException( `Invalid subfield ${subField} for links field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value for links.` }, ); } } diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-multi-select-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-multi-select-field-or-throw.util.ts index 6c263ecf80..41a3ce27b9 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-multi-select-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-multi-select-field-or-throw.util.ts @@ -1,5 +1,6 @@ import { inspect } from 'util'; +import { msg } from '@lingui/core/macro'; import { isNull } from '@sniptt/guards'; import { isDefined } from 'twenty-shared/utils'; @@ -8,6 +9,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; export const validateMultiSelectFieldOrThrow = ( value: unknown, @@ -22,6 +24,7 @@ export const validateMultiSelectFieldOrThrow = ( throw new CommonQueryRunnerException( `Invalid options for field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } @@ -31,9 +34,14 @@ export const validateMultiSelectFieldOrThrow = ( : [preValidatedValue] ).some((item) => !options.includes(item)) ) { + const inspectedValue = inspect(value); + throw new CommonQueryRunnerException( - `Invalid value ${inspect(value)} for multi select field "${fieldName}"`, + `Invalid value ${inspectedValue} for multi select field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { + userFriendlyMessage: msg`Invalid value for multi-select: "${inspectedValue}"`, + }, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util.ts index a56e78d0f7..9d532487a3 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util.ts @@ -1,5 +1,6 @@ import { inspect } from 'util'; +import { msg } from '@lingui/core/macro'; import { isNull } from '@sniptt/guards'; import { @@ -15,11 +16,17 @@ export const validateNumberFieldOrThrow = ( (typeof value !== 'number' && !isNull(value)) || (typeof value === 'number' && (isNaN(value) || value === Infinity || value === -Infinity)) - ) + ) { + const inspectedValue = inspect(value); + throw new CommonQueryRunnerException( - `Invalid number value ${inspect(value)} for field "${fieldName}"`, + `Invalid number value ${inspectedValue} for field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { + userFriendlyMessage: msg`Invalid value for number: "${inspectedValue}"`, + }, ); + } return value; }; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-overridden-position-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-overridden-position-field-or-throw.util.ts index 3ac292ac2c..003ef16ac6 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-overridden-position-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-overridden-position-field-or-throw.util.ts @@ -1,5 +1,7 @@ import { inspect } from 'util'; +import { msg } from '@lingui/core/macro'; + import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, @@ -13,11 +15,17 @@ export const validateOverriddenPositionFieldOrThrow = ( typeof value !== 'number' || (typeof value === 'number' && (isNaN(value) || value === Infinity || value === -Infinity)) - ) + ) { + const inspectedValue = inspect(value); + throw new CommonQueryRunnerException( - `Invalid position value ${inspect(value)} for field "${fieldName}"`, + `Invalid position value ${inspectedValue} for field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { + userFriendlyMessage: msg`Invalid value for position: "${inspectedValue}"`, + }, ); + } return value; }; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-phones-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-phones-field-or-throw.util.ts index 2a3017739a..99f6b8ef87 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-phones-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-phones-field-or-throw.util.ts @@ -1,3 +1,4 @@ +import { msg } from '@lingui/core/macro'; import { isNull } from '@sniptt/guards'; import { validateRawJsonFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util'; @@ -35,6 +36,7 @@ export const validatePhonesFieldOrThrow = ( throw new CommonQueryRunnerException( `Invalid subfield ${subField} for phones field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value for phones.` }, ); } } diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rating-and-select-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rating-and-select-field-or-throw.util.ts index f47b134f08..7cd74de99d 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rating-and-select-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rating-and-select-field-or-throw.util.ts @@ -1,5 +1,6 @@ import { inspect } from 'util'; +import { msg } from '@lingui/core/macro'; import { isNull } from '@sniptt/guards'; import { isDefined } from 'twenty-shared/utils'; @@ -8,6 +9,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; export const validateRatingAndSelectFieldOrThrow = ( value: unknown, @@ -20,13 +22,19 @@ export const validateRatingAndSelectFieldOrThrow = ( throw new CommonQueryRunnerException( `Invalid options for field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } if (!isNull(preValidatedValue) && !options.includes(preValidatedValue)) { + const inspectedValue = inspect(value); + throw new CommonQueryRunnerException( - `Invalid value ${inspect(value)} for field "${fieldName}"`, + `Invalid value ${inspectedValue} for field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { + userFriendlyMessage: msg`Invalid value for select: "${inspectedValue}"`, + }, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util.ts index 6744ac77ea..839ca0c3a0 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util.ts @@ -1,5 +1,6 @@ import { inspect } from 'util'; +import { msg } from '@lingui/core/macro'; import { isNull, isObject } from '@sniptt/guards'; import { @@ -20,6 +21,7 @@ export const validateRawJsonFieldOrThrow = ( throw new CommonQueryRunnerException( `Invalid object value ${inspect(value)} for field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value for JSON.` }, ); } @@ -27,9 +29,12 @@ export const validateRawJsonFieldOrThrow = ( } if (!isObject(value)) { + const inspectedValue = inspect(value); + throw new CommonQueryRunnerException( - `Invalid object value ${inspect(value)} for field "${fieldName}"`, + `Invalid object value ${inspectedValue} for field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value for JSON: "${inspectedValue}"` }, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rich-text-v2-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rich-text-v2-field-or-throw.util.ts index 425aad4a5c..9003fafced 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rich-text-v2-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rich-text-v2-field-or-throw.util.ts @@ -1,5 +1,6 @@ import { inspect } from 'util'; +import { msg } from '@lingui/core/macro'; import { isNull, isObject } from '@sniptt/guards'; import { compositeTypeDefinitions, @@ -48,6 +49,7 @@ export const validateRichTextV2FieldOrThrow = ( throw new CommonQueryRunnerException( `Invalid rich text v2 value ${inspect(value)} for field "${fieldName}" - ${error.message}`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value for rich text.` }, ); } }; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util.ts index f39f8ec6df..ba55c50958 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util.ts @@ -1,5 +1,6 @@ import { inspect } from 'util'; +import { msg } from '@lingui/core/macro'; import { isNull } from '@sniptt/guards'; import { @@ -11,11 +12,15 @@ export const validateTextFieldOrThrow = ( value: unknown, fieldName: string, ): string | null => { - if (typeof value !== 'string' && !isNull(value)) + if (typeof value !== 'string' && !isNull(value)) { + const inspectedValue = inspect(value); + throw new CommonQueryRunnerException( - `Invalid string value ${inspect(value)} for text field "${fieldName}"`, + `Invalid string value ${inspectedValue} for text field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value: "${inspectedValue}"` }, ); + } return value; }; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-uuid-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-uuid-field-or-throw.util.ts index 00e0094215..ed44efe6b7 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-uuid-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-uuid-field-or-throw.util.ts @@ -1,5 +1,6 @@ import { inspect } from 'util'; +import { msg } from '@lingui/core/macro'; import { isNull } from '@sniptt/guards'; import { isValidUuid } from 'twenty-shared/utils'; @@ -12,11 +13,15 @@ export const validateUUIDFieldOrThrow = ( value: unknown, fieldName: string, ): string | null => { - if (!isValidUuid(value as string) && !isNull(value)) + if (!isValidUuid(value as string) && !isNull(value)) { + const inspectedValue = inspect(value); + throw new CommonQueryRunnerException( - `Invalid UUID value ${inspect(value)} for field "${fieldName}"`, + `Invalid UUID value ${inspectedValue} for field "${fieldName}"`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value for UUID: "${inspectedValue}"` }, ); + } return value as string; }; diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts index bf6be52a58..23332234d4 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts @@ -1,6 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { msg } from '@lingui/core/macro'; import { type PermissionFlagType } from 'twenty-shared/constants'; import { isDefined } from 'twenty-shared/utils'; @@ -13,6 +12,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { CommonResultGettersService } from 'src/engine/api/common/common-result-getters/common-result-getters.service'; import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; @@ -104,6 +104,7 @@ export abstract class CommonBaseQueryRunnerService< throw new CommonQueryRunnerException( 'Invalid auth context', CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } @@ -315,6 +316,7 @@ export abstract class CommonBaseQueryRunnerService< throw new CommonQueryRunnerException( 'Invalid auth context', CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } @@ -414,7 +416,7 @@ export abstract class CommonBaseQueryRunnerService< `Query complexity is too high. One-to-Many relation cannot be nested in another One-to-Many relation.`, CommonQueryRunnerExceptionCode.TOO_COMPLEX_QUERY, { - userFriendlyMessage: msg`Query complexity is too high. One-to-Many relation cannot be nested in another One-to-Many relation.`, + userFriendlyMessage: STANDARD_ERROR_MESSAGE, }, ); } @@ -429,7 +431,7 @@ export abstract class CommonBaseQueryRunnerService< `Query complexity is too high. Please, reduce the amount of relation fields requested. Query complexity: ${queryComplexity}. Maximum complexity: ${maximumComplexity}.`, CommonQueryRunnerExceptionCode.TOO_COMPLEX_QUERY, { - userFriendlyMessage: msg`Query complexity is too high. Please, reduce the amount of relation fields requested. Query complexity: ${queryComplexity}. Maximum complexity: ${maximumComplexity}.`, + userFriendlyMessage: STANDARD_ERROR_MESSAGE, }, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts index 0e05b92f3b..ef6465da62 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts @@ -17,6 +17,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; import { @@ -467,6 +468,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer throw new CommonQueryRunnerException( `Missing createdBy field metadata for object ${flatObjectMetadata.nameSingular}`, CommonQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts index 9c5b03b8be..88773f8988 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts @@ -12,6 +12,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; import { @@ -139,6 +140,7 @@ export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerSer throw new CommonQueryRunnerException( 'Filter is required', CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-one-query-runner.service.ts index b28eaa8e67..f5f7636396 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-one-query-runner.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { msg } from '@lingui/core/macro'; import { type ObjectRecord } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; @@ -54,6 +55,9 @@ export class CommonDeleteOneQueryRunnerService extends CommonBaseQueryRunnerServ throw new CommonQueryRunnerException( 'Record not found', CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND, + { + userFriendlyMessage: msg`This record does not exist or has been deleted.`, + }, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts index aa31065f3d..0cbc56f2f4 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts @@ -12,6 +12,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; import { @@ -140,6 +141,7 @@ export class CommonDestroyManyQueryRunnerService extends CommonBaseQueryRunnerSe throw new CommonQueryRunnerException( 'Filter is required', CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service.ts index 2d9c035f02..a23fecd759 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { msg } from '@lingui/core/macro'; import { type ObjectRecord } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; @@ -11,6 +12,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; import { @@ -52,6 +54,9 @@ export class CommonDestroyOneQueryRunnerService extends CommonBaseQueryRunnerSer throw new CommonQueryRunnerException( 'Record not found', CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND, + { + userFriendlyMessage: msg`This record does not exist or has been deleted.`, + }, ); } @@ -89,6 +94,7 @@ export class CommonDestroyOneQueryRunnerService extends CommonBaseQueryRunnerSer throw new CommonQueryRunnerException( 'Missing id', CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts index d16d43ce15..217f97ecbd 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts @@ -16,6 +16,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; import { CommonFindDuplicatesOutputItem } from 'src/engine/api/common/types/common-find-duplicates-output-item.type'; @@ -232,6 +233,7 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne throw new CommonQueryRunnerException( 'You have to provide either "data" or "ids" argument', CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } @@ -239,6 +241,7 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne throw new CommonQueryRunnerException( 'You cannot provide both "data" and "ids" arguments', CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } @@ -246,6 +249,7 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne throw new CommonQueryRunnerException( 'The "data" condition can not be empty when "ids" input not provided', CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts index 236ac58724..14d5806fad 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts @@ -19,6 +19,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; import { CommonFindManyOutput } from 'src/engine/api/common/types/common-find-many-output.type'; @@ -235,36 +236,42 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi throw new CommonQueryRunnerException( 'Cannot provide both first and last', CommonQueryRunnerExceptionCode.ARGS_CONFLICT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } if (args.before && args.after) { throw new CommonQueryRunnerException( 'Cannot provide both before and after', CommonQueryRunnerExceptionCode.ARGS_CONFLICT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } if (args.before && args.first) { throw new CommonQueryRunnerException( 'Cannot provide both before and first', CommonQueryRunnerExceptionCode.ARGS_CONFLICT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } if (args.after && args.last) { throw new CommonQueryRunnerException( 'Cannot provide both after and last', CommonQueryRunnerExceptionCode.ARGS_CONFLICT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } if (args.first !== undefined && args.first < 0) { throw new CommonQueryRunnerException( 'First argument must be non-negative', CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } if (args.last !== undefined && args.last < 0) { throw new CommonQueryRunnerException( 'Last argument must be non-negative', CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts index be0b684055..d7fdce15a1 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { msg } from '@lingui/core/macro'; import { QUERY_MAX_RECORDS_FROM_RELATION } from 'twenty-shared/constants'; import { ObjectRecord } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; @@ -13,6 +14,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; import { @@ -81,6 +83,9 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic throw new CommonQueryRunnerException( 'Record not found', CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND, + { + userFriendlyMessage: msg`This record does not exist or has been deleted.`, + }, ); } @@ -147,6 +152,7 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic throw new CommonQueryRunnerException( 'Missing filter argument', CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts index 1c2b22071a..4f0fa78a78 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts @@ -23,6 +23,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { getGroupByDefinitions } from 'src/engine/api/common/common-query-runners/utils/get-group-by-definitions.util'; import { getObjectAlias } from 'src/engine/api/common/common-query-runners/utils/get-object-alias-for-group-by.util'; import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; @@ -214,6 +215,7 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic throw new CommonQueryRunnerException( `Field metadata not found for field ${viewFilter.fieldMetadataId}`, CommonQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } @@ -363,6 +365,7 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic throw new CommonQueryRunnerException( `Field metadata settings are missing or invalid for field ${groupByField.fieldMetadata.name}`, CommonQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts index 7ecf40f7a5..8cea5038c7 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts @@ -1,5 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; +import { msg } from '@lingui/core/macro'; import { MUTATION_MAX_MERGE_RECORDS, QUERY_MAX_RECORDS_FROM_RELATION, @@ -21,6 +22,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; import { @@ -144,6 +146,7 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ throw new CommonQueryRunnerException( 'One or more records not found', CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND, + { userFriendlyMessage: msg`One or more records were not found.` }, ); } @@ -182,6 +185,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ throw new CommonQueryRunnerException( 'Priority record not found', CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND, + { + userFriendlyMessage: msg`This record does not exist or has been deleted.`, + }, ); } @@ -323,6 +329,7 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ throw new CommonQueryRunnerException( 'Failed to update record', CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } @@ -482,6 +489,7 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ throw new CommonQueryRunnerException( `Merge is only available for objects with duplicate criteria. Object '${flatObjectMetadata.nameSingular}' does not have duplicate criteria defined.`, CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: msg`This type of record cannot be merged.` }, ); } @@ -491,6 +499,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ throw new CommonQueryRunnerException( 'At least 2 record IDs are required for merge', CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { + userFriendlyMessage: msg`Please select at least 2 records to merge.`, + }, ); } @@ -498,6 +509,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ throw new CommonQueryRunnerException( `Maximum ${MUTATION_MAX_MERGE_RECORDS} records can be merged at once`, CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { + userFriendlyMessage: msg`You can merge up to ${MUTATION_MAX_MERGE_RECORDS} records at once.`, + }, ); } @@ -505,6 +519,7 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ throw new CommonQueryRunnerException( `Invalid conflict priority '${conflictPriorityIndex}'. Valid options for ${ids.length} records: 0-${ids.length - 1}`, CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts index 197df7c50d..c789c178fb 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts @@ -12,6 +12,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; import { @@ -139,6 +140,7 @@ export class CommonRestoreManyQueryRunnerService extends CommonBaseQueryRunnerSe throw new CommonQueryRunnerException( 'Filter is required', CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-one-query-runner.service.ts index 93eab1448d..141f024602 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-one-query-runner.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { msg } from '@lingui/core/macro'; import { type ObjectRecord } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; @@ -54,6 +55,9 @@ export class CommonRestoreOneQueryRunnerService extends CommonBaseQueryRunnerSer throw new CommonQueryRunnerException( 'Record not found', CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND, + { + userFriendlyMessage: msg`This record does not exist or has been deleted.`, + }, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts index a1458b4714..10bcb18733 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts @@ -12,6 +12,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; import { @@ -133,6 +134,7 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer throw new CommonQueryRunnerException( 'Filter is required', CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts index b564d0ac8c..b7a077fd3f 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { msg } from '@lingui/core/macro'; import { type ObjectRecord } from 'twenty-shared/types'; import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; @@ -52,6 +53,9 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ throw new CommonQueryRunnerException( 'Record not found', CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND, + { + userFriendlyMessage: msg`This record does not exist or has been deleted.`, + }, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts index 5da8fbd228..7df5f2b4bb 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts @@ -1,5 +1,4 @@ import { type MessageDescriptor } from '@lingui/core'; -import { msg } from '@lingui/core/macro'; import { CustomException } from 'src/utils/custom-exception'; @@ -21,37 +20,14 @@ export enum CommonQueryRunnerExceptionCode { MISSING_TIMEZONE_FOR_DATE_GROUP_BY = 'MISSING_TIMEZONE_FOR_DATE_GROUP_BY', } -const commonQueryRunnerExceptionUserFriendlyMessages: Record< - CommonQueryRunnerExceptionCode, - MessageDescriptor -> = { - [CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND]: msg`Record not found.`, - [CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT]: msg`Invalid query input.`, - [CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT]: msg`Invalid authentication context.`, - [CommonQueryRunnerExceptionCode.ARGS_CONFLICT]: msg`Conflicting arguments provided.`, - [CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA]: msg`Invalid data provided.`, - [CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST]: msg`Invalid 'first' argument.`, - [CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST]: msg`Invalid 'last' argument.`, - [CommonQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT]: msg`Multiple matching records found during upsert.`, - [CommonQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD]: msg`Missing required system field.`, - [CommonQueryRunnerExceptionCode.INVALID_CURSOR]: msg`Invalid cursor provided.`, - [CommonQueryRunnerExceptionCode.TOO_MANY_RECORDS_TO_UPDATE]: msg`Too many records to update at once.`, - [CommonQueryRunnerExceptionCode.BAD_REQUEST]: msg`Bad request.`, - [CommonQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR]: msg`An unexpected error occurred.`, - [CommonQueryRunnerExceptionCode.TOO_COMPLEX_QUERY]: msg`Query is too complex.`, - [CommonQueryRunnerExceptionCode.MISSING_TIMEZONE_FOR_DATE_GROUP_BY]: msg`Missing time zone for date group by.`, -}; - export class CommonQueryRunnerException extends CustomException { constructor( message: string, code: CommonQueryRunnerExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {}, + { userFriendlyMessage }: { userFriendlyMessage: MessageDescriptor }, ) { super(message, code, { - userFriendlyMessage: - userFriendlyMessage ?? - commonQueryRunnerExceptionUserFriendlyMessages[code], + userFriendlyMessage, }); } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/errors/standard-error-message.constant.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/errors/standard-error-message.constant.ts new file mode 100644 index 0000000000..fda63b5fa1 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/errors/standard-error-message.constant.ts @@ -0,0 +1,3 @@ +import { msg } from '@lingui/core/macro'; + +export const STANDARD_ERROR_MESSAGE = msg`An error occurred.`; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts index 89b233e2ad..38ea5fa676 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts @@ -1,5 +1,4 @@ import { type MessageDescriptor } from '@lingui/core'; -import { msg } from '@lingui/core/macro'; import { CustomException } from 'src/utils/custom-exception'; @@ -24,40 +23,14 @@ export enum GraphqlQueryRunnerExceptionCode { UPSERT_MAX_RECORDS_EXCEEDED = 'UPSERT_MAX_RECORDS_EXCEEDED', } -const graphqlQueryRunnerExceptionUserFriendlyMessages: Record< - GraphqlQueryRunnerExceptionCode, - MessageDescriptor -> = { - [GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT]: msg`Invalid query input.`, - [GraphqlQueryRunnerExceptionCode.MAX_DEPTH_REACHED]: msg`Maximum query depth reached.`, - [GraphqlQueryRunnerExceptionCode.INVALID_CURSOR]: msg`Invalid cursor provided.`, - [GraphqlQueryRunnerExceptionCode.INVALID_DIRECTION]: msg`Invalid direction provided.`, - [GraphqlQueryRunnerExceptionCode.UNSUPPORTED_OPERATOR]: msg`Unsupported operator.`, - [GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT]: msg`Conflicting arguments provided.`, - [GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND]: msg`Field not found.`, - [GraphqlQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD]: msg`Missing required system field.`, - [GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND]: msg`Object not found.`, - [GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND]: msg`Record not found.`, - [GraphqlQueryRunnerExceptionCode.INVALID_ARGS_FIRST]: msg`Invalid 'first' argument.`, - [GraphqlQueryRunnerExceptionCode.INVALID_ARGS_LAST]: msg`Invalid 'last' argument.`, - [GraphqlQueryRunnerExceptionCode.RELATION_SETTINGS_NOT_FOUND]: msg`Relation settings not found.`, - [GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND]: msg`Relation target not found.`, - [GraphqlQueryRunnerExceptionCode.NOT_IMPLEMENTED]: msg`This feature is not implemented.`, - [GraphqlQueryRunnerExceptionCode.INVALID_POST_HOOK_PAYLOAD]: msg`Invalid post-hook payload.`, - [GraphqlQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT]: msg`Multiple matching records found during upsert.`, - [GraphqlQueryRunnerExceptionCode.UPSERT_MAX_RECORDS_EXCEEDED]: msg`Maximum records exceeded for upsert.`, -}; - export class GraphqlQueryRunnerException extends CustomException { constructor( message: string, code: GraphqlQueryRunnerExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {}, + { userFriendlyMessage }: { userFriendlyMessage: MessageDescriptor }, ) { super(message, code, { - userFriendlyMessage: - userFriendlyMessage ?? - graphqlQueryRunnerExceptionUserFriendlyMessages[code], + userFriendlyMessage, }); } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts index a595b0e5da..a468fa71f7 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts @@ -1,3 +1,4 @@ +import { msg } from '@lingui/core/macro'; import { compositeTypeDefinitions } from 'twenty-shared/types'; import { capitalize, isDefined } from 'twenty-shared/utils'; import { type WhereExpressionBuilder } from 'typeorm'; @@ -73,6 +74,7 @@ export class GraphqlQueryFilterFieldParser { throw new GraphqlQueryRunnerException( `Invalid filter value for field ${key}. Expected non-empty array`, GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: msg`Invalid filter value: "${value}"` }, ); } const { sql, params } = computeWhereConditionParts({ @@ -133,6 +135,7 @@ export class GraphqlQueryFilterFieldParser { throw new GraphqlQueryRunnerException( `Invalid filter value for field ${subFieldKey}. Expected non-empty array`, GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: msg`Invalid filter value: "${value}"` }, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts index 1404a2bf80..6070e8d3d9 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts @@ -14,6 +14,7 @@ import { isDefined } from 'twenty-shared/utils'; import { type ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { GraphqlQueryRunnerException, GraphqlQueryRunnerExceptionCode, @@ -90,6 +91,7 @@ export class GraphqlQueryOrderFieldParser { throw new GraphqlQueryRunnerException( `Field "${fieldName}" does not exist or is not sortable`, GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/convert-order-by-to-find-options-order.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/convert-order-by-to-find-options-order.ts index dcd26cf4a1..2b0d19b2e5 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/convert-order-by-to-find-options-order.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/convert-order-by-to-find-options-order.ts @@ -1,5 +1,6 @@ import { OrderByDirection } from 'twenty-shared/types'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { GraphqlQueryRunnerException, GraphqlQueryRunnerExceptionCode, @@ -35,6 +36,7 @@ export const convertOrderByToFindOptionsOrder = ( throw new GraphqlQueryRunnerException( `Invalid direction: ${direction}`, GraphqlQueryRunnerExceptionCode.INVALID_DIRECTION, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } }; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util.ts index f40791493c..0489eafd9c 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util.ts @@ -10,6 +10,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { type GroupByField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types'; import { isGroupByDateField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field.util'; import { isGroupByRelationField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-relation-field.util'; @@ -44,6 +45,7 @@ export const getGroupByExpression = ({ throw new CommonQueryRunnerException( 'Time zone should be specified for a group by date on Day, Week, Month, Quarter or Year', CommonQueryRunnerExceptionCode.MISSING_TIMEZONE_FOR_DATE_GROUP_BY, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-relation-field.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-relation-field.util.ts index 0afb620ab9..48edf3206a 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-relation-field.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-relation-field.util.ts @@ -1,6 +1,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { GraphqlQueryRunnerException, GraphqlQueryRunnerExceptionCode, @@ -68,6 +69,7 @@ const getNestedFieldMetadataDetails = ({ throw new GraphqlQueryRunnerException( `Nested field "${nestedFieldName}" not found in target object "${targetObjectMetadata.nameSingular}"`, GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } @@ -133,6 +135,7 @@ const handleNestedCompositeField = ({ throw new GraphqlQueryRunnerException( `Composite field "${nestedFieldName}" requires a subfield to be specified`, GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); }; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/validate-single-key-for-group-by-or-throw.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/validate-single-key-for-group-by-or-throw.util.ts index 25295ea401..098d69239f 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/validate-single-key-for-group-by-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/validate-single-key-for-group-by-or-throw.util.ts @@ -1,3 +1,4 @@ +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { GraphqlQueryRunnerException, GraphqlQueryRunnerExceptionCode, @@ -14,6 +15,7 @@ export const validateSingleKeyForGroupByOrThrow = ({ throw new GraphqlQueryRunnerException( errorMessage, GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } }; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts index 35f825a505..6dc7641eff 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts @@ -8,6 +8,7 @@ import { isDefined } from 'twenty-shared/utils'; import { type ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { CONNECTION_MAX_DEPTH } from 'src/engine/api/graphql/graphql-query-runner/constants/connection-max-depth.constant'; import { GraphqlQueryRunnerException, @@ -159,6 +160,7 @@ export class ObjectRecordsToGraphqlConnectionHelper { throw new GraphqlQueryRunnerException( `Maximum depth of ${CONNECTION_MAX_DEPTH} reached`, GraphqlQueryRunnerExceptionCode.MAX_DEPTH_REACHED, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts index 02f8284081..24902cf5d3 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts @@ -5,6 +5,7 @@ import { type FindOptionsRelations, type ObjectLiteral } from 'typeorm'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { GraphqlQueryRunnerException, GraphqlQueryRunnerExceptionCode, @@ -147,6 +148,7 @@ export class ProcessNestedRelationsV2Helper { throw new GraphqlQueryRunnerException( `Relation settings not found for field ${sourceFieldName}`, GraphqlQueryRunnerExceptionCode.RELATION_SETTINGS_NOT_FOUND, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } @@ -271,6 +273,7 @@ export class ProcessNestedRelationsV2Helper { throw new GraphqlQueryRunnerException( `Field ${sourceFieldName} not found on object ${parentObjectMetadataItem.nameSingular}`, GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } @@ -286,6 +289,7 @@ export class ProcessNestedRelationsV2Helper { throw new GraphqlQueryRunnerException( `Relation target object metadata id or field metadata id not found for field ${sourceFieldName}`, GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts index e05a93211c..be2662ec16 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts @@ -3,6 +3,7 @@ import { isDefined } from 'twenty-shared/utils'; import { type ObjectLiteral } from 'typeorm'; import { findPostgresDefaultNullEquivalentValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/find-postgres-default-null-equivalent-value.util'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { GraphqlQueryRunnerException, GraphqlQueryRunnerExceptionCode, @@ -149,6 +150,7 @@ export const computeWhereConditionParts = ({ throw new GraphqlQueryRunnerException( `Operator "${operator}" is not supported`, GraphqlQueryRunnerExceptionCode.UNSUPPORTED_OPERATOR, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } }; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts index 635dab1c05..dbb148ea91 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts @@ -7,6 +7,7 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; export interface CursorData { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -20,6 +21,7 @@ export const decodeCursor = (cursor: string): T => { throw new CommonQueryRunnerException( `Invalid cursor: ${cursor}`, CommonQueryRunnerExceptionCode.INVALID_CURSOR, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } }; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util.ts index 72a4ab6bf6..a0c8a81447 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util.ts @@ -1,3 +1,4 @@ +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { GraphqlQueryRunnerException, GraphqlQueryRunnerExceptionCode, @@ -14,6 +15,7 @@ export const getTargetObjectMetadataOrThrow = ( throw new GraphqlQueryRunnerException( `Relation target object metadata id not found for field ${fieldMetadata.name}`, GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } @@ -24,6 +26,7 @@ export const getTargetObjectMetadataOrThrow = ( throw new GraphqlQueryRunnerException( `Target object metadata not found for field ${fieldMetadata.name}`, GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util.ts index 21da9fa576..49a587042c 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util.ts @@ -1,3 +1,4 @@ +import { msg } from '@lingui/core/macro'; import { isValidUuid } from 'twenty-shared/utils'; import { @@ -10,6 +11,7 @@ export const assertIsValidUuid = (value: string) => { throw new WorkspaceQueryRunnerException( `Value "${value}" is not a valid UUID`, WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { userFriendlyMessage: msg`Invalid UUID format.` }, ); } }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer.ts index 5dd2a65708..965003e1d6 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer.ts @@ -12,6 +12,7 @@ import { type WorkspacePreQueryHookInstance, } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { GraphqlQueryRunnerException, GraphqlQueryRunnerExceptionCode, @@ -148,6 +149,7 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit { throw new GraphqlQueryRunnerException( `Unsupported payload type: ${payload}`, GraphqlQueryRunnerExceptionCode.INVALID_POST_HOOK_PAYLOAD, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts index 4e14f8ccb7..7992fc3eb2 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts @@ -1,6 +1,8 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { appendCommonExceptionCode, CustomException, @@ -17,18 +19,26 @@ export const WorkspaceQueryRunnerExceptionCode = appendCommonExceptionCode({ NO_ROWS_AFFECTED: 'NO_ROWS_AFFECTED', } as const); -const workspaceQueryRunnerExceptionUserFriendlyMessages: Record< - keyof typeof WorkspaceQueryRunnerExceptionCode, - MessageDescriptor -> = { - INVALID_QUERY_INPUT: msg`Invalid query input.`, - DATA_NOT_FOUND: msg`Data not found.`, - QUERY_TIMEOUT: msg`Query timed out.`, - QUERY_VIOLATES_UNIQUE_CONSTRAINT: msg`A record with this value already exists.`, - QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT: msg`Cannot complete operation due to related records.`, - TOO_MANY_ROWS_AFFECTED: msg`Too many records affected.`, - NO_ROWS_AFFECTED: msg`No records were affected.`, - INTERNAL_SERVER_ERROR: msg`An unexpected error occurred.`, +const getWorkspaceQueryRunnerExceptionUserFriendlyMessage = ( + code: keyof typeof WorkspaceQueryRunnerExceptionCode, +) => { + switch (code) { + case WorkspaceQueryRunnerExceptionCode.QUERY_VIOLATES_UNIQUE_CONSTRAINT: + return msg`A record with this value already exists.`; + case WorkspaceQueryRunnerExceptionCode.QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT: + return msg`Cannot complete operation due to related records.`; + case WorkspaceQueryRunnerExceptionCode.TOO_MANY_ROWS_AFFECTED: + return msg`Too many records affected.`; + case WorkspaceQueryRunnerExceptionCode.NO_ROWS_AFFECTED: + return msg`No records were affected.`; + case WorkspaceQueryRunnerExceptionCode.QUERY_TIMEOUT: + case WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND: + case WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT: + case WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR: + return STANDARD_ERROR_MESSAGE; + default: + assertUnreachable(code); + } }; export class WorkspaceQueryRunnerException extends CustomException< @@ -42,7 +52,7 @@ export class WorkspaceQueryRunnerException extends CustomException< super(message, code, { userFriendlyMessage: userFriendlyMessage ?? - workspaceQueryRunnerExceptionUserFriendlyMessages[code], + getWorkspaceQueryRunnerExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/aggregate-fields-parser-utils/parse-aggregate-fields-rest-request.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/aggregate-fields-parser-utils/parse-aggregate-fields-rest-request.util.ts index f0f24c8434..c99cd6c5f9 100644 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/aggregate-fields-parser-utils/parse-aggregate-fields-rest-request.util.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/aggregate-fields-parser-utils/parse-aggregate-fields-rest-request.util.ts @@ -1,5 +1,6 @@ import { isDefined } from 'twenty-shared/utils'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { type CommonSelectedFields } from 'src/engine/api/common/types/common-selected-fields-result.type'; import { RestInputRequestParserException, @@ -18,6 +19,7 @@ export const parseAggregateFieldsRestRequest = ( throw new RestInputRequestParserException( `Invalid aggregate query parameter - should be a valid array of string - ex: ["countNotEmptyId", "countEmptyField"]`, RestInputRequestParserExceptionCode.INVALID_AGGREGATE_FIELDS_QUERY_PARAM, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } @@ -36,6 +38,7 @@ export const parseAggregateFieldsRestRequest = ( throw new RestInputRequestParserException( `Invalid aggregate query parameter - should be a valid array of string - ex: ["countNotEmptyId", "countEmptyField"]`, RestInputRequestParserExceptionCode.INVALID_AGGREGATE_FIELDS_QUERY_PARAM, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } }; diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util.ts index 0935225dcc..67c4e7d82d 100644 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util.ts @@ -1,3 +1,4 @@ +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { RestInputRequestParserException, RestInputRequestParserExceptionCode, @@ -22,6 +23,7 @@ export const parseDepthRestRequest = (request: AuthenticatedRequest): Depth => { ', ', )}`, RestInputRequestParserExceptionCode.INVALID_DEPTH_QUERY_PARAM, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util.ts index 0d41d1a270..74733d3fcb 100644 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util.ts @@ -1,3 +1,5 @@ +import { msg } from '@lingui/core/macro'; + import { RestInputRequestParserException, RestInputRequestParserExceptionCode, @@ -19,6 +21,7 @@ export const checkFilterQuery = (filterQuery: string): void => { throw new RestInputRequestParserException( `'filter' invalid. ${hint} missing in the query`, RestInputRequestParserExceptionCode.INVALID_FILTER_QUERY_PARAM, + { userFriendlyMessage: msg`Invalid filter parameter.` }, ); } diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter.util.ts index ed855861b7..0b8fd3dd15 100644 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter.util.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter.util.ts @@ -1,5 +1,7 @@ import { BadRequestException } from '@nestjs/common'; +import { msg } from '@lingui/core/macro'; + import { type FieldValue } from 'src/engine/api/rest/core/types/field-value.type'; import { formatFieldValue } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/format-field-values.util'; import { parseBaseFilter } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-base-filter.util'; @@ -40,6 +42,7 @@ export const parseFilter = ( throw new RestInputRequestParserException( `'filter' invalid. 'not' conjunction should contain only 1 condition. eg: not(field[eq]:1)`, RestInputRequestParserExceptionCode.INVALID_FILTER_QUERY_PARAM, + { userFriendlyMessage: msg`Invalid filter parameter.` }, ); } // @ts-expect-error legacy noImplicitAny diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception.ts index b25bbe5780..e9172be41a 100644 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -13,17 +14,27 @@ export enum RestInputRequestParserExceptionCode { INVALID_FILTER_QUERY_PARAM = 'INVALID_FILTER_QUERY_PARAM', } -const restInputRequestParserExceptionUserFriendlyMessages: Record< - RestInputRequestParserExceptionCode, - MessageDescriptor -> = { - [RestInputRequestParserExceptionCode.INVALID_AGGREGATE_FIELDS_QUERY_PARAM]: msg`Invalid aggregate fields parameter.`, - [RestInputRequestParserExceptionCode.INVALID_GROUP_BY_QUERY_PARAM]: msg`Invalid group by parameter.`, - [RestInputRequestParserExceptionCode.INVALID_ORDER_BY_WITH_GROUP_BY_QUERY_PARAM]: msg`Invalid order by with group by parameter.`, - [RestInputRequestParserExceptionCode.INVALID_ORDER_BY_QUERY_PARAM]: msg`Invalid order by parameter.`, - [RestInputRequestParserExceptionCode.INVALID_DEPTH_QUERY_PARAM]: msg`Invalid depth parameter.`, - [RestInputRequestParserExceptionCode.INVALID_LIMIT_QUERY_PARAM]: msg`Invalid limit parameter.`, - [RestInputRequestParserExceptionCode.INVALID_FILTER_QUERY_PARAM]: msg`Invalid filter parameter.`, +const getRestInputRequestParserExceptionUserFriendlyMessage = ( + code: RestInputRequestParserExceptionCode, +) => { + switch (code) { + case RestInputRequestParserExceptionCode.INVALID_AGGREGATE_FIELDS_QUERY_PARAM: + return msg`Invalid aggregate fields parameter.`; + case RestInputRequestParserExceptionCode.INVALID_GROUP_BY_QUERY_PARAM: + return msg`Invalid group by parameter.`; + case RestInputRequestParserExceptionCode.INVALID_ORDER_BY_WITH_GROUP_BY_QUERY_PARAM: + return msg`Invalid order by with group by parameter.`; + case RestInputRequestParserExceptionCode.INVALID_ORDER_BY_QUERY_PARAM: + return msg`Invalid order by parameter.`; + case RestInputRequestParserExceptionCode.INVALID_DEPTH_QUERY_PARAM: + return msg`Invalid depth parameter.`; + case RestInputRequestParserExceptionCode.INVALID_LIMIT_QUERY_PARAM: + return msg`Invalid limit parameter.`; + case RestInputRequestParserExceptionCode.INVALID_FILTER_QUERY_PARAM: + return msg`Invalid filter parameter.`; + default: + assertUnreachable(code); + } }; export class RestInputRequestParserException extends CustomException { @@ -35,7 +46,7 @@ export class RestInputRequestParserException extends CustomException = { - [ApiKeyExceptionCode.API_KEY_NOT_FOUND]: msg`API key not found.`, - [ApiKeyExceptionCode.API_KEY_REVOKED]: msg`This API key has been revoked.`, - [ApiKeyExceptionCode.API_KEY_EXPIRED]: msg`This API key has expired.`, - [ApiKeyExceptionCode.API_KEY_NO_ROLE_ASSIGNED]: msg`This API key has no role assigned.`, - [ApiKeyExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS]: msg`This role cannot be assigned to API keys.`, +const getApiKeyExceptionUserFriendlyMessage = (code: ApiKeyExceptionCode) => { + switch (code) { + case ApiKeyExceptionCode.API_KEY_NOT_FOUND: + return msg`API key not found.`; + case ApiKeyExceptionCode.API_KEY_REVOKED: + return msg`This API key has been revoked.`; + case ApiKeyExceptionCode.API_KEY_EXPIRED: + return msg`This API key has expired.`; + case ApiKeyExceptionCode.API_KEY_NO_ROLE_ASSIGNED: + return msg`This API key has no role assigned.`; + case ApiKeyExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS: + return msg`This role cannot be assigned to API keys.`; + default: + assertUnreachable(code); + } }; export class ApiKeyException extends CustomException { @@ -30,7 +37,7 @@ export class ApiKeyException extends CustomException { ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? apiKeyExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getApiKeyExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/core-modules/application/application.exception.ts b/packages/twenty-server/src/engine/core-modules/application/application.exception.ts index 30d3d57050..f3dcc4f58f 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -12,16 +13,25 @@ export enum ApplicationExceptionCode { FORBIDDEN = 'FORBIDDEN', } -const applicationExceptionUserFriendlyMessages: Record< - ApplicationExceptionCode, - MessageDescriptor -> = { - [ApplicationExceptionCode.OBJECT_NOT_FOUND]: msg`Object not found.`, - [ApplicationExceptionCode.FIELD_NOT_FOUND]: msg`Field not found.`, - [ApplicationExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND]: msg`Serverless function not found.`, - [ApplicationExceptionCode.ENTITY_NOT_FOUND]: msg`Entity not found.`, - [ApplicationExceptionCode.APPLICATION_NOT_FOUND]: msg`Application not found.`, - [ApplicationExceptionCode.FORBIDDEN]: msg`You do not have permission to perform this action.`, +const getApplicationExceptionUserFriendlyMessage = ( + code: ApplicationExceptionCode, +) => { + switch (code) { + case ApplicationExceptionCode.OBJECT_NOT_FOUND: + return msg`Object not found.`; + case ApplicationExceptionCode.FIELD_NOT_FOUND: + return msg`Field not found.`; + case ApplicationExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND: + return msg`Serverless function not found.`; + case ApplicationExceptionCode.ENTITY_NOT_FOUND: + return msg`Entity not found.`; + case ApplicationExceptionCode.APPLICATION_NOT_FOUND: + return msg`Application not found.`; + case ApplicationExceptionCode.FORBIDDEN: + return msg`You do not have permission to perform this action.`; + default: + assertUnreachable(code); + } }; export class ApplicationException extends CustomException { @@ -32,7 +42,7 @@ export class ApplicationException extends CustomException = { - [ApplicationVariableEntityExceptionCode.APPLICATION_VARIABLE_NOT_FOUND]: msg`Application variable not found.`, +const getApplicationVariableEntityExceptionUserFriendlyMessage = ( + code: ApplicationVariableEntityExceptionCode, +) => { + switch (code) { + case ApplicationVariableEntityExceptionCode.APPLICATION_VARIABLE_NOT_FOUND: + return msg`Application variable not found.`; + default: + assertUnreachable(code); + } }; export class ApplicationVariableEntityException extends CustomException { @@ -23,7 +28,7 @@ export class ApplicationVariableEntityException extends CustomException = { - [ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_NOT_FOUND]: msg`Approved access domain not found.`, - [ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VERIFIED]: msg`This domain has already been verified.`, - [ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_REGISTERED]: msg`This domain is already registered.`, - [ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_DOES_NOT_MATCH_DOMAIN_EMAIL]: msg`The domain does not match your email domain.`, - [ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_VALIDATION_TOKEN_INVALID]: msg`Invalid validation token.`, - [ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VALIDATED]: msg`This domain has already been validated.`, - [ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_MUST_BE_A_COMPANY_DOMAIN]: msg`Please use a company email domain.`, +const getApprovedAccessDomainExceptionUserFriendlyMessage = ( + code: ApprovedAccessDomainExceptionCode, +) => { + switch (code) { + case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_NOT_FOUND: + return msg`Approved access domain not found.`; + case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VERIFIED: + return msg`This domain has already been verified.`; + case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_REGISTERED: + return msg`This domain is already registered.`; + case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_DOES_NOT_MATCH_DOMAIN_EMAIL: + return msg`The domain does not match your email domain.`; + case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_VALIDATION_TOKEN_INVALID: + return msg`Invalid validation token.`; + case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VALIDATED: + return msg`This domain has already been validated.`; + case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_MUST_BE_A_COMPANY_DOMAIN: + return msg`Please use a company email domain.`; + default: + assertUnreachable(code); + } }; export class ApprovedAccessDomainException extends CustomException { @@ -35,7 +46,7 @@ export class ApprovedAccessDomainException extends CustomException = { - [AuditExceptionCode.INVALID_TYPE]: msg`Invalid audit type.`, - [AuditExceptionCode.INVALID_INPUT]: msg`Invalid audit input.`, +const getAuditExceptionUserFriendlyMessage = (code: AuditExceptionCode) => { + switch (code) { + case AuditExceptionCode.INVALID_TYPE: + return msg`Invalid audit type.`; + case AuditExceptionCode.INVALID_INPUT: + return msg`Invalid audit input.`; + default: + assertUnreachable(code); + } }; export class AuditException extends CustomException { @@ -24,7 +28,7 @@ export class AuditException extends CustomException { ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? auditExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getAuditExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts index c6b60cc25f..888050e099 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts @@ -1,55 +1,13 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { appendCommonExceptionCode, CustomException, } from 'src/utils/custom-exception'; -const authExceptionUserFriendlyMessages: Record< - keyof typeof AuthExceptionCode, - MessageDescriptor -> = { - USER_NOT_FOUND: msg`User not found.`, - USER_WORKSPACE_NOT_FOUND: msg`User workspace not found.`, - EMAIL_NOT_VERIFIED: msg`Email is not verified.`, - CLIENT_NOT_FOUND: msg`Client not found.`, - WORKSPACE_NOT_FOUND: msg`Workspace not found.`, - APPLICATION_NOT_FOUND: msg`Application not found.`, - INVALID_INPUT: msg`Invalid input provided.`, - FORBIDDEN_EXCEPTION: msg`You do not have permission to perform this action.`, - INSUFFICIENT_SCOPES: msg`Insufficient permissions.`, - UNAUTHENTICATED: msg`You must be authenticated to perform this action.`, - INVALID_DATA: msg`Invalid data provided.`, - OAUTH_ACCESS_DENIED: msg`OAuth access was denied.`, - SSO_AUTH_FAILED: msg`Single sign-on authentication failed.`, - USE_SSO_AUTH: msg`Please use single sign-on to authenticate.`, - SIGNUP_DISABLED: msg`Sign up is disabled.`, - GOOGLE_API_AUTH_DISABLED: msg`Google API authentication is disabled.`, - MICROSOFT_API_AUTH_DISABLED: msg`Microsoft API authentication is disabled.`, - MISSING_ENVIRONMENT_VARIABLE: msg`A required configuration is missing.`, - INVALID_JWT_TOKEN_TYPE: msg`Invalid authentication token.`, - TWO_FACTOR_AUTHENTICATION_PROVISION_REQUIRED: msg`Two-factor authentication setup is required.`, - TWO_FACTOR_AUTHENTICATION_VERIFICATION_REQUIRED: msg`Two-factor authentication verification is required.`, - USER_ALREADY_EXISTS: msg`A user with this email already exists.`, - INTERNAL_SERVER_ERROR: msg`An unexpected error occurred.`, -}; - -export class AuthException extends CustomException< - keyof typeof AuthExceptionCode -> { - constructor( - message: string, - code: keyof typeof AuthExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {}, - ) { - super(message, code, { - userFriendlyMessage: - userFriendlyMessage ?? authExceptionUserFriendlyMessages[code], - }); - } -} - export const AuthExceptionCode = appendCommonExceptionCode({ USER_NOT_FOUND: 'USER_NOT_FOUND', USER_WORKSPACE_NOT_FOUND: 'USER_WORKSPACE_NOT_FOUND', @@ -76,3 +34,70 @@ export const AuthExceptionCode = appendCommonExceptionCode({ 'TWO_FACTOR_AUTHENTICATION_VERIFICATION_REQUIRED', USER_ALREADY_EXISTS: 'USER_ALREADY_EXISTS', } as const); + +const getAuthExceptionUserFriendlyMessage = ( + code: keyof typeof AuthExceptionCode, +) => { + switch (code) { + case AuthExceptionCode.USER_NOT_FOUND: + return msg`User not found.`; + case AuthExceptionCode.USER_WORKSPACE_NOT_FOUND: + return msg`User workspace not found.`; + case AuthExceptionCode.EMAIL_NOT_VERIFIED: + return msg`Email is not verified.`; + case AuthExceptionCode.WORKSPACE_NOT_FOUND: + return msg`Workspace not found.`; + case AuthExceptionCode.APPLICATION_NOT_FOUND: + return msg`Application not found.`; + case AuthExceptionCode.INVALID_INPUT: + return msg`Invalid input provided.`; + case AuthExceptionCode.FORBIDDEN_EXCEPTION: + return msg`You do not have permission to perform this action.`; + case AuthExceptionCode.INSUFFICIENT_SCOPES: + return msg`Insufficient permissions.`; + case AuthExceptionCode.UNAUTHENTICATED: + return msg`You must be authenticated to perform this action.`; + case AuthExceptionCode.OAUTH_ACCESS_DENIED: + return msg`OAuth access was denied.`; + case AuthExceptionCode.SSO_AUTH_FAILED: + return msg`Single sign-on authentication failed.`; + case AuthExceptionCode.USE_SSO_AUTH: + return msg`Please use single sign-on to authenticate.`; + case AuthExceptionCode.SIGNUP_DISABLED: + return msg`Sign up is disabled.`; + case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED: + return msg`Google API authentication is disabled.`; + case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED: + return msg`Microsoft API authentication is disabled.`; + case AuthExceptionCode.MISSING_ENVIRONMENT_VARIABLE: + return msg`A required configuration is missing.`; + case AuthExceptionCode.TWO_FACTOR_AUTHENTICATION_PROVISION_REQUIRED: + return msg`Two-factor authentication setup is required.`; + case AuthExceptionCode.TWO_FACTOR_AUTHENTICATION_VERIFICATION_REQUIRED: + return msg`Two-factor authentication verification is required.`; + case AuthExceptionCode.USER_ALREADY_EXISTS: + return msg`A user with this email already exists.`; + case AuthExceptionCode.INTERNAL_SERVER_ERROR: + case AuthExceptionCode.INVALID_DATA: + case AuthExceptionCode.CLIENT_NOT_FOUND: + case AuthExceptionCode.INVALID_JWT_TOKEN_TYPE: + return STANDARD_ERROR_MESSAGE; + default: + assertUnreachable(code); + } +}; + +export class AuthException extends CustomException< + keyof typeof AuthExceptionCode +> { + constructor( + message: string, + code: keyof typeof AuthExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {}, + ) { + super(message, code, { + userFriendlyMessage: + userFriendlyMessage ?? getAuthExceptionUserFriendlyMessage(code), + }); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index 52543d7cbc..33394c5199 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -8,10 +8,10 @@ import { render } from '@react-email/render'; import { addMilliseconds } from 'date-fns'; import ms from 'ms'; import { PasswordUpdateNotifyEmail } from 'twenty-emails'; +import { PermissionFlagType } from 'twenty-shared/constants'; import { AppPath } from 'twenty-shared/types'; import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; -import { PermissionFlagType } from 'twenty-shared/constants'; import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface'; @@ -191,7 +191,7 @@ export class AuthService { 'Wrong password', AuthExceptionCode.FORBIDDEN_EXCEPTION, { - userFriendlyMessage: msg`Wrong password`, + userFriendlyMessage: msg`Wrong password.`, }, ); } @@ -357,6 +357,9 @@ export class AuthService { throw new AuthException( 'Email is required', AuthExceptionCode.INVALID_INPUT, + { + userFriendlyMessage: msg`Email is required.`, + }, ); } @@ -603,6 +606,9 @@ export class AuthService { throw new AuthException( 'Password is too weak', AuthExceptionCode.INVALID_INPUT, + { + userFriendlyMessage: msg`Password is too weak.`, + }, ); } @@ -650,6 +656,9 @@ export class AuthService { throw new AuthException( 'Workspace does not exist', AuthExceptionCode.INVALID_INPUT, + { + userFriendlyMessage: msg`Workspace does not exist.`, + }, ); } diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.spec.ts index ce8732a2a7..8242ed5f40 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.spec.ts @@ -1,5 +1,7 @@ import { randomUUID } from 'crypto'; +import { msg } from '@lingui/core/macro'; + import { AuthException, AuthExceptionCode, @@ -12,15 +14,6 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { JwtAuthStrategy } from './jwt.auth.strategy'; -jest.mock('twenty-shared/utils', () => ({ - ...jest.requireActual('twenty-shared/utils'), - assertIsDefinedOrThrow: jest.fn((value, error) => { - if (value === null || value === undefined) { - throw error; - } - }), -})); - describe('JwtAuthStrategy', () => { let strategy: JwtAuthStrategy; let workspaceRepository: any; @@ -230,7 +223,9 @@ describe('JwtAuthStrategy', () => { ); await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow( - new AuthException('UserWorkspaceEntity not found', expect.any(String)), + new AuthException('UserWorkspaceEntity not found', expect.any(String), { + userFriendlyMessage: msg`User does not have access to this workspace.`, + }), ); try { @@ -269,7 +264,9 @@ describe('JwtAuthStrategy', () => { ); await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow( - new AuthException('UserWorkspaceEntity not found', expect.any(String)), + new AuthException('UserWorkspaceEntity not found', expect.any(String), { + userFriendlyMessage: msg`User does not have access to this workspace.`, + }), ); try { @@ -345,7 +342,9 @@ describe('JwtAuthStrategy', () => { ); await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow( - new AuthException('Application not found', expect.any(String)), + new AuthException('Application not found', expect.any(String), { + userFriendlyMessage: msg`Application not found.`, + }), ); try { @@ -552,6 +551,7 @@ describe('JwtAuthStrategy', () => { new AuthException( 'Invalid impersonation token, cannot find impersonator or impersonated user workspace', AuthExceptionCode.USER_WORKSPACE_NOT_FOUND, + { userFriendlyMessage: msg`User workspace not found.` }, ), ); }); diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts index e20c7c81f9..d08c217c91 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts @@ -2,6 +2,7 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -33,35 +34,61 @@ export enum BillingExceptionCode { BILLING_CREDITS_EXHAUSTED = 'BILLING_CREDITS_EXHAUSTED', } -const billingExceptionUserFriendlyMessages: Record< - BillingExceptionCode, - MessageDescriptor -> = { - [BillingExceptionCode.BILLING_CUSTOMER_NOT_FOUND]: msg`Billing customer not found.`, - [BillingExceptionCode.BILLING_PLAN_NOT_FOUND]: msg`Billing plan not found.`, - [BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND]: msg`Billing product not found.`, - [BillingExceptionCode.BILLING_PRICE_NOT_FOUND]: msg`Billing price not found.`, - [BillingExceptionCode.BILLING_METER_NOT_FOUND]: msg`Billing meter not found.`, - [BillingExceptionCode.BILLING_SUBSCRIPTION_NOT_FOUND]: msg`Subscription not found.`, - [BillingExceptionCode.BILLING_SUBSCRIPTION_ITEM_NOT_FOUND]: msg`Subscription item not found.`, - [BillingExceptionCode.BILLING_SUBSCRIPTION_INVALID]: msg`Invalid subscription.`, - [BillingExceptionCode.BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND]: msg`Workspace not found for subscription event.`, - [BillingExceptionCode.BILLING_CUSTOMER_EVENT_WORKSPACE_NOT_FOUND]: msg`Workspace not found for customer event.`, - [BillingExceptionCode.BILLING_ACTIVE_SUBSCRIPTION_NOT_FOUND]: msg`No active subscription found.`, - [BillingExceptionCode.BILLING_METER_EVENT_FAILED]: msg`Failed to record billing event.`, - [BillingExceptionCode.BILLING_MISSING_REQUEST_BODY]: msg`Missing request body.`, - [BillingExceptionCode.BILLING_UNHANDLED_ERROR]: msg`An unexpected billing error occurred.`, - [BillingExceptionCode.BILLING_STRIPE_ERROR]: msg`A payment processing error occurred.`, - [BillingExceptionCode.BILLING_SUBSCRIPTION_NOT_IN_TRIAL_PERIOD]: msg`Subscription is not in trial period.`, - [BillingExceptionCode.BILLING_SUBSCRIPTION_INTERVAL_NOT_SWITCHABLE]: msg`Cannot switch subscription interval.`, - [BillingExceptionCode.BILLING_SUBSCRIPTION_INTERVAL_INVALID]: msg`Invalid subscription interval.`, - [BillingExceptionCode.BILLING_SUBSCRIPTION_PLAN_NOT_SWITCHABLE]: msg`Cannot switch subscription plan.`, - [BillingExceptionCode.BILLING_SUBSCRIPTION_ITEM_INVALID]: msg`Invalid subscription item.`, - [BillingExceptionCode.BILLING_PRICE_INVALID_TIERS]: msg`Invalid pricing tiers.`, - [BillingExceptionCode.BILLING_PRICE_INVALID]: msg`Invalid price.`, - [BillingExceptionCode.BILLING_SUBSCRIPTION_PHASE_NOT_FOUND]: msg`Subscription phase not found.`, - [BillingExceptionCode.BILLING_TOO_MUCH_SUBSCRIPTIONS_FOUND]: msg`Multiple subscriptions found where one was expected.`, - [BillingExceptionCode.BILLING_CREDITS_EXHAUSTED]: msg`You have exhausted your credits. Please upgrade your plan to continue.`, +const getBillingExceptionUserFriendlyMessage = (code: BillingExceptionCode) => { + switch (code) { + case BillingExceptionCode.BILLING_CUSTOMER_NOT_FOUND: + return msg`Billing customer not found.`; + case BillingExceptionCode.BILLING_PLAN_NOT_FOUND: + return msg`Billing plan not found.`; + case BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND: + return msg`Billing product not found.`; + case BillingExceptionCode.BILLING_PRICE_NOT_FOUND: + return msg`Billing price not found.`; + case BillingExceptionCode.BILLING_METER_NOT_FOUND: + return msg`Billing meter not found.`; + case BillingExceptionCode.BILLING_SUBSCRIPTION_NOT_FOUND: + return msg`Subscription not found.`; + case BillingExceptionCode.BILLING_SUBSCRIPTION_ITEM_NOT_FOUND: + return msg`Subscription item not found.`; + case BillingExceptionCode.BILLING_SUBSCRIPTION_INVALID: + return msg`Invalid subscription.`; + case BillingExceptionCode.BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND: + return msg`Workspace not found for subscription event.`; + case BillingExceptionCode.BILLING_CUSTOMER_EVENT_WORKSPACE_NOT_FOUND: + return msg`Workspace not found for customer event.`; + case BillingExceptionCode.BILLING_ACTIVE_SUBSCRIPTION_NOT_FOUND: + return msg`No active subscription found.`; + case BillingExceptionCode.BILLING_METER_EVENT_FAILED: + return msg`Failed to record billing event.`; + case BillingExceptionCode.BILLING_MISSING_REQUEST_BODY: + return msg`Missing request body.`; + case BillingExceptionCode.BILLING_UNHANDLED_ERROR: + return msg`An unexpected billing error occurred.`; + case BillingExceptionCode.BILLING_STRIPE_ERROR: + return msg`A payment processing error occurred.`; + case BillingExceptionCode.BILLING_SUBSCRIPTION_NOT_IN_TRIAL_PERIOD: + return msg`Subscription is not in trial period.`; + case BillingExceptionCode.BILLING_SUBSCRIPTION_INTERVAL_NOT_SWITCHABLE: + return msg`Cannot switch subscription interval.`; + case BillingExceptionCode.BILLING_SUBSCRIPTION_INTERVAL_INVALID: + return msg`Invalid subscription interval.`; + case BillingExceptionCode.BILLING_SUBSCRIPTION_PLAN_NOT_SWITCHABLE: + return msg`Cannot switch subscription plan.`; + case BillingExceptionCode.BILLING_SUBSCRIPTION_ITEM_INVALID: + return msg`Invalid subscription item.`; + case BillingExceptionCode.BILLING_PRICE_INVALID_TIERS: + return msg`Invalid pricing tiers.`; + case BillingExceptionCode.BILLING_PRICE_INVALID: + return msg`Invalid price.`; + case BillingExceptionCode.BILLING_SUBSCRIPTION_PHASE_NOT_FOUND: + return msg`Subscription phase not found.`; + case BillingExceptionCode.BILLING_TOO_MUCH_SUBSCRIPTIONS_FOUND: + return msg`Multiple subscriptions found where one was expected.`; + case BillingExceptionCode.BILLING_CREDITS_EXHAUSTED: + return msg`You have exhausted your credits. Please upgrade your plan to continue.`; + default: + assertUnreachable(code); + } }; export class BillingException extends CustomException { @@ -72,7 +99,7 @@ export class BillingException extends CustomException { ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? billingExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getBillingExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/core-modules/captcha/captcha.exception.ts b/packages/twenty-server/src/engine/core-modules/captcha/captcha.exception.ts index f7578b7d29..325bdad486 100644 --- a/packages/twenty-server/src/engine/core-modules/captcha/captcha.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/captcha/captcha.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -7,11 +8,13 @@ export enum CaptchaExceptionCode { INVALID_CAPTCHA = 'INVALID_CAPTCHA', } -const captchaExceptionUserFriendlyMessages: Record< - CaptchaExceptionCode, - MessageDescriptor -> = { - [CaptchaExceptionCode.INVALID_CAPTCHA]: msg`Invalid captcha. Please try again.`, +const getCaptchaExceptionUserFriendlyMessage = (code: CaptchaExceptionCode) => { + switch (code) { + case CaptchaExceptionCode.INVALID_CAPTCHA: + return msg`Invalid captcha. Please try again.`; + default: + assertUnreachable(code); + } }; export class CaptchaException extends CustomException { @@ -22,7 +25,7 @@ export class CaptchaException extends CustomException { ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? captchaExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getCaptchaExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.exception.ts b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.exception.ts index ad0c2c4977..52c7351880 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -14,18 +15,27 @@ export enum EmailVerificationExceptionCode { RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED', } -const emailVerificationExceptionUserFriendlyMessages: Record< - EmailVerificationExceptionCode, - MessageDescriptor -> = { - [EmailVerificationExceptionCode.EMAIL_VERIFICATION_NOT_REQUIRED]: msg`Email verification is not required.`, - [EmailVerificationExceptionCode.INVALID_TOKEN]: msg`Invalid verification token.`, - [EmailVerificationExceptionCode.INVALID_APP_TOKEN_TYPE]: msg`Invalid token type.`, - [EmailVerificationExceptionCode.TOKEN_EXPIRED]: msg`Verification token has expired.`, - [EmailVerificationExceptionCode.EMAIL_MISSING]: msg`Email is required.`, - [EmailVerificationExceptionCode.EMAIL_ALREADY_VERIFIED]: msg`Email is already verified.`, - [EmailVerificationExceptionCode.INVALID_EMAIL]: msg`Invalid email address.`, - [EmailVerificationExceptionCode.RATE_LIMIT_EXCEEDED]: msg`Too many requests. Please try again later.`, +const getEmailVerificationExceptionUserFriendlyMessage = ( + code: EmailVerificationExceptionCode, +) => { + switch (code) { + case EmailVerificationExceptionCode.EMAIL_VERIFICATION_NOT_REQUIRED: + return msg`Email verification is not required.`; + case EmailVerificationExceptionCode.INVALID_TOKEN: + case EmailVerificationExceptionCode.INVALID_APP_TOKEN_TYPE: + case EmailVerificationExceptionCode.TOKEN_EXPIRED: + return msg`There is an issue with your token. Please try again.`; + case EmailVerificationExceptionCode.EMAIL_MISSING: + return msg`Email is required.`; + case EmailVerificationExceptionCode.EMAIL_ALREADY_VERIFIED: + return msg`Email is already verified.`; + case EmailVerificationExceptionCode.INVALID_EMAIL: + return msg`Invalid email address.`; + case EmailVerificationExceptionCode.RATE_LIMIT_EXCEEDED: + return msg`Too many requests. Please try again later.`; + default: + assertUnreachable(code); + } }; export class EmailVerificationException extends CustomException { @@ -37,7 +47,7 @@ export class EmailVerificationException extends CustomException = { - [EmailingDomainDriverExceptionCode.NOT_FOUND]: msg`Email domain not found.`, - [EmailingDomainDriverExceptionCode.TEMPORARY_ERROR]: msg`A temporary error occurred. Please try again.`, - [EmailingDomainDriverExceptionCode.INSUFFICIENT_PERMISSIONS]: msg`Insufficient permissions for email domain.`, - [EmailingDomainDriverExceptionCode.CONFIGURATION_ERROR]: msg`Email domain configuration error.`, - [EmailingDomainDriverExceptionCode.UNKNOWN]: msg`An unknown email error occurred.`, +const getEmailingDomainDriverExceptionUserFriendlyMessage = ( + code: EmailingDomainDriverExceptionCode, +) => { + switch (code) { + case EmailingDomainDriverExceptionCode.NOT_FOUND: + return msg`Email domain not found.`; + case EmailingDomainDriverExceptionCode.INSUFFICIENT_PERMISSIONS: + return msg`Insufficient permissions for email domain.`; + case EmailingDomainDriverExceptionCode.CONFIGURATION_ERROR: + return msg`Email domain configuration error.`; + case EmailingDomainDriverExceptionCode.TEMPORARY_ERROR: + case EmailingDomainDriverExceptionCode.UNKNOWN: + return STANDARD_ERROR_MESSAGE; + default: + assertUnreachable(code); + } }; export class EmailingDomainDriverException extends CustomException { @@ -31,7 +40,7 @@ export class EmailingDomainDriverException extends CustomException = { - [FeatureFlagExceptionCode.INVALID_FEATURE_FLAG_KEY]: msg`Invalid feature flag key.`, +const getFeatureFlagExceptionUserFriendlyMessage = ( + code: FeatureFlagExceptionCode, +) => { + switch (code) { + case FeatureFlagExceptionCode.INVALID_FEATURE_FLAG_KEY: + return msg`Invalid feature flag key.`; + default: + assertUnreachable(code); + } }; export class FeatureFlagException extends CustomException { @@ -22,7 +27,7 @@ export class FeatureFlagException extends CustomException = { - [FileStorageExceptionCode.FILE_NOT_FOUND]: msg`File not found.`, +const getFileStorageExceptionUserFriendlyMessage = ( + code: FileStorageExceptionCode, +) => { + switch (code) { + case FileStorageExceptionCode.FILE_NOT_FOUND: + return msg`File not found.`; + default: + assertUnreachable(code); + } }; export class FileStorageException extends CustomException { @@ -22,7 +27,7 @@ export class FileStorageException extends CustomException = { - UNAUTHENTICATED: msg`Authentication is required.`, - FILE_NOT_FOUND: msg`File not found.`, - INTERNAL_SERVER_ERROR: msg`An unexpected error occurred.`, +const getFileExceptionUserFriendlyMessage = ( + code: keyof typeof FileExceptionCode, +) => { + switch (code) { + case FileExceptionCode.UNAUTHENTICATED: + return msg`Authentication is required.`; + case FileExceptionCode.FILE_NOT_FOUND: + return msg`File not found.`; + case FileExceptionCode.INTERNAL_SERVER_ERROR: + return STANDARD_ERROR_MESSAGE; + default: + assertUnreachable(code); + } }; export class FileException extends CustomException< @@ -30,7 +38,7 @@ export class FileException extends CustomException< ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? fileExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getFileExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/core-modules/public-domain/public-domain.exception.ts b/packages/twenty-server/src/engine/core-modules/public-domain/public-domain.exception.ts index a42bf01a3d..aca6b2e5de 100644 --- a/packages/twenty-server/src/engine/core-modules/public-domain/public-domain.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/public-domain/public-domain.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -9,13 +10,19 @@ export enum PublicDomainExceptionCode { PUBLIC_DOMAIN_NOT_FOUND = 'PUBLIC_DOMAIN_NOT_FOUND', } -const publicDomainExceptionUserFriendlyMessages: Record< - PublicDomainExceptionCode, - MessageDescriptor -> = { - [PublicDomainExceptionCode.PUBLIC_DOMAIN_ALREADY_REGISTERED]: msg`This public domain is already registered.`, - [PublicDomainExceptionCode.DOMAIN_ALREADY_REGISTERED_AS_CUSTOM_DOMAIN]: msg`This domain is already registered as a custom domain.`, - [PublicDomainExceptionCode.PUBLIC_DOMAIN_NOT_FOUND]: msg`Public domain not found.`, +const getPublicDomainExceptionUserFriendlyMessage = ( + code: PublicDomainExceptionCode, +) => { + switch (code) { + case PublicDomainExceptionCode.PUBLIC_DOMAIN_ALREADY_REGISTERED: + return msg`This public domain is already registered.`; + case PublicDomainExceptionCode.DOMAIN_ALREADY_REGISTERED_AS_CUSTOM_DOMAIN: + return msg`This domain is already registered as a custom domain.`; + case PublicDomainExceptionCode.PUBLIC_DOMAIN_NOT_FOUND: + return msg`Public domain not found.`; + default: + assertUnreachable(code); + } }; export class PublicDomainException extends CustomException { @@ -26,7 +33,8 @@ export class PublicDomainException extends CustomException = { - [RecordCrudExceptionCode.INVALID_REQUEST]: msg`Invalid request.`, - [RecordCrudExceptionCode.WORKSPACE_ID_NOT_FOUND]: msg`Workspace not found.`, - [RecordCrudExceptionCode.OBJECT_NOT_FOUND]: msg`Object not found.`, - [RecordCrudExceptionCode.RECORD_NOT_FOUND]: msg`Record not found.`, - [RecordCrudExceptionCode.RECORD_CREATION_FAILED]: msg`Failed to create record.`, - [RecordCrudExceptionCode.RECORD_UPDATE_FAILED]: msg`Failed to update record.`, - [RecordCrudExceptionCode.RECORD_DELETION_FAILED]: msg`Failed to delete record.`, - [RecordCrudExceptionCode.RECORD_UPSERT_FAILED]: msg`Failed to upsert record.`, - [RecordCrudExceptionCode.QUERY_FAILED]: msg`Query failed.`, +const getRecordCrudExceptionUserFriendlyMessage = ( + code: RecordCrudExceptionCode, +) => { + switch (code) { + case RecordCrudExceptionCode.INVALID_REQUEST: + return msg`Invalid request.`; + case RecordCrudExceptionCode.WORKSPACE_ID_NOT_FOUND: + return msg`Workspace not found.`; + case RecordCrudExceptionCode.OBJECT_NOT_FOUND: + return msg`Object not found.`; + case RecordCrudExceptionCode.RECORD_NOT_FOUND: + return msg`Record not found.`; + case RecordCrudExceptionCode.RECORD_CREATION_FAILED: + return msg`Failed to create record.`; + case RecordCrudExceptionCode.RECORD_UPDATE_FAILED: + return msg`Failed to update record.`; + case RecordCrudExceptionCode.RECORD_DELETION_FAILED: + return msg`Failed to delete record.`; + case RecordCrudExceptionCode.RECORD_UPSERT_FAILED: + return msg`Failed to upsert record.`; + case RecordCrudExceptionCode.QUERY_FAILED: + return msg`Query failed.`; + default: + assertUnreachable(code); + } }; export class RecordCrudException extends CustomException { @@ -38,7 +51,7 @@ export class RecordCrudException extends CustomException = { - [RecordTransformerExceptionCode.INVALID_URL]: msg`Invalid URL format.`, - [RecordTransformerExceptionCode.INVALID_PHONE_NUMBER]: msg`Invalid phone number.`, - [RecordTransformerExceptionCode.INVALID_PHONE_COUNTRY_CODE]: msg`Invalid phone country code.`, - [RecordTransformerExceptionCode.INVALID_PHONE_CALLING_CODE]: msg`Invalid phone calling code.`, - [RecordTransformerExceptionCode.CONFLICTING_PHONE_COUNTRY_CODE]: msg`Conflicting phone country code.`, - [RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE]: msg`Conflicting phone calling code.`, - [RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE_AND_COUNTRY_CODE]: msg`Conflicting phone calling code and country code.`, +const getRecordTransformerExceptionUserFriendlyMessage = ( + code: RecordTransformerExceptionCode, +) => { + switch (code) { + case RecordTransformerExceptionCode.INVALID_URL: + return msg`Invalid URL format.`; + case RecordTransformerExceptionCode.INVALID_PHONE_NUMBER: + return msg`Invalid phone number.`; + case RecordTransformerExceptionCode.INVALID_PHONE_COUNTRY_CODE: + return msg`Invalid phone country code.`; + case RecordTransformerExceptionCode.INVALID_PHONE_CALLING_CODE: + return msg`Invalid phone calling code.`; + case RecordTransformerExceptionCode.CONFLICTING_PHONE_COUNTRY_CODE: + return msg`Conflicting phone country code.`; + case RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE: + return msg`Conflicting phone calling code.`; + case RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE_AND_COUNTRY_CODE: + return msg`Conflicting phone calling code and country code.`; + default: + assertUnreachable(code); + } }; export class RecordTransformerException extends CustomException { @@ -35,7 +46,7 @@ export class RecordTransformerException extends CustomException = { - [SearchExceptionCode.LABEL_IDENTIFIER_FIELD_NOT_FOUND]: msg`Label identifier field not found.`, - [SearchExceptionCode.OBJECT_METADATA_NOT_FOUND]: msg`Object not found.`, +const getSearchExceptionUserFriendlyMessage = (code: SearchExceptionCode) => { + switch (code) { + case SearchExceptionCode.LABEL_IDENTIFIER_FIELD_NOT_FOUND: + return msg`No identifier to search by was found.`; + case SearchExceptionCode.OBJECT_METADATA_NOT_FOUND: + return msg`Object not found.`; + default: + assertUnreachable(code); + } }; export class SearchException extends CustomException { @@ -24,7 +28,7 @@ export class SearchException extends CustomException { ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? searchExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getSearchExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/core-modules/sso/sso.exception.ts b/packages/twenty-server/src/engine/core-modules/sso/sso.exception.ts index 4f7dd1f636..2ef3ed0ca4 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/sso.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/sso.exception.ts @@ -2,6 +2,7 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -14,16 +15,23 @@ export enum SSOExceptionCode { SSO_DISABLE = 'SSO_DISABLE', } -const ssoExceptionUserFriendlyMessages: Record< - SSOExceptionCode, - MessageDescriptor -> = { - [SSOExceptionCode.USER_NOT_FOUND]: msg`User not found.`, - [SSOExceptionCode.IDENTITY_PROVIDER_NOT_FOUND]: msg`Identity provider not found.`, - [SSOExceptionCode.INVALID_ISSUER_URL]: msg`Invalid issuer URL.`, - [SSOExceptionCode.INVALID_IDP_TYPE]: msg`Invalid identity provider type.`, - [SSOExceptionCode.UNKNOWN_SSO_CONFIGURATION_ERROR]: msg`SSO configuration error.`, - [SSOExceptionCode.SSO_DISABLE]: msg`SSO is disabled.`, +const getSSOExceptionUserFriendlyMessage = (code: SSOExceptionCode) => { + switch (code) { + case SSOExceptionCode.USER_NOT_FOUND: + return msg`User not found.`; + case SSOExceptionCode.IDENTITY_PROVIDER_NOT_FOUND: + return msg`Identity provider not found.`; + case SSOExceptionCode.INVALID_ISSUER_URL: + return msg`Invalid issuer URL.`; + case SSOExceptionCode.INVALID_IDP_TYPE: + return msg`Invalid identity provider type.`; + case SSOExceptionCode.UNKNOWN_SSO_CONFIGURATION_ERROR: + return msg`SSO configuration error.`; + case SSOExceptionCode.SSO_DISABLE: + return msg`SSO is disabled.`; + default: + assertUnreachable(code); + } }; export class SSOException extends CustomException { @@ -34,7 +42,7 @@ export class SSOException extends CustomException { ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? ssoExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getSSOExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/core-modules/throttler/throttler.exception.ts b/packages/twenty-server/src/engine/core-modules/throttler/throttler.exception.ts index ac5ef8fb36..4010f0cd2a 100644 --- a/packages/twenty-server/src/engine/core-modules/throttler/throttler.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/throttler/throttler.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -7,11 +8,15 @@ export enum ThrottlerExceptionCode { LIMIT_REACHED = 'LIMIT_REACHED', } -const throttlerExceptionUserFriendlyMessages: Record< - ThrottlerExceptionCode, - MessageDescriptor -> = { - [ThrottlerExceptionCode.LIMIT_REACHED]: msg`Rate limit reached. Please try again later.`, +const getThrottlerExceptionUserFriendlyMessage = ( + code: ThrottlerExceptionCode, +) => { + switch (code) { + case ThrottlerExceptionCode.LIMIT_REACHED: + return msg`Rate limit reached. Please try again later.`; + default: + assertUnreachable(code); + } }; export class ThrottlerException extends CustomException { @@ -22,7 +27,7 @@ export class ThrottlerException extends CustomException ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? throttlerExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getThrottlerExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception.ts index 05070e9634..eb761f8fad 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -12,16 +13,25 @@ export enum SendEmailToolExceptionCode { INVALID_FILE_ID = 'INVALID_FILE_ID', } -const sendEmailToolExceptionUserFriendlyMessages: Record< - SendEmailToolExceptionCode, - MessageDescriptor -> = { - [SendEmailToolExceptionCode.INVALID_CONNECTED_ACCOUNT_ID]: msg`Invalid connected account ID.`, - [SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND]: msg`Connected account not found.`, - [SendEmailToolExceptionCode.INVALID_EMAIL]: msg`Invalid email address.`, - [SendEmailToolExceptionCode.WORKSPACE_ID_NOT_FOUND]: msg`Workspace not found.`, - [SendEmailToolExceptionCode.FILE_NOT_FOUND]: msg`File not found.`, - [SendEmailToolExceptionCode.INVALID_FILE_ID]: msg`Invalid file ID.`, +const getSendEmailToolExceptionUserFriendlyMessage = ( + code: SendEmailToolExceptionCode, +) => { + switch (code) { + case SendEmailToolExceptionCode.INVALID_CONNECTED_ACCOUNT_ID: + return msg`Invalid connected account ID.`; + case SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND: + return msg`Connected account not found.`; + case SendEmailToolExceptionCode.INVALID_EMAIL: + return msg`Invalid email address.`; + case SendEmailToolExceptionCode.WORKSPACE_ID_NOT_FOUND: + return msg`Workspace not found.`; + case SendEmailToolExceptionCode.FILE_NOT_FOUND: + return msg`File not found.`; + case SendEmailToolExceptionCode.INVALID_FILE_ID: + return msg`Invalid file ID.`; + default: + assertUnreachable(code); + } }; export class SendEmailToolException extends CustomException { @@ -32,7 +42,8 @@ export class SendEmailToolException extends CustomException = { - [ConfigVariableExceptionCode.DATABASE_CONFIG_DISABLED]: msg`Database configuration is disabled.`, - [ConfigVariableExceptionCode.ENVIRONMENT_ONLY_VARIABLE]: msg`This variable can only be set via environment.`, - [ConfigVariableExceptionCode.VARIABLE_NOT_FOUND]: msg`Configuration variable not found.`, - [ConfigVariableExceptionCode.VALIDATION_FAILED]: msg`Configuration validation failed.`, - [ConfigVariableExceptionCode.UNSUPPORTED_CONFIG_TYPE]: msg`Unsupported configuration type.`, - [ConfigVariableExceptionCode.INTERNAL_ERROR]: msg`An unexpected configuration error occurred.`, +const getConfigVariableExceptionUserFriendlyMessage = ( + code: ConfigVariableExceptionCode, +) => { + switch (code) { + case ConfigVariableExceptionCode.DATABASE_CONFIG_DISABLED: + return msg`Database configuration is disabled.`; + case ConfigVariableExceptionCode.ENVIRONMENT_ONLY_VARIABLE: + return msg`This variable can only be set via environment.`; + case ConfigVariableExceptionCode.VARIABLE_NOT_FOUND: + return msg`Configuration variable not found.`; + case ConfigVariableExceptionCode.VALIDATION_FAILED: + return msg`Configuration validation failed.`; + case ConfigVariableExceptionCode.UNSUPPORTED_CONFIG_TYPE: + return msg`Unsupported configuration type.`; + case ConfigVariableExceptionCode.INTERNAL_ERROR: + return msg`An unexpected configuration error occurred.`; + default: + assertUnreachable(code); + } }; export class ConfigVariableException extends CustomException { @@ -33,7 +43,7 @@ export class ConfigVariableException extends CustomException = { - [TwoFactorAuthenticationExceptionCode.INVALID_CONFIGURATION]: msg`Invalid two-factor authentication configuration.`, - [TwoFactorAuthenticationExceptionCode.TWO_FACTOR_AUTHENTICATION_METHOD_NOT_FOUND]: msg`Two-factor authentication method not found.`, - [TwoFactorAuthenticationExceptionCode.INVALID_OTP]: msg`Invalid verification code.`, - [TwoFactorAuthenticationExceptionCode.TWO_FACTOR_AUTHENTICATION_METHOD_ALREADY_PROVISIONED]: msg`Two-factor authentication is already set up.`, - [TwoFactorAuthenticationExceptionCode.MALFORMED_DATABASE_OBJECT]: msg`An error occurred with two-factor authentication data.`, +const getTwoFactorAuthenticationExceptionUserFriendlyMessage = ( + code: TwoFactorAuthenticationExceptionCode, +) => { + switch (code) { + case TwoFactorAuthenticationExceptionCode.INVALID_CONFIGURATION: + return msg`Invalid two-factor authentication configuration.`; + case TwoFactorAuthenticationExceptionCode.TWO_FACTOR_AUTHENTICATION_METHOD_NOT_FOUND: + return msg`Two-factor authentication method not found.`; + case TwoFactorAuthenticationExceptionCode.INVALID_OTP: + return msg`Invalid verification code.`; + case TwoFactorAuthenticationExceptionCode.TWO_FACTOR_AUTHENTICATION_METHOD_ALREADY_PROVISIONED: + return msg`Two-factor authentication is already set up.`; + case TwoFactorAuthenticationExceptionCode.MALFORMED_DATABASE_OBJECT: + return msg`An error occurred with two-factor authentication data.`; + default: + assertUnreachable(code); + } }; export class TwoFactorAuthenticationException extends CustomException { @@ -31,7 +40,7 @@ export class TwoFactorAuthenticationException extends CustomException = { - [UserWorkspaceExceptionCode.USER_WORKSPACE_NOT_FOUND]: msg`User workspace not found.`, +const getUserWorkspaceExceptionUserFriendlyMessage = ( + code: UserWorkspaceExceptionCode, +) => { + switch (code) { + case UserWorkspaceExceptionCode.USER_WORKSPACE_NOT_FOUND: + return msg`User workspace not found.`; + default: + assertUnreachable(code); + } }; export class UserWorkspaceException extends CustomException { @@ -22,7 +27,8 @@ export class UserWorkspaceException extends CustomException = { - [UserExceptionCode.USER_NOT_FOUND]: msg`User not found.`, - [UserExceptionCode.EMAIL_ALREADY_IN_USE]: msg`This email is already in use.`, - [UserExceptionCode.EMAIL_UNCHANGED]: msg`Email is unchanged.`, - [UserExceptionCode.EMAIL_UPDATE_RESTRICTED_TO_SINGLE_WORKSPACE]: msg`Email update is restricted to single workspace users.`, +const getUserExceptionUserFriendlyMessage = (code: UserExceptionCode) => { + switch (code) { + case UserExceptionCode.USER_NOT_FOUND: + return msg`User not found.`; + case UserExceptionCode.EMAIL_ALREADY_IN_USE: + return msg`This email is already in use.`; + case UserExceptionCode.EMAIL_UNCHANGED: + return msg`Email is unchanged.`; + case UserExceptionCode.EMAIL_UPDATE_RESTRICTED_TO_SINGLE_WORKSPACE: + return msg`Email update is restricted to single workspace users.`; + default: + assertUnreachable(code); + } }; export class UserException extends CustomException { @@ -28,7 +34,7 @@ export class UserException extends CustomException { ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? userExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getUserExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/core-modules/webhook/webhook.exception.ts b/packages/twenty-server/src/engine/core-modules/webhook/webhook.exception.ts index 2a40cd705c..311b54ebd8 100644 --- a/packages/twenty-server/src/engine/core-modules/webhook/webhook.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/webhook/webhook.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -8,12 +9,15 @@ export enum WebhookExceptionCode { INVALID_TARGET_URL = 'INVALID_TARGET_URL', } -const webhookExceptionUserFriendlyMessages: Record< - WebhookExceptionCode, - MessageDescriptor -> = { - [WebhookExceptionCode.WEBHOOK_NOT_FOUND]: msg`Webhook not found.`, - [WebhookExceptionCode.INVALID_TARGET_URL]: msg`Invalid target URL.`, +const getWebhookExceptionUserFriendlyMessage = (code: WebhookExceptionCode) => { + switch (code) { + case WebhookExceptionCode.WEBHOOK_NOT_FOUND: + return msg`Webhook not found.`; + case WebhookExceptionCode.INVALID_TARGET_URL: + return msg`Invalid target URL.`; + default: + assertUnreachable(code); + } }; export class WebhookException extends CustomException { @@ -24,7 +28,7 @@ export class WebhookException extends CustomException { ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? webhookExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getWebhookExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.exception.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.exception.ts index ae7d06efbd..1df15537db 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -12,16 +13,23 @@ export enum WorkspaceInvitationExceptionCode { EMAIL_MISSING = 'EMAIL_MISSING', } -const workspaceInvitationExceptionUserFriendlyMessages: Record< - WorkspaceInvitationExceptionCode, - MessageDescriptor -> = { - [WorkspaceInvitationExceptionCode.INVALID_APP_TOKEN_TYPE]: msg`Invalid token type.`, - [WorkspaceInvitationExceptionCode.INVITATION_CORRUPTED]: msg`Invitation is corrupted.`, - [WorkspaceInvitationExceptionCode.INVITATION_ALREADY_EXIST]: msg`An invitation has already been sent to this email.`, - [WorkspaceInvitationExceptionCode.USER_ALREADY_EXIST]: msg`This user is already a member of the workspace.`, - [WorkspaceInvitationExceptionCode.INVALID_INVITATION]: msg`Invalid invitation.`, - [WorkspaceInvitationExceptionCode.EMAIL_MISSING]: msg`Email is required.`, +const getWorkspaceInvitationExceptionUserFriendlyMessage = ( + code: WorkspaceInvitationExceptionCode, +) => { + switch (code) { + case WorkspaceInvitationExceptionCode.INVALID_APP_TOKEN_TYPE: + case WorkspaceInvitationExceptionCode.INVITATION_CORRUPTED: + case WorkspaceInvitationExceptionCode.INVALID_INVITATION: + return msg`There is an issue with your invitation. Please try again.`; + case WorkspaceInvitationExceptionCode.INVITATION_ALREADY_EXIST: + return msg`An invitation has already been sent to this email.`; + case WorkspaceInvitationExceptionCode.USER_ALREADY_EXIST: + return msg`This user is already a member of the workspace.`; + case WorkspaceInvitationExceptionCode.EMAIL_MISSING: + return msg`Email is required.`; + default: + assertUnreachable(code); + } }; export class WorkspaceInvitationException extends CustomException { @@ -33,7 +41,7 @@ export class WorkspaceInvitationException extends CustomException = { - [WorkspaceExceptionCode.SUBDOMAIN_NOT_FOUND]: msg`Subdomain not found.`, - [WorkspaceExceptionCode.SUBDOMAIN_ALREADY_TAKEN]: msg`This subdomain is already taken.`, - [WorkspaceExceptionCode.SUBDOMAIN_NOT_VALID]: msg`Invalid subdomain.`, - [WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN]: msg`This domain is already taken.`, - [WorkspaceExceptionCode.WORKSPACE_NOT_FOUND]: msg`Workspace not found.`, - [WorkspaceExceptionCode.WORKSPACE_CUSTOM_DOMAIN_DISABLED]: msg`Custom domains are disabled for this workspace.`, - [WorkspaceExceptionCode.ENVIRONMENT_VAR_NOT_ENABLED]: msg`This feature is not enabled.`, - [WorkspaceExceptionCode.CUSTOM_DOMAIN_NOT_FOUND]: msg`Custom domain not found.`, +const getWorkspaceExceptionUserFriendlyMessage = ( + code: WorkspaceExceptionCode, +) => { + switch (code) { + case WorkspaceExceptionCode.SUBDOMAIN_NOT_FOUND: + return msg`Subdomain not found.`; + case WorkspaceExceptionCode.SUBDOMAIN_ALREADY_TAKEN: + return msg`This subdomain is already taken.`; + case WorkspaceExceptionCode.SUBDOMAIN_NOT_VALID: + return msg`Invalid subdomain.`; + case WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN: + return msg`This domain is already taken.`; + case WorkspaceExceptionCode.WORKSPACE_NOT_FOUND: + return msg`Workspace not found.`; + case WorkspaceExceptionCode.WORKSPACE_CUSTOM_DOMAIN_DISABLED: + return msg`Custom domains are disabled for this workspace.`; + case WorkspaceExceptionCode.ENVIRONMENT_VAR_NOT_ENABLED: + return msg`This feature is not enabled.`; + case WorkspaceExceptionCode.CUSTOM_DOMAIN_NOT_FOUND: + return msg`Custom domain not found.`; + default: + assertUnreachable(code); + } }; export class WorkspaceException extends CustomException { @@ -36,7 +48,7 @@ export class WorkspaceException extends CustomException ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? workspaceExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getWorkspaceExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/agent.exception.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/agent.exception.ts index b3dd060cad..a152a3da69 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/agent.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/agent.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -15,19 +16,29 @@ export enum AgentExceptionCode { AGENT_IS_STANDARD = 'AGENT_IS_STANDARD', } -const agentExceptionUserFriendlyMessages: Record< - AgentExceptionCode, - MessageDescriptor -> = { - [AgentExceptionCode.AGENT_NOT_FOUND]: msg`Agent not found.`, - [AgentExceptionCode.AGENT_EXECUTION_FAILED]: msg`Agent execution failed.`, - [AgentExceptionCode.API_KEY_NOT_CONFIGURED]: msg`API key is not configured.`, - [AgentExceptionCode.USER_WORKSPACE_ID_NOT_FOUND]: msg`User workspace not found.`, - [AgentExceptionCode.ROLE_NOT_FOUND]: msg`Role not found.`, - [AgentExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS]: msg`This role cannot be assigned to agents.`, - [AgentExceptionCode.INVALID_AGENT_INPUT]: msg`Invalid agent input.`, - [AgentExceptionCode.AGENT_ALREADY_EXISTS]: msg`An agent with this name already exists.`, - [AgentExceptionCode.AGENT_IS_STANDARD]: msg`Standard agents cannot be modified.`, +const getAgentExceptionUserFriendlyMessage = (code: AgentExceptionCode) => { + switch (code) { + case AgentExceptionCode.AGENT_NOT_FOUND: + return msg`Agent not found.`; + case AgentExceptionCode.AGENT_EXECUTION_FAILED: + return msg`Agent execution failed.`; + case AgentExceptionCode.API_KEY_NOT_CONFIGURED: + return msg`API key is not configured.`; + case AgentExceptionCode.USER_WORKSPACE_ID_NOT_FOUND: + return msg`User workspace not found.`; + case AgentExceptionCode.ROLE_NOT_FOUND: + return msg`Role not found.`; + case AgentExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS: + return msg`This role cannot be assigned to agents.`; + case AgentExceptionCode.INVALID_AGENT_INPUT: + return msg`Invalid agent input.`; + case AgentExceptionCode.AGENT_ALREADY_EXISTS: + return msg`An agent with this name already exists.`; + case AgentExceptionCode.AGENT_IS_STANDARD: + return msg`Standard agents cannot be modified.`; + default: + assertUnreachable(code); + } }; export class AgentException extends CustomException { @@ -38,7 +49,7 @@ export class AgentException extends CustomException { ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? agentExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getAgentExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception.ts b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception.ts index 414f656136..a136349d97 100644 --- a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -9,13 +10,19 @@ export enum CronTriggerExceptionCode { SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND', } -const cronTriggerExceptionUserFriendlyMessages: Record< - CronTriggerExceptionCode, - MessageDescriptor -> = { - [CronTriggerExceptionCode.CRON_TRIGGER_NOT_FOUND]: msg`Cron trigger not found.`, - [CronTriggerExceptionCode.CRON_TRIGGER_ALREADY_EXIST]: msg`Cron trigger already exists.`, - [CronTriggerExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND]: msg`Serverless function not found.`, +const getCronTriggerExceptionUserFriendlyMessage = ( + code: CronTriggerExceptionCode, +) => { + switch (code) { + case CronTriggerExceptionCode.CRON_TRIGGER_NOT_FOUND: + return msg`Cron trigger not found.`; + case CronTriggerExceptionCode.CRON_TRIGGER_ALREADY_EXIST: + return msg`Cron trigger already exists.`; + case CronTriggerExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND: + return msg`Serverless function not found.`; + default: + assertUnreachable(code); + } }; export class CronTriggerException extends CustomException { @@ -26,7 +33,7 @@ export class CronTriggerException extends CustomException = { - [DataSourceExceptionCode.DATA_SOURCE_NOT_FOUND]: msg`Data source not found.`, +const getDataSourceExceptionUserFriendlyMessage = ( + code: DataSourceExceptionCode, +) => { + switch (code) { + case DataSourceExceptionCode.DATA_SOURCE_NOT_FOUND: + return STANDARD_ERROR_MESSAGE; + default: + assertUnreachable(code); + } }; export class DataSourceException extends CustomException { @@ -22,7 +27,7 @@ export class DataSourceException extends CustomException = { - [DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND]: msg`Database event trigger not found.`, - [DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_ALREADY_EXIST]: msg`Database event trigger already exists.`, - [DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_INVALID]: msg`Invalid database event trigger.`, - [DatabaseEventTriggerExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND]: msg`Serverless function not found.`, +const getDatabaseEventTriggerExceptionUserFriendlyMessage = ( + code: DatabaseEventTriggerExceptionCode, +) => { + switch (code) { + case DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND: + return msg`Database event trigger not found.`; + case DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_ALREADY_EXIST: + return msg`Database event trigger already exists.`; + case DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_INVALID: + return msg`Invalid database event trigger.`; + case DatabaseEventTriggerExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND: + return msg`Serverless function not found.`; + default: + assertUnreachable(code); + } }; export class DatabaseEventTriggerException extends CustomException { @@ -29,7 +37,7 @@ export class DatabaseEventTriggerException extends CustomException = { - FIELD_METADATA_NOT_FOUND: msg`Field not found.`, - INVALID_FIELD_INPUT: msg`Invalid field input.`, - FIELD_MUTATION_NOT_ALLOWED: msg`This field cannot be modified.`, - FIELD_ALREADY_EXISTS: msg`A field with this name already exists.`, - OBJECT_METADATA_NOT_FOUND: msg`Object not found.`, - FIELD_METADATA_RELATION_NOT_ENABLED: msg`Relation is not enabled for this field.`, - FIELD_METADATA_RELATION_MALFORMED: msg`Relation configuration is invalid.`, - LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND: msg`Label identifier field not found.`, - UNCOVERED_FIELD_METADATA_TYPE_VALIDATION: msg`Field type validation error.`, - RESERVED_KEYWORD: msg`This name is a reserved keyword.`, - NOT_AVAILABLE: msg`This field name is not available.`, - NAME_NOT_SYNCED_WITH_LABEL: msg`Field name is not synced with label.`, - INTERNAL_SERVER_ERROR: msg`An unexpected error occurred.`, +const getFieldMetadataExceptionUserFriendlyMessage = ( + code: keyof typeof FieldMetadataExceptionCode, +) => { + switch (code) { + case FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND: + return msg`Field not found.`; + case FieldMetadataExceptionCode.INVALID_FIELD_INPUT: + return msg`Invalid field input.`; + case FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED: + return msg`This field cannot be modified.`; + case FieldMetadataExceptionCode.FIELD_ALREADY_EXISTS: + return msg`A field with this name already exists.`; + case FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND: + return msg`Object not found.`; + case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_NOT_ENABLED: + return msg`Relation is not enabled for this field.`; + case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED: + return msg`Relation configuration is invalid.`; + case FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND: + return msg`Label identifier field not found.`; + case FieldMetadataExceptionCode.UNCOVERED_FIELD_METADATA_TYPE_VALIDATION: + return msg`Field type validation error.`; + case FieldMetadataExceptionCode.RESERVED_KEYWORD: + return msg`This name is a reserved keyword.`; + case FieldMetadataExceptionCode.NOT_AVAILABLE: + return msg`This field name is not available.`; + case FieldMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL: + return msg`Field name is not synced with label.`; + case FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR: + return STANDARD_ERROR_MESSAGE; + default: + assertUnreachable(code); + } }; export class FieldMetadataException extends CustomException< @@ -56,7 +74,8 @@ export class FieldMetadataException extends CustomException< ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? fieldMetadataExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? + getFieldMetadataExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception.ts index e437ad7e45..d2fac754c3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception.ts @@ -1,6 +1,7 @@ import { type MessageDescriptor } from '@lingui/core'; -import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { appendCommonExceptionCode, CustomException, @@ -12,14 +13,18 @@ export const FlatEntityMapsExceptionCode = appendCommonExceptionCode({ ENTITY_MALFORMED: 'ENTITY_MALFORMED', } as const); -const flatEntityMapsExceptionUserFriendlyMessages: Record< - keyof typeof FlatEntityMapsExceptionCode, - MessageDescriptor -> = { - ENTITY_ALREADY_EXISTS: msg`Entity already exists.`, - ENTITY_NOT_FOUND: msg`Entity not found.`, - ENTITY_MALFORMED: msg`Entity data is malformed.`, - INTERNAL_SERVER_ERROR: msg`An unexpected error occurred.`, +const getFlatEntityMapsExceptionUserFriendlyMessage = ( + code: keyof typeof FlatEntityMapsExceptionCode, +) => { + switch (code) { + case FlatEntityMapsExceptionCode.ENTITY_ALREADY_EXISTS: + case FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND: + case FlatEntityMapsExceptionCode.ENTITY_MALFORMED: + case FlatEntityMapsExceptionCode.INTERNAL_SERVER_ERROR: + return STANDARD_ERROR_MESSAGE; + default: + assertUnreachable(code); + } }; export class FlatEntityMapsException extends CustomException< @@ -33,7 +38,7 @@ export class FlatEntityMapsException extends CustomException< super(message, code, { userFriendlyMessage: userFriendlyMessage ?? - flatEntityMapsExceptionUserFriendlyMessages[code], + getFlatEntityMapsExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts index 813a8671f6..212e1f0c62 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts @@ -1,6 +1,8 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { CustomException } from 'src/utils/custom-exception'; export enum ObjectMetadataExceptionCode { @@ -14,18 +16,29 @@ export enum ObjectMetadataExceptionCode { NAME_CONFLICT = 'NAME_CONFLICT', } -const objectMetadataExceptionUserFriendlyMessages: Record< - ObjectMetadataExceptionCode, - MessageDescriptor -> = { - [ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND]: msg`Object not found.`, - [ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT]: msg`Invalid object input.`, - [ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED]: msg`This object cannot be modified.`, - [ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS]: msg`An object with this name already exists.`, - [ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD]: msg`Custom object is missing a label identifier field.`, - [ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT]: msg`Invalid data format.`, - [ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR]: msg`An unexpected error occurred.`, - [ObjectMetadataExceptionCode.NAME_CONFLICT]: msg`A name conflict occurred.`, +const getObjectMetadataExceptionUserFriendlyMessage = ( + code: ObjectMetadataExceptionCode, +) => { + switch (code) { + case ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND: + return msg`Object not found.`; + case ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT: + return msg`Invalid object input.`; + case ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED: + return msg`This object cannot be modified.`; + case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS: + return msg`An object with this name already exists.`; + case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD: + return msg`Custom object is missing a label identifier field.`; + case ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT: + return msg`Invalid data format.`; + case ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR: + return STANDARD_ERROR_MESSAGE; + case ObjectMetadataExceptionCode.NAME_CONFLICT: + return msg`A name conflict occurred.`; + default: + assertUnreachable(code); + } }; export class ObjectMetadataException extends CustomException { @@ -37,7 +50,7 @@ export class ObjectMetadataException extends CustomException = { - [PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND]: msg`Page layout tab not found.`, - [PageLayoutTabExceptionCode.INVALID_PAGE_LAYOUT_TAB_DATA]: msg`Invalid page layout tab data.`, +const getPageLayoutTabExceptionUserFriendlyMessage = ( + code: PageLayoutTabExceptionCode, +) => { + switch (code) { + case PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND: + return msg`Page layout tab not found.`; + case PageLayoutTabExceptionCode.INVALID_PAGE_LAYOUT_TAB_DATA: + return msg`Invalid page layout tab data.`; + default: + assertUnreachable(code); + } }; export class PageLayoutTabException extends CustomException { @@ -33,7 +38,8 @@ export class PageLayoutTabException extends CustomException = { - [PageLayoutWidgetExceptionCode.PAGE_LAYOUT_WIDGET_NOT_FOUND]: msg`Page layout widget not found.`, - [PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA]: msg`Invalid page layout widget data.`, +const getPageLayoutWidgetExceptionUserFriendlyMessage = ( + code: PageLayoutWidgetExceptionCode, +) => { + switch (code) { + case PageLayoutWidgetExceptionCode.PAGE_LAYOUT_WIDGET_NOT_FOUND: + return msg`Page layout widget not found.`; + case PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA: + return msg`Invalid page layout widget data.`; + default: + assertUnreachable(code); + } }; export class PageLayoutWidgetException extends CustomException { @@ -37,7 +42,7 @@ export class PageLayoutWidgetException extends CustomException = { - [PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND]: msg`Page layout not found.`, - [PageLayoutExceptionCode.INVALID_PAGE_LAYOUT_DATA]: msg`Invalid page layout data.`, - [PageLayoutExceptionCode.TAB_NOT_FOUND_FOR_WIDGET_DUPLICATION]: msg`Tab not found for widget duplication.`, +const getPageLayoutExceptionUserFriendlyMessage = ( + code: PageLayoutExceptionCode, +) => { + switch (code) { + case PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND: + return msg`Page layout not found.`; + case PageLayoutExceptionCode.INVALID_PAGE_LAYOUT_DATA: + return msg`Invalid page layout data.`; + case PageLayoutExceptionCode.TAB_NOT_FOUND_FOR_WIDGET_DUPLICATION: + return msg`Tab not found for widget duplication.`; + default: + assertUnreachable(code); + } }; export class PageLayoutException extends CustomException { @@ -33,7 +39,7 @@ export class PageLayoutException extends CustomException = { - [PermissionsExceptionCode.PERMISSION_DENIED]: msg`You do not have permission to perform this action.`, - [PermissionsExceptionCode.ADMIN_ROLE_NOT_FOUND]: msg`Admin role not found.`, - [PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND]: msg`User workspace not found.`, - [PermissionsExceptionCode.WORKSPACE_ID_ROLE_USER_WORKSPACE_MISMATCH]: msg`Workspace ID and role mismatch.`, - [PermissionsExceptionCode.TOO_MANY_ADMIN_CANDIDATES]: msg`Too many admin candidates found.`, - [PermissionsExceptionCode.USER_WORKSPACE_ALREADY_HAS_ROLE]: msg`User already has a role assigned.`, - [PermissionsExceptionCode.WORKSPACE_MEMBER_NOT_FOUND]: msg`Workspace member not found.`, - [PermissionsExceptionCode.ROLE_NOT_FOUND]: msg`Role not found.`, - [PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN]: msg`Cannot remove the last admin from the workspace.`, - [PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER]: msg`Cannot delete the last admin user.`, - [PermissionsExceptionCode.UNKNOWN_OPERATION_NAME]: msg`Unknown operation.`, - [PermissionsExceptionCode.UNKNOWN_REQUIRED_PERMISSION]: msg`Unknown permission required.`, - [PermissionsExceptionCode.CANNOT_UPDATE_SELF_ROLE]: msg`You cannot update your own role.`, - [PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE]: msg`No role found for this user in the workspace.`, - [PermissionsExceptionCode.API_KEY_ROLE_NOT_FOUND]: msg`API key role not found.`, - [PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT]: msg`Authentication is required.`, - [PermissionsExceptionCode.INVALID_ARG]: msg`Invalid argument provided.`, - [PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS]: msg`A role with this label already exists.`, - [PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND]: msg`Default role not found.`, - [PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND]: msg`Object metadata not found.`, - [PermissionsExceptionCode.INVALID_SETTING]: msg`Invalid permission setting.`, - [PermissionsExceptionCode.ROLE_NOT_EDITABLE]: msg`This role cannot be edited.`, - [PermissionsExceptionCode.DEFAULT_ROLE_CANNOT_BE_DELETED]: msg`The default role cannot be deleted.`, - [PermissionsExceptionCode.NO_PERMISSIONS_FOUND_IN_DATASOURCE]: msg`No permissions found in datasource.`, - [PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT]: msg`Cannot add permissions on system objects.`, - [PermissionsExceptionCode.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT]: msg`Cannot add field permissions on system objects.`, - [PermissionsExceptionCode.METHOD_NOT_ALLOWED]: msg`This method is not allowed.`, - [PermissionsExceptionCode.RAW_SQL_NOT_ALLOWED]: msg`Raw SQL queries are not allowed.`, - [PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT]: msg`Cannot give write permission on non-readable objects.`, - [PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION]: msg`Cannot give write permission without read permission.`, - [PermissionsExceptionCode.FIELD_METADATA_NOT_FOUND]: msg`Field metadata not found.`, - [PermissionsExceptionCode.ONLY_FIELD_RESTRICTION_ALLOWED]: msg`Only field restrictions are allowed.`, - [PermissionsExceptionCode.FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT]: msg`Field restrictions only apply to readable objects.`, - [PermissionsExceptionCode.FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT]: msg`Update field restrictions only apply to updatable objects.`, - [PermissionsExceptionCode.UPSERT_FIELD_PERMISSION_FAILED]: msg`Failed to update field permission.`, - [PermissionsExceptionCode.PERMISSION_NOT_FOUND]: msg`Permission not found.`, - [PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND]: msg`Object permission not found.`, - [PermissionsExceptionCode.EMPTY_FIELD_PERMISSION_NOT_ALLOWED]: msg`Empty field permissions are not allowed.`, - [PermissionsExceptionCode.JOIN_COLUMN_NAME_REQUIRED]: msg`Join column name is required.`, - [PermissionsExceptionCode.COMPOSITE_TYPE_NOT_FOUND]: msg`Composite type not found.`, - [PermissionsExceptionCode.ROLE_MUST_HAVE_AT_LEAST_ONE_TARGET]: msg`Role must have at least one target.`, - [PermissionsExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_USERS]: msg`This role cannot be assigned to users.`, +const getPermissionsExceptionUserFriendlyMessage = ( + code: PermissionsExceptionCode, +) => { + switch (code) { + case PermissionsExceptionCode.PERMISSION_DENIED: + return msg`You do not have permission to perform this action.`; + case PermissionsExceptionCode.ADMIN_ROLE_NOT_FOUND: + return msg`Admin role not found.`; + case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND: + return msg`User workspace not found.`; + case PermissionsExceptionCode.WORKSPACE_ID_ROLE_USER_WORKSPACE_MISMATCH: + return msg`Workspace ID and role mismatch.`; + case PermissionsExceptionCode.TOO_MANY_ADMIN_CANDIDATES: + return msg`Too many admin candidates found.`; + case PermissionsExceptionCode.USER_WORKSPACE_ALREADY_HAS_ROLE: + return msg`User already has a role assigned.`; + case PermissionsExceptionCode.WORKSPACE_MEMBER_NOT_FOUND: + return msg`Workspace member not found.`; + case PermissionsExceptionCode.ROLE_NOT_FOUND: + return msg`Role not found.`; + case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN: + return msg`Cannot remove the last admin from the workspace.`; + case PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER: + return msg`Cannot delete the last admin user.`; + case PermissionsExceptionCode.UNKNOWN_OPERATION_NAME: + return msg`Unknown operation.`; + case PermissionsExceptionCode.UNKNOWN_REQUIRED_PERMISSION: + return msg`Unknown permission required.`; + case PermissionsExceptionCode.CANNOT_UPDATE_SELF_ROLE: + return msg`You cannot update your own role.`; + case PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE: + return msg`No role found for this user in the workspace.`; + case PermissionsExceptionCode.API_KEY_ROLE_NOT_FOUND: + return msg`API key role not found.`; + case PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT: + return msg`Authentication is required.`; + case PermissionsExceptionCode.INVALID_ARG: + return msg`Invalid argument provided.`; + case PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS: + return msg`A role with this label already exists.`; + case PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND: + return msg`Default role not found.`; + case PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND: + return msg`Object metadata not found.`; + case PermissionsExceptionCode.INVALID_SETTING: + return msg`Invalid permission setting.`; + case PermissionsExceptionCode.ROLE_NOT_EDITABLE: + return msg`This role cannot be edited.`; + case PermissionsExceptionCode.DEFAULT_ROLE_CANNOT_BE_DELETED: + return msg`The default role cannot be deleted.`; + case PermissionsExceptionCode.NO_PERMISSIONS_FOUND_IN_DATASOURCE: + return msg`No permissions found in datasource.`; + case PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT: + return msg`Cannot add permissions on system objects.`; + case PermissionsExceptionCode.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT: + return msg`Cannot add field permissions on system objects.`; + case PermissionsExceptionCode.METHOD_NOT_ALLOWED: + return msg`This method is not allowed.`; + case PermissionsExceptionCode.RAW_SQL_NOT_ALLOWED: + return msg`Raw SQL queries are not allowed.`; + case PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT: + return msg`Cannot give write permission on non-readable objects.`; + case PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION: + return msg`Cannot give write permission without read permission.`; + case PermissionsExceptionCode.FIELD_METADATA_NOT_FOUND: + return msg`Field metadata not found.`; + case PermissionsExceptionCode.ONLY_FIELD_RESTRICTION_ALLOWED: + return msg`Only field restrictions are allowed.`; + case PermissionsExceptionCode.FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT: + return msg`Field restrictions only apply to readable objects.`; + case PermissionsExceptionCode.FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT: + return msg`Update field restrictions only apply to updatable objects.`; + case PermissionsExceptionCode.UPSERT_FIELD_PERMISSION_FAILED: + return msg`Failed to update field permission.`; + case PermissionsExceptionCode.PERMISSION_NOT_FOUND: + return msg`Permission not found.`; + case PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND: + return msg`Object permission not found.`; + case PermissionsExceptionCode.EMPTY_FIELD_PERMISSION_NOT_ALLOWED: + return msg`Empty field permissions are not allowed.`; + case PermissionsExceptionCode.JOIN_COLUMN_NAME_REQUIRED: + return msg`Join column name is required.`; + case PermissionsExceptionCode.COMPOSITE_TYPE_NOT_FOUND: + return msg`Composite type not found.`; + case PermissionsExceptionCode.ROLE_MUST_HAVE_AT_LEAST_ONE_TARGET: + return msg`Role must have at least one target.`; + case PermissionsExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_USERS: + return msg`This role cannot be assigned to users.`; + default: + assertUnreachable(code); + } }; export class PermissionsException extends CustomException { @@ -104,7 +150,7 @@ export class PermissionsException extends CustomException = { - [RemoteServerExceptionCode.REMOTE_SERVER_NOT_FOUND]: msg`Remote server not found.`, - [RemoteServerExceptionCode.REMOTE_SERVER_ALREADY_EXISTS]: msg`Remote server already exists.`, - [RemoteServerExceptionCode.REMOTE_SERVER_MUTATION_NOT_ALLOWED]: msg`This remote server cannot be modified.`, - [RemoteServerExceptionCode.REMOTE_SERVER_CONNECTION_ERROR]: msg`Failed to connect to remote server.`, - [RemoteServerExceptionCode.INVALID_REMOTE_SERVER_INPUT]: msg`Invalid remote server input.`, +const getRemoteServerExceptionUserFriendlyMessage = ( + code: RemoteServerExceptionCode, +) => { + switch (code) { + case RemoteServerExceptionCode.REMOTE_SERVER_NOT_FOUND: + return msg`Remote server not found.`; + case RemoteServerExceptionCode.REMOTE_SERVER_ALREADY_EXISTS: + return msg`Remote server already exists.`; + case RemoteServerExceptionCode.REMOTE_SERVER_MUTATION_NOT_ALLOWED: + return msg`This remote server cannot be modified.`; + case RemoteServerExceptionCode.REMOTE_SERVER_CONNECTION_ERROR: + return msg`Failed to connect to remote server.`; + case RemoteServerExceptionCode.INVALID_REMOTE_SERVER_INPUT: + return msg`Invalid remote server input.`; + default: + assertUnreachable(code); + } }; export class RemoteServerException extends CustomException { @@ -30,7 +39,8 @@ export class RemoteServerException extends CustomException = { - TIMEOUT_ERROR: msg`Request timed out.`, - INTERNAL_SERVER_ERROR: msg`An unexpected error occurred.`, +const getDistantTableExceptionUserFriendlyMessage = ( + code: keyof typeof DistantTableExceptionCode, +) => { + switch (code) { + case DistantTableExceptionCode.TIMEOUT_ERROR: + return msg`Request timed out.`; + case DistantTableExceptionCode.INTERNAL_SERVER_ERROR: + return msg`An unexpected error occurred.`; + default: + assertUnreachable(code); + } }; export class DistantTableException extends CustomException< @@ -28,7 +34,8 @@ export class DistantTableException extends CustomException< ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? distantTableExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? + getDistantTableExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.exception.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.exception.ts index 22a8212de3..968562794e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -8,12 +9,17 @@ export enum ForeignTableExceptionCode { INVALID_FOREIGN_TABLE_INPUT = 'INVALID_FOREIGN_TABLE_INPUT', } -const foreignTableExceptionUserFriendlyMessages: Record< - ForeignTableExceptionCode, - MessageDescriptor -> = { - [ForeignTableExceptionCode.FOREIGN_TABLE_MUTATION_NOT_ALLOWED]: msg`This foreign table cannot be modified.`, - [ForeignTableExceptionCode.INVALID_FOREIGN_TABLE_INPUT]: msg`Invalid foreign table input.`, +const getForeignTableExceptionUserFriendlyMessage = ( + code: ForeignTableExceptionCode, +) => { + switch (code) { + case ForeignTableExceptionCode.FOREIGN_TABLE_MUTATION_NOT_ALLOWED: + return msg`This foreign table cannot be modified.`; + case ForeignTableExceptionCode.INVALID_FOREIGN_TABLE_INPUT: + return msg`Invalid foreign table input.`; + default: + assertUnreachable(code); + } }; export class ForeignTableException extends CustomException { @@ -24,7 +30,8 @@ export class ForeignTableException extends CustomException = { - [RemoteTableExceptionCode.REMOTE_TABLE_NOT_FOUND]: msg`Remote table not found.`, - [RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT]: msg`Invalid remote table input.`, - [RemoteTableExceptionCode.REMOTE_TABLE_ALREADY_EXISTS]: msg`Remote table already exists.`, - [RemoteTableExceptionCode.NO_FOREIGN_TABLES_FOUND]: msg`No foreign tables found.`, - [RemoteTableExceptionCode.NO_OBJECT_METADATA_FOUND]: msg`Object metadata not found.`, - [RemoteTableExceptionCode.NO_FIELD_METADATA_FOUND]: msg`Field metadata not found.`, +const getRemoteTableExceptionUserFriendlyMessage = ( + code: RemoteTableExceptionCode, +) => { + switch (code) { + case RemoteTableExceptionCode.REMOTE_TABLE_NOT_FOUND: + return msg`Remote table not found.`; + case RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT: + return msg`Invalid remote table input.`; + case RemoteTableExceptionCode.REMOTE_TABLE_ALREADY_EXISTS: + return msg`Remote table already exists.`; + case RemoteTableExceptionCode.NO_FOREIGN_TABLES_FOUND: + return msg`No foreign tables found.`; + case RemoteTableExceptionCode.NO_OBJECT_METADATA_FOUND: + return msg`Object metadata not found.`; + case RemoteTableExceptionCode.NO_FIELD_METADATA_FOUND: + return msg`Field metadata not found.`; + default: + assertUnreachable(code); + } }; export class RemoteTableException extends CustomException { @@ -32,7 +42,7 @@ export class RemoteTableException extends CustomException = { - [RoleTargetExceptionCode.ROLE_TARGET_NOT_FOUND]: msg`Role target not found.`, - [RoleTargetExceptionCode.INVALID_ROLE_TARGET_DATA]: msg`Invalid role target data.`, - [RoleTargetExceptionCode.ROLE_TARGET_MISSING_IDENTIFIER]: msg`Role target is missing identifier.`, - [RoleTargetExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_ENTITY]: msg`Role cannot be assigned to this entity.`, - [RoleTargetExceptionCode.ROLE_NOT_FOUND]: msg`Role not found.`, +const getRoleTargetExceptionUserFriendlyMessage = ( + code: RoleTargetExceptionCode, +) => { + switch (code) { + case RoleTargetExceptionCode.ROLE_TARGET_NOT_FOUND: + return msg`Role target not found.`; + case RoleTargetExceptionCode.INVALID_ROLE_TARGET_DATA: + return msg`Invalid role target data.`; + case RoleTargetExceptionCode.ROLE_TARGET_MISSING_IDENTIFIER: + return msg`Role target is missing identifier.`; + case RoleTargetExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_ENTITY: + return msg`Role cannot be assigned to this entity.`; + case RoleTargetExceptionCode.ROLE_NOT_FOUND: + return msg`Role not found.`; + default: + assertUnreachable(code); + } }; export class RoleTargetException extends CustomException { @@ -30,7 +39,7 @@ export class RoleTargetException extends CustomException = { - [RouteTriggerExceptionCode.WORKSPACE_NOT_FOUND]: msg`Workspace not found.`, - [RouteTriggerExceptionCode.ROUTE_NOT_FOUND]: msg`Route not found.`, - [RouteTriggerExceptionCode.TRIGGER_NOT_FOUND]: msg`Trigger not found.`, - [RouteTriggerExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND]: msg`Serverless function not found.`, - [RouteTriggerExceptionCode.ROUTE_ALREADY_EXIST]: msg`Route already exists.`, - [RouteTriggerExceptionCode.ROUTE_PATH_ALREADY_EXIST]: msg`Route path already exists.`, - [RouteTriggerExceptionCode.FORBIDDEN_EXCEPTION]: msg`You do not have permission to perform this action.`, - [RouteTriggerExceptionCode.SERVERLESS_FUNCTION_EXECUTION_ERROR]: msg`Serverless function execution failed.`, +const getRouteTriggerExceptionUserFriendlyMessage = ( + code: RouteTriggerExceptionCode, +) => { + switch (code) { + case RouteTriggerExceptionCode.WORKSPACE_NOT_FOUND: + return msg`Workspace not found.`; + case RouteTriggerExceptionCode.ROUTE_NOT_FOUND: + return msg`Route not found.`; + case RouteTriggerExceptionCode.TRIGGER_NOT_FOUND: + return msg`Trigger not found.`; + case RouteTriggerExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND: + return msg`Serverless function not found.`; + case RouteTriggerExceptionCode.ROUTE_ALREADY_EXIST: + return msg`Route already exists.`; + case RouteTriggerExceptionCode.ROUTE_PATH_ALREADY_EXIST: + return msg`Route path already exists.`; + case RouteTriggerExceptionCode.FORBIDDEN_EXCEPTION: + return msg`You do not have permission to perform this action.`; + case RouteTriggerExceptionCode.SERVERLESS_FUNCTION_EXECUTION_ERROR: + return msg`Serverless function execution failed.`; + default: + assertUnreachable(code); + } }; export class RouteTriggerException extends CustomException { @@ -36,7 +48,8 @@ export class RouteTriggerException extends CustomException = { - [ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND]: msg`Function not found.`, - [ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_VERSION_NOT_FOUND]: msg`Function version not found.`, - [ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_ALREADY_EXIST]: msg`A function with this name already exists.`, - [ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_READY]: msg`Function is not ready.`, - [ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_BUILDING]: msg`Function is currently building.`, - [ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CODE_UNCHANGED]: msg`Function code is unchanged.`, - [ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED]: msg`Function execution limit reached.`, - [ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CREATE_FAILED]: msg`Failed to create function.`, - [ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_EXECUTION_TIMEOUT]: msg`Function execution timed out.`, +const getServerlessFunctionExceptionUserFriendlyMessage = ( + code: ServerlessFunctionExceptionCode, +) => { + switch (code) { + case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND: + return msg`Function not found.`; + case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_VERSION_NOT_FOUND: + return msg`Function version not found.`; + case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_ALREADY_EXIST: + return msg`A function with this name already exists.`; + case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_READY: + return msg`Function is not ready.`; + case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_BUILDING: + return msg`Function is currently building.`; + case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CODE_UNCHANGED: + return msg`Function code is unchanged.`; + case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED: + return msg`Function execution limit reached.`; + case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CREATE_FAILED: + return msg`Failed to create function.`; + case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_EXECUTION_TIMEOUT: + return msg`Function execution timed out.`; + default: + assertUnreachable(code); + } }; export class ServerlessFunctionException extends CustomException { @@ -39,7 +52,7 @@ export class ServerlessFunctionException extends CustomException = { - [InvalidMetadataExceptionCode.LABEL_REQUIRED]: msg`Label is required.`, - [InvalidMetadataExceptionCode.INPUT_TOO_SHORT]: msg`Input is too short.`, - [InvalidMetadataExceptionCode.EXCEEDS_MAX_LENGTH]: msg`Input exceeds maximum length.`, - [InvalidMetadataExceptionCode.RESERVED_KEYWORD]: msg`This name is a reserved keyword.`, - [InvalidMetadataExceptionCode.NOT_CAMEL_CASE]: msg`Name must be in camelCase format.`, - [InvalidMetadataExceptionCode.INVALID_LABEL]: msg`Invalid label format.`, - [InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL]: msg`Name is not synced with label.`, - [InvalidMetadataExceptionCode.INVALID_STRING]: msg`Invalid string format.`, - [InvalidMetadataExceptionCode.NOT_AVAILABLE]: msg`This name is not available.`, +const getInvalidMetadataExceptionUserFriendlyMessage = ( + code: InvalidMetadataExceptionCode, +) => { + switch (code) { + case InvalidMetadataExceptionCode.LABEL_REQUIRED: + return msg`Label is required.`; + case InvalidMetadataExceptionCode.INPUT_TOO_SHORT: + return msg`Input is too short.`; + case InvalidMetadataExceptionCode.EXCEEDS_MAX_LENGTH: + return msg`Input exceeds maximum length.`; + case InvalidMetadataExceptionCode.RESERVED_KEYWORD: + return msg`This name is a reserved keyword.`; + case InvalidMetadataExceptionCode.NOT_CAMEL_CASE: + return msg`Name must be in camelCase format.`; + case InvalidMetadataExceptionCode.INVALID_LABEL: + return msg`Invalid label format.`; + case InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL: + return msg`Name is not synced with label.`; + case InvalidMetadataExceptionCode.INVALID_STRING: + return msg`Invalid string format.`; + case InvalidMetadataExceptionCode.NOT_AVAILABLE: + return msg`This name is not available.`; + default: + assertUnreachable(code); + } }; export class InvalidMetadataException extends CustomException { @@ -39,7 +52,7 @@ export class InvalidMetadataException extends CustomException = { - VIEW_NOT_FOUND: msg`View not found.`, - VIEW_ALREADY_EXISTS: msg`View already exists.`, - INTERNAL_SERVER_ERROR: msg`An unexpected error occurred.`, +const getFlatViewExceptionUserFriendlyMessage = ( + code: keyof typeof FlatViewExceptionCode, +) => { + switch (code) { + case FlatViewExceptionCode.VIEW_NOT_FOUND: + return msg`View not found.`; + case FlatViewExceptionCode.VIEW_ALREADY_EXISTS: + return msg`View already exists.`; + case FlatViewExceptionCode.INTERNAL_SERVER_ERROR: + return STANDARD_ERROR_MESSAGE; + default: + assertUnreachable(code); + } }; export class FlatViewException extends CustomException< @@ -30,7 +38,7 @@ export class FlatViewException extends CustomException< ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? flatViewExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getFlatViewExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception.ts index b83f9d6ea6..f06ac1dd50 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -7,11 +8,15 @@ export enum WorkspaceMetadataVersionExceptionCode { METADATA_VERSION_NOT_FOUND = 'METADATA_VERSION_NOT_FOUND', } -const workspaceMetadataVersionExceptionUserFriendlyMessages: Record< - WorkspaceMetadataVersionExceptionCode, - MessageDescriptor -> = { - [WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND]: msg`Metadata version not found.`, +const getWorkspaceMetadataVersionExceptionUserFriendlyMessage = ( + code: WorkspaceMetadataVersionExceptionCode, +) => { + switch (code) { + case WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND: + return msg`Metadata version not found.`; + default: + assertUnreachable(code); + } }; export class WorkspaceMetadataVersionException extends CustomException { @@ -23,7 +28,7 @@ export class WorkspaceMetadataVersionException extends CustomException = { - [WorkspaceMigrationExceptionCode.NO_FACTORY_FOUND]: msg`Migration factory not found.`, - [WorkspaceMigrationExceptionCode.INVALID_ACTION]: msg`Invalid migration action.`, - [WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA]: msg`Invalid field metadata.`, - [WorkspaceMigrationExceptionCode.INVALID_COMPOSITE_TYPE]: msg`Invalid composite type.`, - [WorkspaceMigrationExceptionCode.ENUM_TYPE_NAME_NOT_FOUND]: msg`Enum type not found.`, +const getWorkspaceMigrationExceptionUserFriendlyMessage = ( + code: WorkspaceMigrationExceptionCode, +) => { + switch (code) { + case WorkspaceMigrationExceptionCode.NO_FACTORY_FOUND: + return msg`Migration factory not found.`; + case WorkspaceMigrationExceptionCode.INVALID_ACTION: + return msg`Invalid migration action.`; + case WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA: + return msg`Invalid field metadata.`; + case WorkspaceMigrationExceptionCode.INVALID_COMPOSITE_TYPE: + return msg`Invalid composite type.`; + case WorkspaceMigrationExceptionCode.ENUM_TYPE_NAME_NOT_FOUND: + return msg`Enum type not found.`; + default: + assertUnreachable(code); + } }; export class WorkspaceMigrationException extends CustomException { @@ -31,7 +40,7 @@ export class WorkspaceMigrationException extends CustomException = { - [RelationExceptionCode.RELATION_OBJECT_METADATA_NOT_FOUND]: msg`Relation object not found.`, - [RelationExceptionCode.RELATION_TARGET_FIELD_METADATA_ID_NOT_FOUND]: msg`Relation target field not found.`, - [RelationExceptionCode.RELATION_JOIN_COLUMN_ON_BOTH_SIDES]: msg`Relation has join column on both sides.`, - [RelationExceptionCode.MISSING_RELATION_JOIN_COLUMN]: msg`Missing relation join column.`, - [RelationExceptionCode.MULTIPLE_JOIN_COLUMNS_FOUND]: msg`Multiple join columns found.`, +const getRelationExceptionUserFriendlyMessage = ( + code: RelationExceptionCode, +) => { + switch (code) { + case RelationExceptionCode.RELATION_OBJECT_METADATA_NOT_FOUND: + case RelationExceptionCode.RELATION_TARGET_FIELD_METADATA_ID_NOT_FOUND: + case RelationExceptionCode.RELATION_JOIN_COLUMN_ON_BOTH_SIDES: + case RelationExceptionCode.MISSING_RELATION_JOIN_COLUMN: + case RelationExceptionCode.MULTIPLE_JOIN_COLUMNS_FOUND: + return STANDARD_ERROR_MESSAGE; + default: + assertUnreachable(code); + } }; export class RelationException extends CustomException { @@ -30,7 +35,7 @@ export class RelationException extends CustomException { ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? relationExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getRelationExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts b/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts index 5f0b71ac65..ede2e90247 100644 --- a/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts +++ b/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts @@ -1,6 +1,8 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { CustomException } from 'src/utils/custom-exception'; export enum TwentyORMExceptionCode { @@ -25,29 +27,50 @@ export enum TwentyORMExceptionCode { ORM_EVENT_DATA_CORRUPTED = 'ORM_EVENT_DATA_CORRUPTED', } -const twentyORMExceptionUserFriendlyMessages: Record< - TwentyORMExceptionCode, - MessageDescriptor -> = { - [TwentyORMExceptionCode.METADATA_VERSION_MISMATCH]: msg`Data version mismatch. Please refresh and try again.`, - [TwentyORMExceptionCode.WORKSPACE_SCHEMA_NOT_FOUND]: msg`Workspace schema not found.`, - [TwentyORMExceptionCode.ROLES_PERMISSIONS_VERSION_NOT_FOUND]: msg`Roles and permissions configuration not found.`, - [TwentyORMExceptionCode.FEATURE_FLAG_MAP_VERSION_NOT_FOUND]: msg`Feature configuration not found.`, - [TwentyORMExceptionCode.USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND]: msg`User workspace role configuration not found.`, - [TwentyORMExceptionCode.API_KEY_ROLE_MAP_VERSION_NOT_FOUND]: msg`API key role configuration not found.`, - [TwentyORMExceptionCode.MALFORMED_METADATA]: msg`Data structure is invalid.`, - [TwentyORMExceptionCode.WORKSPACE_NOT_FOUND]: msg`Workspace not found.`, - [TwentyORMExceptionCode.CONNECT_RECORD_NOT_FOUND]: msg`Related record not found.`, - [TwentyORMExceptionCode.CONNECT_NOT_ALLOWED]: msg`This connection is not allowed.`, - [TwentyORMExceptionCode.CONNECT_UNIQUE_CONSTRAINT_ERROR]: msg`A record with this relationship already exists.`, - [TwentyORMExceptionCode.MISSING_MAIN_ALIAS_TARGET]: msg`Missing main alias target.`, - [TwentyORMExceptionCode.METHOD_NOT_ALLOWED]: msg`This operation is not allowed.`, - [TwentyORMExceptionCode.ENUM_TYPE_NAME_NOT_FOUND]: msg`Enum type not found.`, - [TwentyORMExceptionCode.QUERY_READ_TIMEOUT]: msg`Query timed out. Please try again.`, - [TwentyORMExceptionCode.DUPLICATE_ENTRY_DETECTED]: msg`A duplicate entry was detected.`, - [TwentyORMExceptionCode.TOO_MANY_RECORDS_TO_UPDATE]: msg`Too many records to update at once.`, - [TwentyORMExceptionCode.INVALID_INPUT]: msg`Invalid input provided.`, - [TwentyORMExceptionCode.ORM_EVENT_DATA_CORRUPTED]: msg`Event data is corrupted.`, +const getTwentyORMExceptionUserFriendlyMessage = ( + code: TwentyORMExceptionCode, +) => { + switch (code) { + case TwentyORMExceptionCode.METADATA_VERSION_MISMATCH: + return msg`Data version mismatch. Please refresh and try again.`; + case TwentyORMExceptionCode.WORKSPACE_SCHEMA_NOT_FOUND: + return msg`Workspace schema not found.`; + case TwentyORMExceptionCode.ROLES_PERMISSIONS_VERSION_NOT_FOUND: + return msg`Roles and permissions configuration not found.`; + case TwentyORMExceptionCode.FEATURE_FLAG_MAP_VERSION_NOT_FOUND: + return msg`Feature configuration not found.`; + case TwentyORMExceptionCode.USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND: + return msg`User workspace role configuration not found.`; + case TwentyORMExceptionCode.API_KEY_ROLE_MAP_VERSION_NOT_FOUND: + return msg`API key role configuration not found.`; + case TwentyORMExceptionCode.MALFORMED_METADATA: + return msg`Data structure is invalid.`; + case TwentyORMExceptionCode.WORKSPACE_NOT_FOUND: + return msg`Workspace not found.`; + case TwentyORMExceptionCode.CONNECT_RECORD_NOT_FOUND: + return msg`Related record not found.`; + case TwentyORMExceptionCode.CONNECT_NOT_ALLOWED: + return msg`This connection is not allowed.`; + case TwentyORMExceptionCode.CONNECT_UNIQUE_CONSTRAINT_ERROR: + return msg`A record with this relationship already exists.`; + case TwentyORMExceptionCode.MISSING_MAIN_ALIAS_TARGET: + return msg`Missing main alias target.`; + case TwentyORMExceptionCode.METHOD_NOT_ALLOWED: + return msg`This operation is not allowed.`; + case TwentyORMExceptionCode.QUERY_READ_TIMEOUT: + return msg`Query timed out. Please try again.`; + case TwentyORMExceptionCode.DUPLICATE_ENTRY_DETECTED: + return msg`A duplicate entry was detected.`; + case TwentyORMExceptionCode.TOO_MANY_RECORDS_TO_UPDATE: + return msg`Too many records to update at once.`; + case TwentyORMExceptionCode.INVALID_INPUT: + return msg`Invalid input provided.`; + case TwentyORMExceptionCode.ENUM_TYPE_NAME_NOT_FOUND: + case TwentyORMExceptionCode.ORM_EVENT_DATA_CORRUPTED: + return STANDARD_ERROR_MESSAGE; + default: + assertUnreachable(code); + } }; export class TwentyORMException extends CustomException { @@ -58,7 +81,7 @@ export class TwentyORMException extends CustomException ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? twentyORMExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getTwentyORMExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/exceptions/workspace-schema-manager.exception.ts b/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/exceptions/workspace-schema-manager.exception.ts index 8b5a35a3c7..eda234bfa5 100644 --- a/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/exceptions/workspace-schema-manager.exception.ts +++ b/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/exceptions/workspace-schema-manager.exception.ts @@ -1,6 +1,7 @@ import { type MessageDescriptor } from '@lingui/core'; -import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { appendCommonExceptionCode, CustomException, @@ -10,12 +11,16 @@ export const WorkspaceSchemaManagerExceptionCode = appendCommonExceptionCode({ ENUM_OPERATION_FAILED: 'ENUM_OPERATION_FAILED', } as const); -const workspaceSchemaManagerExceptionUserFriendlyMessages: Record< - keyof typeof WorkspaceSchemaManagerExceptionCode, - MessageDescriptor -> = { - ENUM_OPERATION_FAILED: msg`Schema enum operation failed.`, - INTERNAL_SERVER_ERROR: msg`An unexpected error occurred.`, +const getWorkspaceSchemaManagerExceptionUserFriendlyMessage = ( + code: keyof typeof WorkspaceSchemaManagerExceptionCode, +) => { + switch (code) { + case WorkspaceSchemaManagerExceptionCode.ENUM_OPERATION_FAILED: + case WorkspaceSchemaManagerExceptionCode.INTERNAL_SERVER_ERROR: + return STANDARD_ERROR_MESSAGE; + default: + assertUnreachable(code); + } }; export class WorkspaceSchemaManagerException extends CustomException< @@ -29,7 +34,7 @@ export class WorkspaceSchemaManagerException extends CustomException< super(message, code, { userFriendlyMessage: userFriendlyMessage ?? - workspaceSchemaManagerExceptionUserFriendlyMessages[code], + getWorkspaceSchemaManagerExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/workspace-cache/exceptions/workspace-cache.exception.ts b/packages/twenty-server/src/engine/workspace-cache/exceptions/workspace-cache.exception.ts index d11a03845c..06c7db1e95 100644 --- a/packages/twenty-server/src/engine/workspace-cache/exceptions/workspace-cache.exception.ts +++ b/packages/twenty-server/src/engine/workspace-cache/exceptions/workspace-cache.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { appendCommonExceptionCode, @@ -11,13 +12,19 @@ export const WorkspaceCacheExceptionCode = appendCommonExceptionCode({ INVALID_PARAMETERS: 'INVALID_PARAMETERS', } as const); -const workspaceCacheExceptionUserFriendlyMessages: Record< - keyof typeof WorkspaceCacheExceptionCode, - MessageDescriptor -> = { - MISSING_DECORATOR: msg`Missing decorator configuration.`, - INVALID_PARAMETERS: msg`Invalid parameters provided.`, - INTERNAL_SERVER_ERROR: msg`An unexpected error occurred.`, +const getWorkspaceCacheExceptionUserFriendlyMessage = ( + code: keyof typeof WorkspaceCacheExceptionCode, +) => { + switch (code) { + case WorkspaceCacheExceptionCode.MISSING_DECORATOR: + return msg`Missing decorator configuration.`; + case WorkspaceCacheExceptionCode.INVALID_PARAMETERS: + return msg`Invalid parameters provided.`; + case WorkspaceCacheExceptionCode.INTERNAL_SERVER_ERROR: + return msg`An unexpected error occurred.`; + default: + assertUnreachable(code); + } }; export class WorkspaceCacheException extends CustomException< @@ -31,7 +38,7 @@ export class WorkspaceCacheException extends CustomException< super(message, code, { userFriendlyMessage: userFriendlyMessage ?? - workspaceCacheExceptionUserFriendlyMessages[code], + getWorkspaceCacheExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/exceptions/workspace-cleaner.exception.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/exceptions/workspace-cleaner.exception.ts index ca3a513616..55303fcc3a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/exceptions/workspace-cleaner.exception.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/exceptions/workspace-cleaner.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -7,11 +8,15 @@ export enum WorkspaceCleanerExceptionCode { BILLING_SUBSCRIPTION_NOT_FOUND = 'BILLING_SUBSCRIPTION_NOT_FOUND', } -const workspaceCleanerExceptionUserFriendlyMessages: Record< - WorkspaceCleanerExceptionCode, - MessageDescriptor -> = { - [WorkspaceCleanerExceptionCode.BILLING_SUBSCRIPTION_NOT_FOUND]: msg`Billing subscription not found.`, +const getWorkspaceCleanerExceptionUserFriendlyMessage = ( + code: WorkspaceCleanerExceptionCode, +) => { + switch (code) { + case WorkspaceCleanerExceptionCode.BILLING_SUBSCRIPTION_NOT_FOUND: + return msg`Billing subscription not found.`; + default: + assertUnreachable(code); + } }; export class WorkspaceCleanerException extends CustomException { @@ -23,7 +28,7 @@ export class WorkspaceCleanerException extends CustomException = { - FIELD_METADATA_NOT_FOUND: msg`Field metadata not found.`, - OBJECT_METADATA_NOT_FOUND: msg`Object metadata not found.`, - ENUM_OPERATION_FAILED: msg`Enum operation failed.`, - UNSUPPORTED_COMPOSITE_COLUMN_TYPE: msg`Unsupported composite column type.`, - NOT_SUPPORTED: msg`This operation is not supported.`, - INVALID_ACTION_TYPE: msg`Invalid action type.`, - FLAT_ENTITY_NOT_FOUND: msg`Entity not found.`, - INTERNAL_SERVER_ERROR: msg`An unexpected error occurred.`, +const getWorkspaceMigrationRunnerExceptionUserFriendlyMessage = ( + code: keyof typeof WorkspaceMigrationRunnerExceptionCode, +) => { + switch (code) { + case WorkspaceMigrationRunnerExceptionCode.FIELD_METADATA_NOT_FOUND: + return msg`Field metadata not found.`; + case WorkspaceMigrationRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND: + return msg`Object metadata not found.`; + case WorkspaceMigrationRunnerExceptionCode.ENUM_OPERATION_FAILED: + return msg`Enum operation failed.`; + case WorkspaceMigrationRunnerExceptionCode.UNSUPPORTED_COMPOSITE_COLUMN_TYPE: + return msg`Unsupported composite column type.`; + case WorkspaceMigrationRunnerExceptionCode.NOT_SUPPORTED: + return msg`This operation is not supported.`; + case WorkspaceMigrationRunnerExceptionCode.INVALID_ACTION_TYPE: + return msg`Invalid action type.`; + case WorkspaceMigrationRunnerExceptionCode.FLAT_ENTITY_NOT_FOUND: + return msg`Entity not found.`; + case WorkspaceMigrationRunnerExceptionCode.INTERNAL_SERVER_ERROR: + return msg`An unexpected error occurred.`; + default: + assertUnreachable(code); + } }; export class WorkspaceMigrationRunnerException extends CustomException< @@ -41,7 +53,7 @@ export class WorkspaceMigrationRunnerException extends CustomException< super(message, code, { userFriendlyMessage: userFriendlyMessage ?? - workspaceMigrationRunnerExceptionUserFriendlyMessages[code], + getWorkspaceMigrationRunnerExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts index 643122d83b..e9fa1f6ea5 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -14,18 +15,29 @@ export enum CalendarEventImportDriverExceptionCode { CHANNEL_MISCONFIGURED = 'CHANNEL_MISCONFIGURED', } -const calendarEventImportDriverExceptionUserFriendlyMessages: Record< - CalendarEventImportDriverExceptionCode, - MessageDescriptor -> = { - [CalendarEventImportDriverExceptionCode.NOT_FOUND]: msg`Calendar event not found.`, - [CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR]: msg`A temporary error occurred. Please try again.`, - [CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS]: msg`Insufficient permissions to access calendar.`, - [CalendarEventImportDriverExceptionCode.SYNC_CURSOR_ERROR]: msg`Calendar sync error.`, - [CalendarEventImportDriverExceptionCode.UNKNOWN]: msg`An unknown calendar error occurred.`, - [CalendarEventImportDriverExceptionCode.UNKNOWN_NETWORK_ERROR]: msg`A network error occurred while accessing calendar.`, - [CalendarEventImportDriverExceptionCode.HANDLE_ALIASES_REQUIRED]: msg`Handle aliases are required.`, - [CalendarEventImportDriverExceptionCode.CHANNEL_MISCONFIGURED]: msg`Calendar channel is misconfigured.`, +const getCalendarEventImportDriverExceptionUserFriendlyMessage = ( + code: CalendarEventImportDriverExceptionCode, +) => { + switch (code) { + case CalendarEventImportDriverExceptionCode.NOT_FOUND: + return msg`Calendar event not found.`; + case CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR: + return msg`A temporary error occurred. Please try again.`; + case CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS: + return msg`Insufficient permissions to access calendar.`; + case CalendarEventImportDriverExceptionCode.SYNC_CURSOR_ERROR: + return msg`Calendar sync error.`; + case CalendarEventImportDriverExceptionCode.UNKNOWN: + return msg`An unknown calendar error occurred.`; + case CalendarEventImportDriverExceptionCode.UNKNOWN_NETWORK_ERROR: + return msg`A network error occurred while accessing calendar.`; + case CalendarEventImportDriverExceptionCode.HANDLE_ALIASES_REQUIRED: + return msg`Handle aliases are required.`; + case CalendarEventImportDriverExceptionCode.CHANNEL_MISCONFIGURED: + return msg`Calendar channel is misconfigured.`; + default: + assertUnreachable(code); + } }; export class CalendarEventImportDriverException extends CustomException { @@ -37,7 +49,7 @@ export class CalendarEventImportDriverException extends CustomException = { - [CalendarEventImportExceptionCode.PROVIDER_NOT_SUPPORTED]: msg`Calendar provider is not supported.`, - [CalendarEventImportExceptionCode.UNKNOWN]: msg`An unknown calendar error occurred.`, +const getCalendarEventImportExceptionUserFriendlyMessage = ( + code: CalendarEventImportExceptionCode, +) => { + switch (code) { + case CalendarEventImportExceptionCode.PROVIDER_NOT_SUPPORTED: + return msg`Calendar provider is not supported.`; + case CalendarEventImportExceptionCode.UNKNOWN: + return msg`An unknown calendar error occurred.`; + default: + assertUnreachable(code); + } }; export class CalendarEventImportException extends CustomException { @@ -25,7 +31,7 @@ export class CalendarEventImportException extends CustomException = { - [ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_TOKEN_NOT_FOUND]: msg`Refresh token not found.`, - [ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN]: msg`Invalid refresh token.`, - [ConnectedAccountRefreshAccessTokenExceptionCode.PROVIDER_NOT_SUPPORTED]: msg`This provider is not supported.`, - [ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR]: msg`A temporary network error occurred.`, - [ConnectedAccountRefreshAccessTokenExceptionCode.ACCESS_TOKEN_NOT_FOUND]: msg`Access token not found.`, +const getConnectedAccountRefreshAccessTokenExceptionUserFriendlyMessage = ( + code: ConnectedAccountRefreshAccessTokenExceptionCode, +) => { + switch (code) { + case ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_TOKEN_NOT_FOUND: + return msg`Refresh token not found.`; + case ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN: + return msg`Invalid refresh token.`; + case ConnectedAccountRefreshAccessTokenExceptionCode.PROVIDER_NOT_SUPPORTED: + return msg`This provider is not supported.`; + case ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR: + return msg`A temporary network error occurred.`; + case ConnectedAccountRefreshAccessTokenExceptionCode.ACCESS_TOKEN_NOT_FOUND: + return msg`Access token not found.`; + default: + assertUnreachable(code); + } }; export class ConnectedAccountRefreshAccessTokenException extends CustomException { @@ -31,7 +40,7 @@ export class ConnectedAccountRefreshAccessTokenException extends CustomException super(message, code, { userFriendlyMessage: userFriendlyMessage ?? - connectedAccountRefreshAccessTokenExceptionUserFriendlyMessages[code], + getConnectedAccountRefreshAccessTokenExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/modules/dashboard/exceptions/dashboard.exception.ts b/packages/twenty-server/src/modules/dashboard/exceptions/dashboard.exception.ts index 837395baf9..57e63557b9 100644 --- a/packages/twenty-server/src/modules/dashboard/exceptions/dashboard.exception.ts +++ b/packages/twenty-server/src/modules/dashboard/exceptions/dashboard.exception.ts @@ -16,13 +16,19 @@ export enum DashboardExceptionMessageKey { PAGE_LAYOUT_NOT_FOUND = 'PAGE_LAYOUT_NOT_FOUND', } -const dashboardExceptionUserFriendlyMessages: Record< - DashboardExceptionCode, - MessageDescriptor -> = { - [DashboardExceptionCode.DASHBOARD_NOT_FOUND]: msg`Dashboard not found.`, - [DashboardExceptionCode.DASHBOARD_DUPLICATION_FAILED]: msg`Failed to duplicate dashboard.`, - [DashboardExceptionCode.PAGE_LAYOUT_NOT_FOUND]: msg`Page layout not found.`, +const getDashboardExceptionUserFriendlyMessage = ( + code: DashboardExceptionCode, +) => { + switch (code) { + case DashboardExceptionCode.DASHBOARD_NOT_FOUND: + return msg`Dashboard not found.`; + case DashboardExceptionCode.DASHBOARD_DUPLICATION_FAILED: + return msg`Failed to duplicate dashboard.`; + case DashboardExceptionCode.PAGE_LAYOUT_NOT_FOUND: + return msg`Page layout not found.`; + default: + assertUnreachable(code); + } }; export class DashboardException extends CustomException { @@ -33,7 +39,7 @@ export class DashboardException extends CustomException ) { super(message, code, { userFriendlyMessage: - userFriendlyMessage ?? dashboardExceptionUserFriendlyMessages[code], + userFriendlyMessage ?? getDashboardExceptionUserFriendlyMessage(code), }); } } diff --git a/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-common.exception.ts b/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-common.exception.ts index fe1b5285f7..4391e0c157 100644 --- a/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-common.exception.ts +++ b/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-common.exception.ts @@ -1,5 +1,6 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; @@ -7,11 +8,15 @@ export enum WorkflowCommonExceptionCode { OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND', } -const workflowCommonExceptionUserFriendlyMessages: Record< - WorkflowCommonExceptionCode, - MessageDescriptor -> = { - [WorkflowCommonExceptionCode.OBJECT_METADATA_NOT_FOUND]: msg`Object metadata not found.`, +const getWorkflowCommonExceptionUserFriendlyMessage = ( + code: WorkflowCommonExceptionCode, +) => { + switch (code) { + case WorkflowCommonExceptionCode.OBJECT_METADATA_NOT_FOUND: + return msg`Object metadata not found.`; + default: + assertUnreachable(code); + } }; export class WorkflowCommonException extends CustomException { @@ -23,7 +28,7 @@ export class WorkflowCommonException extends CustomException = { - [WorkflowQueryValidationExceptionCode.FORBIDDEN]: msg`You do not have permission to perform this workflow action.`, +const getWorkflowQueryValidationExceptionUserFriendlyMessage = ( + code: WorkflowQueryValidationExceptionCode, +) => { + switch (code) { + case WorkflowQueryValidationExceptionCode.FORBIDDEN: + return msg`You do not have permission to perform this workflow action.`; + default: + assertUnreachable(code); + } }; export class WorkflowQueryValidationException extends CustomException { @@ -23,7 +28,7 @@ export class WorkflowQueryValidationException extends CustomException = { - [WorkflowVersionEdgeExceptionCode.NOT_FOUND]: msg`Workflow edge not found.`, - [WorkflowVersionEdgeExceptionCode.INVALID_REQUEST]: msg`Invalid workflow edge request.`, +const getWorkflowVersionEdgeExceptionUserFriendlyMessage = ( + code: WorkflowVersionEdgeExceptionCode, +) => { + switch (code) { + case WorkflowVersionEdgeExceptionCode.NOT_FOUND: + return msg`Workflow edge not found.`; + case WorkflowVersionEdgeExceptionCode.INVALID_REQUEST: + return msg`Invalid workflow edge request.`; + default: + assertUnreachable(code); + } }; export class WorkflowVersionEdgeException extends CustomException { @@ -25,7 +31,7 @@ export class WorkflowVersionEdgeException extends CustomException = { - [WorkflowVersionStepExceptionCode.INVALID_REQUEST]: msg`Invalid workflow step request.`, - [WorkflowVersionStepExceptionCode.NOT_FOUND]: msg`Workflow step not found.`, - [WorkflowVersionStepExceptionCode.CODE_STEP_FAILURE]: msg`Code step execution failed.`, - [WorkflowVersionStepExceptionCode.AI_AGENT_STEP_FAILURE]: msg`AI agent step execution failed.`, +const getWorkflowVersionStepExceptionUserFriendlyMessage = ( + code: WorkflowVersionStepExceptionCode, +) => { + switch (code) { + case WorkflowVersionStepExceptionCode.INVALID_REQUEST: + return msg`Invalid workflow step request.`; + case WorkflowVersionStepExceptionCode.NOT_FOUND: + return msg`Workflow step not found.`; + case WorkflowVersionStepExceptionCode.CODE_STEP_FAILURE: + return msg`Code step execution failed.`; + case WorkflowVersionStepExceptionCode.AI_AGENT_STEP_FAILURE: + return msg`AI agent step execution failed.`; + default: + assertUnreachable(code); + } }; export class WorkflowVersionStepException extends CustomException { @@ -29,7 +37,7 @@ export class WorkflowVersionStepException extends CustomException = { - [WorkflowStepExecutorExceptionCode.SCOPED_WORKSPACE_NOT_FOUND]: msg`Workspace not found.`, - [WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE]: msg`Invalid workflow step type.`, - [WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND]: msg`Workflow step not found.`, - [WorkflowStepExecutorExceptionCode.INTERNAL_ERROR]: msg`An unexpected workflow error occurred.`, +const getWorkflowStepExecutorExceptionUserFriendlyMessage = ( + code: WorkflowStepExecutorExceptionCode, +) => { + switch (code) { + case WorkflowStepExecutorExceptionCode.SCOPED_WORKSPACE_NOT_FOUND: + return msg`Workspace not found.`; + case WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE: + return msg`Invalid workflow step type.`; + case WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND: + return msg`Workflow step not found.`; + case WorkflowStepExecutorExceptionCode.INTERNAL_ERROR: + return STANDARD_ERROR_MESSAGE; + default: + assertUnreachable(code); + } }; export class WorkflowStepExecutorException extends CustomException { @@ -29,7 +38,7 @@ export class WorkflowStepExecutorException extends CustomException = { - [WorkflowRunExceptionCode.WORKFLOW_RUN_NOT_FOUND]: msg`Workflow run not found.`, - [WorkflowRunExceptionCode.WORKFLOW_ROOT_STEP_NOT_FOUND]: msg`Workflow root step not found.`, - [WorkflowRunExceptionCode.INVALID_OPERATION]: msg`Invalid workflow operation.`, - [WorkflowRunExceptionCode.INVALID_INPUT]: msg`Invalid workflow input.`, - [WorkflowRunExceptionCode.WORKFLOW_RUN_LIMIT_REACHED]: msg`Workflow run limit reached.`, - [WorkflowRunExceptionCode.WORKFLOW_RUN_INVALID]: msg`Invalid workflow run.`, +const getWorkflowRunExceptionUserFriendlyMessage = ( + code: WorkflowRunExceptionCode, +) => { + switch (code) { + case WorkflowRunExceptionCode.WORKFLOW_RUN_NOT_FOUND: + return msg`Workflow run not found.`; + case WorkflowRunExceptionCode.WORKFLOW_ROOT_STEP_NOT_FOUND: + return msg`Workflow root step not found.`; + case WorkflowRunExceptionCode.INVALID_OPERATION: + return msg`Invalid workflow operation.`; + case WorkflowRunExceptionCode.INVALID_INPUT: + return msg`Invalid workflow input.`; + case WorkflowRunExceptionCode.WORKFLOW_RUN_LIMIT_REACHED: + return msg`Workflow run limit reached.`; + case WorkflowRunExceptionCode.WORKFLOW_RUN_INVALID: + return msg`Invalid workflow run.`; + default: + assertUnreachable(code); + } }; export class WorkflowRunException extends CustomException { @@ -32,7 +42,7 @@ export class WorkflowRunException extends CustomException = { - [WorkflowTriggerExceptionCode.INVALID_INPUT]: msg`Invalid workflow trigger input.`, - [WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER]: msg`Invalid workflow trigger configuration.`, - [WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION]: msg`Invalid workflow version.`, - [WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS]: msg`Invalid workflow status.`, - [WorkflowTriggerExceptionCode.INVALID_ACTION_TYPE]: msg`Invalid action type.`, - [WorkflowTriggerExceptionCode.NOT_FOUND]: msg`Workflow trigger not found.`, - [WorkflowTriggerExceptionCode.FORBIDDEN]: msg`You do not have permission to access this workflow.`, - [WorkflowTriggerExceptionCode.INTERNAL_ERROR]: msg`An unexpected workflow error occurred.`, +const getWorkflowTriggerExceptionUserFriendlyMessage = ( + code: WorkflowTriggerExceptionCode, +) => { + switch (code) { + case WorkflowTriggerExceptionCode.INVALID_INPUT: + return msg`Invalid workflow trigger input.`; + case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER: + return msg`Invalid workflow trigger configuration.`; + case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION: + return msg`Invalid workflow version.`; + case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS: + return msg`Invalid workflow status.`; + case WorkflowTriggerExceptionCode.INVALID_ACTION_TYPE: + return msg`Invalid action type.`; + case WorkflowTriggerExceptionCode.NOT_FOUND: + return msg`Workflow not found.`; + case WorkflowTriggerExceptionCode.FORBIDDEN: + return msg`You do not have permission to access this workflow.`; + case WorkflowTriggerExceptionCode.INTERNAL_ERROR: + return STANDARD_ERROR_MESSAGE; + default: + assertUnreachable(code); + } }; export class WorkflowTriggerException extends CustomException { @@ -37,7 +50,7 @@ export class WorkflowTriggerException extends CustomException