Skip to content

Commit eab4e7e

Browse files
committed
feat(server): enforce userFriendlyMessage on all exceptions
- Make userFriendlyMessage required in CustomException (no longer optional) - Remove ForceFriendlyMessage generic parameter as it's no longer needed - Update all 74+ exception classes to provide default user-friendly messages - Each exception class now has a sensible default message using Lingui msg macro - This ensures end users always see a readable error message
1 parent 48a7a24 commit eab4e7e

File tree

94 files changed

+2342
-246
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+2342
-246
lines changed

eslint.config.react.mjs

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,217 @@ export default [
128128
additionalHooks: 'useRecoilCallback',
129129
},
130130
],
131+
132+
// Lingui - detect untranslated strings
133+
'lingui/no-unlocalized-strings': [
134+
'warn',
135+
{
136+
ignore: [
137+
// Ignore strings which are a single "word" (no spaces) and don't start with uppercase
138+
'^(?![A-Z])\\S+$',
139+
// Ignore UPPERCASE literals (constants, env vars)
140+
'^[A-Z0-9_-]+$',
141+
// Ignore strings that look like code/technical (contain special chars)
142+
'^[\\s]*$', // whitespace only
143+
'.*[{}/<>].*', // contains code-like characters
144+
'^\\d+(\\.\\d+)?(px|rem|em|%|vh|vw|s|ms)?$', // CSS units
145+
'^#[0-9a-fA-F]{3,8}$', // hex colors
146+
'^rgba?\\(.*\\)$', // rgb/rgba colors
147+
'^\\d+$', // numbers only
148+
'^https?:\\/\\/.*', // URLs
149+
'^@.*', // @ mentions or decorators
150+
'^\\/.*', // paths starting with /
151+
],
152+
ignoreNames: [
153+
// HTML/React attributes that shouldn't be translated
154+
{ regex: { pattern: 'className', flags: 'i' } },
155+
{ regex: { pattern: 'styleName', flags: 'i' } },
156+
{ regex: { pattern: 'testId', flags: 'i' } },
157+
'data-testid',
158+
'dataTestId',
159+
'src',
160+
'srcSet',
161+
'href',
162+
'target',
163+
'rel',
164+
'type',
165+
'id',
166+
'key',
167+
'name',
168+
'htmlFor',
169+
'width',
170+
'height',
171+
'fill',
172+
'stroke',
173+
'viewBox',
174+
'clipPath',
175+
'd', // SVG path
176+
'transform',
177+
'displayName',
178+
'defaultValue',
179+
'to', // router links
180+
'path',
181+
'pathname',
182+
'hash',
183+
'componentInstanceId',
184+
'hotkeyScope',
185+
'dropdownId',
186+
'recoilScopeId',
187+
'modalId',
188+
'dialogId',
189+
'color', // color prop values
190+
'variant', // component variants
191+
'size', // size prop values
192+
'position', // position values
193+
'align', // alignment values
194+
'justify', // justification values
195+
'direction', // direction values
196+
'orientation', // orientation values
197+
'status', // status values
198+
'state', // state values
199+
'mode', // mode values
200+
'accent', // accent values
201+
202+
// Styled components
203+
'css',
204+
'theme',
205+
'animation',
206+
'transition',
207+
208+
// GraphQL
209+
'query',
210+
'mutation',
211+
'subscription',
212+
'fragment',
213+
'operationName',
214+
'variables',
215+
216+
// Technical identifiers
217+
'fieldName',
218+
'columnName',
219+
'objectNameSingular',
220+
'objectNamePlural',
221+
'metadataId',
222+
'nameSingular',
223+
'namePlural',
224+
225+
// Event types
226+
'eventName',
227+
'event',
228+
'action',
229+
'actionType',
230+
231+
// UPPER_CASE names (constants)
232+
{ regex: { pattern: '^[A-Z][A-Z0-9_]*$' } },
233+
],
234+
ignoreFunctions: [
235+
// Console and logging
236+
'console.*',
237+
'*.log',
238+
'*.warn',
239+
'*.error',
240+
'*.debug',
241+
'*.info',
242+
'*.trace',
243+
244+
// Error handling (technical messages, not user-facing)
245+
'Error',
246+
'TypeError',
247+
'RangeError',
248+
'SyntaxError',
249+
'throw',
250+
251+
// Testing
252+
'describe',
253+
'it',
254+
'test',
255+
'expect',
256+
'jest.*',
257+
'*.toBe',
258+
'*.toEqual',
259+
'*.toContain',
260+
'*.toMatch',
261+
'*.toThrow',
262+
263+
// React/Libraries internals
264+
'require',
265+
'import',
266+
'styled',
267+
'styled.*',
268+
'css',
269+
'keyframes',
270+
'createGlobalStyle',
271+
272+
// Router
273+
'useNavigate',
274+
'navigate',
275+
'useLocation',
276+
'useParams',
277+
278+
// Recoil
279+
'atom',
280+
'atomFamily',
281+
'selector',
282+
'selectorFamily',
283+
'useSetRecoilState',
284+
'useRecoilState',
285+
'useRecoilValue',
286+
287+
// GraphQL operations
288+
'gql',
289+
'useQuery',
290+
'useMutation',
291+
'useLazyQuery',
292+
'useSubscription',
293+
294+
// Type checking and validation
295+
'*.includes',
296+
'*.indexOf',
297+
'*.startsWith',
298+
'*.endsWith',
299+
'*.split',
300+
'*.join',
301+
'*.match',
302+
'*.replace',
303+
'*.test',
304+
'Object.keys',
305+
'Object.values',
306+
'Object.entries',
307+
'Array.isArray',
308+
309+
// DOM operations
310+
'*.getElementById',
311+
'*.getElementsByClassName',
312+
'*.querySelector',
313+
'*.querySelectorAll',
314+
'*.getAttribute',
315+
'*.setAttribute',
316+
'*.addEventListener',
317+
'*.removeEventListener',
318+
'*.dispatchEvent',
319+
'*.createElement',
320+
321+
// Storage
322+
'localStorage.*',
323+
'sessionStorage.*',
324+
'searchParams.*',
325+
'*.get',
326+
'*.set',
327+
'*.has',
328+
'*.delete',
329+
330+
// Misc utilities
331+
'cva',
332+
'cn',
333+
'clsx',
334+
'classNames',
335+
'track',
336+
'*.postMessage',
337+
'*.dispatch',
338+
'*.commit',
339+
],
340+
},
341+
],
131342
},
132343
},
133344

packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { CustomException } from 'src/utils/custom-exception';
1+
import { type MessageDescriptor } from '@lingui/core';
2+
import { msg } from '@lingui/core/macro';
23

3-
export class CommonQueryRunnerException extends CustomException<CommonQueryRunnerExceptionCode> {}
4+
import { CustomException } from 'src/utils/custom-exception';
45

56
export enum CommonQueryRunnerExceptionCode {
67
RECORD_NOT_FOUND = 'RECORD_NOT_FOUND',
@@ -18,3 +19,37 @@ export enum CommonQueryRunnerExceptionCode {
1819
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
1920
TOO_COMPLEX_QUERY = 'TOO_COMPLEX_QUERY',
2021
}
22+
23+
const commonQueryRunnerExceptionUserFriendlyMessages: Record<
24+
CommonQueryRunnerExceptionCode,
25+
MessageDescriptor
26+
> = {
27+
[CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND]: msg`Record not found.`,
28+
[CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT]: msg`Invalid query input.`,
29+
[CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT]: msg`Invalid authentication context.`,
30+
[CommonQueryRunnerExceptionCode.ARGS_CONFLICT]: msg`Conflicting arguments provided.`,
31+
[CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA]: msg`Invalid data provided.`,
32+
[CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST]: msg`Invalid 'first' argument.`,
33+
[CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST]: msg`Invalid 'last' argument.`,
34+
[CommonQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT]: msg`Multiple matching records found during upsert.`,
35+
[CommonQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD]: msg`Missing required system field.`,
36+
[CommonQueryRunnerExceptionCode.INVALID_CURSOR]: msg`Invalid cursor provided.`,
37+
[CommonQueryRunnerExceptionCode.TOO_MANY_RECORDS_TO_UPDATE]: msg`Too many records to update at once.`,
38+
[CommonQueryRunnerExceptionCode.BAD_REQUEST]: msg`Bad request.`,
39+
[CommonQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR]: msg`An unexpected error occurred.`,
40+
[CommonQueryRunnerExceptionCode.TOO_COMPLEX_QUERY]: msg`Query is too complex.`,
41+
};
42+
43+
export class CommonQueryRunnerException extends CustomException<CommonQueryRunnerExceptionCode> {
44+
constructor(
45+
message: string,
46+
code: CommonQueryRunnerExceptionCode,
47+
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
48+
) {
49+
super(message, code, {
50+
userFriendlyMessage:
51+
userFriendlyMessage ??
52+
commonQueryRunnerExceptionUserFriendlyMessages[code],
53+
});
54+
}
55+
}

packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { CustomException } from 'src/utils/custom-exception';
1+
import { type MessageDescriptor } from '@lingui/core';
2+
import { msg } from '@lingui/core/macro';
23

3-
export class GraphqlQueryRunnerException extends CustomException<GraphqlQueryRunnerExceptionCode> {}
4+
import { CustomException } from 'src/utils/custom-exception';
45

56
export enum GraphqlQueryRunnerExceptionCode {
67
INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT',
@@ -22,3 +23,41 @@ export enum GraphqlQueryRunnerExceptionCode {
2223
UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT = 'UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT',
2324
UPSERT_MAX_RECORDS_EXCEEDED = 'UPSERT_MAX_RECORDS_EXCEEDED',
2425
}
26+
27+
const graphqlQueryRunnerExceptionUserFriendlyMessages: Record<
28+
GraphqlQueryRunnerExceptionCode,
29+
MessageDescriptor
30+
> = {
31+
[GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT]: msg`Invalid query input.`,
32+
[GraphqlQueryRunnerExceptionCode.MAX_DEPTH_REACHED]: msg`Maximum query depth reached.`,
33+
[GraphqlQueryRunnerExceptionCode.INVALID_CURSOR]: msg`Invalid cursor provided.`,
34+
[GraphqlQueryRunnerExceptionCode.INVALID_DIRECTION]: msg`Invalid direction provided.`,
35+
[GraphqlQueryRunnerExceptionCode.UNSUPPORTED_OPERATOR]: msg`Unsupported operator.`,
36+
[GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT]: msg`Conflicting arguments provided.`,
37+
[GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND]: msg`Field not found.`,
38+
[GraphqlQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD]: msg`Missing required system field.`,
39+
[GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND]: msg`Object not found.`,
40+
[GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND]: msg`Record not found.`,
41+
[GraphqlQueryRunnerExceptionCode.INVALID_ARGS_FIRST]: msg`Invalid 'first' argument.`,
42+
[GraphqlQueryRunnerExceptionCode.INVALID_ARGS_LAST]: msg`Invalid 'last' argument.`,
43+
[GraphqlQueryRunnerExceptionCode.RELATION_SETTINGS_NOT_FOUND]: msg`Relation settings not found.`,
44+
[GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND]: msg`Relation target not found.`,
45+
[GraphqlQueryRunnerExceptionCode.NOT_IMPLEMENTED]: msg`This feature is not implemented.`,
46+
[GraphqlQueryRunnerExceptionCode.INVALID_POST_HOOK_PAYLOAD]: msg`Invalid post-hook payload.`,
47+
[GraphqlQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT]: msg`Multiple matching records found during upsert.`,
48+
[GraphqlQueryRunnerExceptionCode.UPSERT_MAX_RECORDS_EXCEEDED]: msg`Maximum records exceeded for upsert.`,
49+
};
50+
51+
export class GraphqlQueryRunnerException extends CustomException<GraphqlQueryRunnerExceptionCode> {
52+
constructor(
53+
message: string,
54+
code: GraphqlQueryRunnerExceptionCode,
55+
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
56+
) {
57+
super(message, code, {
58+
userFriendlyMessage:
59+
userFriendlyMessage ??
60+
graphqlQueryRunnerExceptionUserFriendlyMessages[code],
61+
});
62+
}
63+
}
Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1+
import { type MessageDescriptor } from '@lingui/core';
2+
import { msg } from '@lingui/core/macro';
3+
14
import {
25
appendCommonExceptionCode,
36
CustomException,
47
} from 'src/utils/custom-exception';
58

6-
export class WorkspaceQueryRunnerException extends CustomException<
7-
keyof typeof WorkspaceQueryRunnerExceptionCode
8-
> {}
9-
109
export const WorkspaceQueryRunnerExceptionCode = appendCommonExceptionCode({
1110
INVALID_QUERY_INPUT: 'INVALID_QUERY_INPUT',
1211
DATA_NOT_FOUND: 'DATA_NOT_FOUND',
@@ -17,3 +16,33 @@ export const WorkspaceQueryRunnerExceptionCode = appendCommonExceptionCode({
1716
TOO_MANY_ROWS_AFFECTED: 'TOO_MANY_ROWS_AFFECTED',
1817
NO_ROWS_AFFECTED: 'NO_ROWS_AFFECTED',
1918
} as const);
19+
20+
const workspaceQueryRunnerExceptionUserFriendlyMessages: Record<
21+
keyof typeof WorkspaceQueryRunnerExceptionCode,
22+
MessageDescriptor
23+
> = {
24+
INVALID_QUERY_INPUT: msg`Invalid query input.`,
25+
DATA_NOT_FOUND: msg`Data not found.`,
26+
QUERY_TIMEOUT: msg`Query timed out.`,
27+
QUERY_VIOLATES_UNIQUE_CONSTRAINT: msg`A record with this value already exists.`,
28+
QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT: msg`Cannot complete operation due to related records.`,
29+
TOO_MANY_ROWS_AFFECTED: msg`Too many records affected.`,
30+
NO_ROWS_AFFECTED: msg`No records were affected.`,
31+
INTERNAL_SERVER_ERROR: msg`An unexpected error occurred.`,
32+
};
33+
34+
export class WorkspaceQueryRunnerException extends CustomException<
35+
keyof typeof WorkspaceQueryRunnerExceptionCode
36+
> {
37+
constructor(
38+
message: string,
39+
code: keyof typeof WorkspaceQueryRunnerExceptionCode,
40+
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
41+
) {
42+
super(message, code, {
43+
userFriendlyMessage:
44+
userFriendlyMessage ??
45+
workspaceQueryRunnerExceptionUserFriendlyMessages[code],
46+
});
47+
}
48+
}

0 commit comments

Comments
 (0)