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