Files
twenty/packages/twenty-server/src/engine/metadata-modules/view-field/exceptions/view-field.exception.ts
T
Félix Malfait 04c596817a feat(server): enforce userFriendlyMessage on all exceptions (#16589)
## Summary
This PR enforces that all custom exceptions must provide a
`userFriendlyMessage`, ensuring end users always see readable error
messages.

## Changes

### Core Changes
- **`CustomException` simplified**: Removed the `ForceFriendlyMessage`
generic parameter - `userFriendlyMessage` is now always required
- **Type safety**: The constructor now requires `{ userFriendlyMessage:
MessageDescriptor }` (no longer optional)

### Updated Files
- **74+ exception classes** updated to provide default user-friendly
messages using Lingui `msg` macro
- Each exception class has a sensible fallback message (e.g., `msg\`An
authentication error occurred.\``)
- Exception classes that had code-specific message maps retain their
behavior

## Benefits
- **Compile-time enforcement**: Forgetting to add a user-friendly
message now causes a TypeScript error
- **Better UX**: End users always see a localized, human-readable error
message
- **Simpler API**: No more boolean generic parameter to think about

## Testing
- `npx nx run twenty-server:typecheck` passes
- `npx nx run twenty-server:lint` passes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Enforces `userFriendlyMessage` on `CustomException` and updates all
exception classes to supply localized default messages, with
filters/tests adjusted accordingly.
> 
> - **Core**:
> - Enforce required `userFriendlyMessage` in `CustomException` (remove
optional generic; constructor now requires `{ userFriendlyMessage:
MessageDescriptor }`).
> - **Exceptions**:
> - Update ~70+ exception classes to set default localized messages via
Lingui `msg` maps and pass them in constructors (e.g., `AuthException`,
`ObjectMetadataException`, `FieldMetadataException`, etc.).
> - Add fallback messages where needed (e.g., `INTERNAL_SERVER_ERROR` or
domain-specific defaults).
> - **HTTP/GraphQL Filters**:
> - Ensure fallbacks create `UnknownException` with `msg` for
user-friendly text in REST/GraphQL exception filters.
> - **Tests**:
>   - Adjust unit tests to pass `userFriendlyMessage` to exceptions.
> - Update Jest snapshots to include `extensions.userFriendlyMessage` or
message objects where applicable.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
221004fdfc0d97b7d152a258b347bf571e70f10e. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2025-12-16 14:44:27 +01:00

74 lines
2.7 KiB
TypeScript

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';
export class ViewFieldException extends CustomException<ViewFieldExceptionCode> {
constructor(
message: string,
code: ViewFieldExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ?? msg`A view field error occurred.`,
});
}
}
export enum ViewFieldExceptionCode {
VIEW_FIELD_NOT_FOUND = 'VIEW_FIELD_NOT_FOUND',
VIEW_NOT_FOUND = 'VIEW_NOT_FOUND',
INVALID_VIEW_FIELD_DATA = 'INVALID_VIEW_FIELD_DATA',
}
export enum ViewFieldExceptionMessageKey {
WORKSPACE_ID_REQUIRED = 'WORKSPACE_ID_REQUIRED',
VIEW_ID_REQUIRED = 'VIEW_ID_REQUIRED',
VIEW_FIELD_NOT_FOUND = 'VIEW_FIELD_NOT_FOUND',
VIEW_NOT_FOUND = 'VIEW_NOT_FOUND',
INVALID_VIEW_FIELD_DATA = 'INVALID_VIEW_FIELD_DATA',
FIELD_METADATA_ID_REQUIRED = 'FIELD_METADATA_ID_REQUIRED',
VIEW_FIELD_ALREADY_EXISTS = 'VIEW_FIELD_ALREADY_EXISTS',
}
export const generateViewFieldExceptionMessage = (
key: ViewFieldExceptionMessageKey,
id?: string,
) => {
switch (key) {
case ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return 'WorkspaceId is required';
case ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED:
return 'ViewId is required';
case ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND:
return `View field${id ? ` (id: ${id})` : ''} not found`;
case ViewFieldExceptionMessageKey.VIEW_NOT_FOUND:
return `View${id ? ` (id: ${id})` : ''} not found`;
case ViewFieldExceptionMessageKey.INVALID_VIEW_FIELD_DATA:
return `Invalid view field data${id ? ` for view field id: ${id}` : ''}`;
case ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return 'FieldMetadataId is required';
case ViewFieldExceptionMessageKey.VIEW_FIELD_ALREADY_EXISTS:
return 'View field already exists';
default:
assertUnreachable(key);
}
};
export const generateViewFieldUserFriendlyExceptionMessage = (
key: ViewFieldExceptionMessageKey,
): MessageDescriptor | undefined => {
switch (key) {
case ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return msg`WorkspaceId is required to create a view field.`;
case ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED:
return msg`ViewId is required to create a view field.`;
case ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return msg`FieldMetadataId is required to create a view field.`;
case ViewFieldExceptionMessageKey.VIEW_FIELD_ALREADY_EXISTS:
return msg`View field already exists.`;
}
};