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:
@@ -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 { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export enum RelationExceptionCode {
|
||||
@@ -11,15 +12,19 @@ export enum RelationExceptionCode {
|
||||
MULTIPLE_JOIN_COLUMNS_FOUND = 'MULTIPLE_JOIN_COLUMNS_FOUND',
|
||||
}
|
||||
|
||||
const relationExceptionUserFriendlyMessages: Record<
|
||||
RelationExceptionCode,
|
||||
MessageDescriptor
|
||||
> = {
|
||||
[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<RelationExceptionCode> {
|
||||
@@ -30,7 +35,7 @@ export class RelationException extends CustomException<RelationExceptionCode> {
|
||||
) {
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ?? relationExceptionUserFriendlyMessages[code],
|
||||
userFriendlyMessage ?? getRelationExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TwentyORMExceptionCode> {
|
||||
@@ -58,7 +81,7 @@ export class TwentyORMException extends CustomException<TwentyORMExceptionCode>
|
||||
) {
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ?? twentyORMExceptionUserFriendlyMessages[code],
|
||||
userFriendlyMessage ?? getTwentyORMExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+13
-8
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user