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:
Marie
2025-12-30 10:08:43 +01:00
committed by GitHub
parent 7522ff6675
commit 19c9f957b1
135 changed files with 1672 additions and 845 deletions
@@ -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.` },
);
}
};
@@ -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 },
);
}
@@ -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),
});
}
}