Improve userFriendlyMessage devX (#16815)
Two challenges with error messages - always provide a useful/meaningful error message for the end user instead of the generic one. eg: show "Wrong password" and not "An error occured" - avoid technical details unless error regards a technical feature. eg: show "An error occured" and not "Invalid post-hook payload."; but do show "Invalid issuer URL." as it occurs while configuring SSO What this PR does - Make userFriendlyMessage mandatory for widely used GraphqlQueryRunnerException and CommonQueryRunnerException, so that developers are forced to ask themselves what the error message should be, and as it contains very wide error codes (eg: "Bad request") which should not be mapped to just one default message - Keep userFriendlyMessage optional for service-specific exceptions (eg: workflowStepExecutorException), but convert the error code to userFriendlyMessage mapper to a switch case function with a typecheck ensuring that all codes are mapped to a message. These default messages are still overridable where they are thrown.
This commit is contained in:
+7
@@ -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(
|
||||
|
||||
+2
@@ -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.` },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -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.` },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -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}"` },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -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;
|
||||
};
|
||||
|
||||
+2
@@ -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.` },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -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}"` },
|
||||
);
|
||||
};
|
||||
|
||||
+2
@@ -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.` },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -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.` },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -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.` },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -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}"`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+9
-2
@@ -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;
|
||||
};
|
||||
|
||||
+10
-2
@@ -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;
|
||||
};
|
||||
|
||||
+2
@@ -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.` },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -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}"`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -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}"` },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -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.` },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
+7
-2
@@ -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;
|
||||
};
|
||||
|
||||
+7
-2
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user