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:
+11
-6
@@ -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<WorkflowCommonExceptionCode> {
|
||||
@@ -23,7 +28,7 @@ export class WorkflowCommonException extends CustomException<WorkflowCommonExcep
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ??
|
||||
workflowCommonExceptionUserFriendlyMessages[code],
|
||||
getWorkflowCommonExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+11
-6
@@ -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 WorkflowQueryValidationExceptionCode {
|
||||
FORBIDDEN = 'FORBIDDEN',
|
||||
}
|
||||
|
||||
const workflowQueryValidationExceptionUserFriendlyMessages: Record<
|
||||
WorkflowQueryValidationExceptionCode,
|
||||
MessageDescriptor
|
||||
> = {
|
||||
[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<WorkflowQueryValidationExceptionCode> {
|
||||
@@ -23,7 +28,7 @@ export class WorkflowQueryValidationException extends CustomException<WorkflowQu
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ??
|
||||
workflowQueryValidationExceptionUserFriendlyMessages[code],
|
||||
getWorkflowQueryValidationExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+13
-7
@@ -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 WorkflowVersionEdgeExceptionCode {
|
||||
INVALID_REQUEST = 'INVALID_REQUEST',
|
||||
}
|
||||
|
||||
const workflowVersionEdgeExceptionUserFriendlyMessages: Record<
|
||||
WorkflowVersionEdgeExceptionCode,
|
||||
MessageDescriptor
|
||||
> = {
|
||||
[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<WorkflowVersionEdgeExceptionCode> {
|
||||
@@ -25,7 +31,7 @@ export class WorkflowVersionEdgeException extends CustomException<WorkflowVersio
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ??
|
||||
workflowVersionEdgeExceptionUserFriendlyMessages[code],
|
||||
getWorkflowVersionEdgeExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+17
-9
@@ -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';
|
||||
|
||||
@@ -10,14 +11,21 @@ export enum WorkflowVersionStepExceptionCode {
|
||||
AI_AGENT_STEP_FAILURE = 'AI_AGENT_STEP_FAILURE',
|
||||
}
|
||||
|
||||
const workflowVersionStepExceptionUserFriendlyMessages: Record<
|
||||
WorkflowVersionStepExceptionCode,
|
||||
MessageDescriptor
|
||||
> = {
|
||||
[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<WorkflowVersionStepExceptionCode> {
|
||||
@@ -29,7 +37,7 @@ export class WorkflowVersionStepException extends CustomException<WorkflowVersio
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ??
|
||||
workflowVersionStepExceptionUserFriendlyMessages[code],
|
||||
getWorkflowVersionStepExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+18
-9
@@ -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 WorkflowStepExecutorExceptionCode {
|
||||
@@ -10,14 +12,21 @@ export enum WorkflowStepExecutorExceptionCode {
|
||||
INTERNAL_ERROR = 'INTERNAL_ERROR',
|
||||
}
|
||||
|
||||
const workflowStepExecutorExceptionUserFriendlyMessages: Record<
|
||||
WorkflowStepExecutorExceptionCode,
|
||||
MessageDescriptor
|
||||
> = {
|
||||
[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<WorkflowStepExecutorExceptionCode> {
|
||||
@@ -29,7 +38,7 @@ export class WorkflowStepExecutorException extends CustomException<WorkflowStepE
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ??
|
||||
workflowStepExecutorExceptionUserFriendlyMessages[code],
|
||||
getWorkflowStepExecutorExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+21
-11
@@ -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 WorkflowRunExceptionCode {
|
||||
WORKFLOW_RUN_INVALID = 'WORKFLOW_RUN_INVALID',
|
||||
}
|
||||
|
||||
const workflowRunExceptionUserFriendlyMessages: Record<
|
||||
WorkflowRunExceptionCode,
|
||||
MessageDescriptor
|
||||
> = {
|
||||
[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<WorkflowRunExceptionCode> {
|
||||
@@ -32,7 +42,7 @@ export class WorkflowRunException extends CustomException<WorkflowRunExceptionCo
|
||||
) {
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ?? workflowRunExceptionUserFriendlyMessages[code],
|
||||
userFriendlyMessage ?? getWorkflowRunExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+26
-13
@@ -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 WorkflowTriggerExceptionCode {
|
||||
@@ -14,18 +16,29 @@ export enum WorkflowTriggerExceptionCode {
|
||||
INTERNAL_ERROR = 'INTERNAL_ERROR',
|
||||
}
|
||||
|
||||
const workflowTriggerExceptionUserFriendlyMessages: Record<
|
||||
WorkflowTriggerExceptionCode,
|
||||
MessageDescriptor
|
||||
> = {
|
||||
[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<WorkflowTriggerExceptionCode> {
|
||||
@@ -37,7 +50,7 @@ export class WorkflowTriggerException extends CustomException<WorkflowTriggerExc
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ??
|
||||
workflowTriggerExceptionUserFriendlyMessages[code],
|
||||
getWorkflowTriggerExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user