@venizia/ignis-docs 0.2.0 → 0.2.1-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +22 -11
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +6 -2
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +182 -93
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +107 -322
- package/content/extensions/components/authentication/api.md +454 -603
- package/content/extensions/components/authentication/errors.md +121 -498
- package/content/extensions/components/authentication/index.md +88 -801
- package/content/extensions/components/authentication/usage.md +207 -956
- package/content/extensions/components/authorization/api.md +736 -656
- package/content/extensions/components/authorization/errors.md +168 -206
- package/content/extensions/components/authorization/index.md +82 -797
- package/content/extensions/components/authorization/usage.md +194 -527
- package/content/extensions/components/health-check.md +71 -243
- package/content/extensions/components/mail/api.md +504 -287
- package/content/extensions/components/mail/errors.md +73 -61
- package/content/extensions/components/mail/index.md +96 -467
- package/content/extensions/components/mail/usage.md +130 -172
- package/content/extensions/components/request-tracker.md +66 -173
- package/content/extensions/components/socket-io/api.md +195 -13
- package/content/extensions/components/socket-io/errors.md +3 -3
- package/content/extensions/components/socket-io/index.md +50 -337
- package/content/extensions/components/socket-io/usage.md +143 -26
- package/content/extensions/components/static-asset/api.md +410 -142
- package/content/extensions/components/static-asset/errors.md +110 -53
- package/content/extensions/components/static-asset/index.md +79 -608
- package/content/extensions/components/static-asset/usage.md +180 -300
- package/content/extensions/components/websocket/api.md +275 -399
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +74 -407
- package/content/extensions/components/websocket/usage.md +110 -341
- package/content/extensions/helpers/cron/index.md +51 -160
- package/content/extensions/helpers/crypto/index.md +62 -483
- package/content/extensions/helpers/crypto/reference.md +456 -0
- package/content/extensions/helpers/env/index.md +60 -178
- package/content/extensions/helpers/error/index.md +221 -207
- package/content/extensions/helpers/inversion/index.md +65 -556
- package/content/extensions/helpers/inversion/reference.md +522 -0
- package/content/extensions/helpers/kafka/admin.md +20 -1
- package/content/extensions/helpers/kafka/compile-binary.md +41 -35
- package/content/extensions/helpers/kafka/consumer.md +54 -20
- package/content/extensions/helpers/kafka/examples.md +21 -16
- package/content/extensions/helpers/kafka/index.md +80 -610
- package/content/extensions/helpers/kafka/producer.md +134 -5
- package/content/extensions/helpers/kafka/schema-registry.md +45 -70
- package/content/extensions/helpers/logger/hf-logger.md +193 -0
- package/content/extensions/helpers/logger/index.md +64 -563
- package/content/extensions/helpers/logger/pino.md +85 -0
- package/content/extensions/helpers/logger/reference.md +746 -0
- package/content/extensions/helpers/network/api.md +241 -195
- package/content/extensions/helpers/network/index.md +72 -530
- package/content/extensions/helpers/queue/index.md +72 -900
- package/content/extensions/helpers/queue/reference.md +467 -0
- package/content/extensions/helpers/redis/index.md +73 -645
- package/content/extensions/helpers/redis/reference.md +727 -0
- package/content/extensions/helpers/secrets/index.md +66 -0
- package/content/extensions/helpers/socket-io/api.md +305 -203
- package/content/extensions/helpers/socket-io/index.md +66 -432
- package/content/extensions/helpers/storage/api.md +564 -462
- package/content/extensions/helpers/storage/index.md +77 -573
- package/content/extensions/helpers/types/index.md +66 -499
- package/content/extensions/helpers/types/reference.md +650 -0
- package/content/extensions/helpers/uid/index.md +58 -227
- package/content/extensions/helpers/websocket/api.md +329 -216
- package/content/extensions/helpers/websocket/index.md +65 -503
- package/content/extensions/helpers/worker-thread/index.md +58 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
- package/content/guides/core-concepts/persistent/transactions.md +1 -1
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/migrations/redis-helpers-migration.md +1 -1
- package/content/guides/migrations/unified-connectors-migration.md +2 -2
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/references/base/application.md +1 -1
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/datasources-reference.md +599 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +17 -39
- package/content/references/base/filter-system/application-usage.md +69 -121
- package/content/references/base/filter-system/array-operators.md +12 -0
- package/content/references/base/filter-system/comparison-operators.md +12 -0
- package/content/references/base/filter-system/default-filter.md +136 -348
- package/content/references/base/filter-system/fields-order-pagination.md +38 -16
- package/content/references/base/filter-system/index.md +106 -257
- package/content/references/base/filter-system/json-filtering.md +12 -2
- package/content/references/base/filter-system/list-operators.md +16 -2
- package/content/references/base/filter-system/logical-operators.md +13 -0
- package/content/references/base/filter-system/null-operators.md +13 -0
- package/content/references/base/filter-system/pattern-matching.md +12 -0
- package/content/references/base/filter-system/quick-reference.md +11 -2
- package/content/references/base/filter-system/range-operators.md +12 -0
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +156 -233
- package/content/references/base/middlewares.md +35 -21
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +80 -1452
- package/content/references/base/repositories/advanced.md +156 -192
- package/content/references/base/repositories/index.md +77 -650
- package/content/references/base/repositories/mixins.md +22 -18
- package/content/references/base/repositories/relations.md +123 -171
- package/content/references/base/repositories/soft-deletable.md +58 -56
- package/content/references/base/secrets.md +263 -0
- package/content/references/base/services.md +2 -2
- package/content/references/configuration/environment-variables.md +48 -4
- package/content/references/configuration/index.md +49 -31
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/index.md +1 -1
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +29 -62
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +57 -218
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- package/package.json +8 -8
|
@@ -24,24 +24,67 @@ throw getError({
|
|
|
24
24
|
message: 'User not found',
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
// Error with
|
|
27
|
+
// Error with context - any key the framework does not model lands in `extra`
|
|
28
28
|
throw getError({
|
|
29
29
|
statusCode: HTTP.ResultCodes.RS_4.BadRequest,
|
|
30
30
|
message: 'Invalid request',
|
|
31
|
-
details: {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
details: { field: 'email', reason: 'Must be a valid email address' },
|
|
32
|
+
});
|
|
33
|
+
// -> error.extra.details
|
|
34
|
+
|
|
35
|
+
// The same thing, explicit. Prefer this when the context could be mistaken for a field.
|
|
36
|
+
throw getError({
|
|
37
|
+
statusCode: HTTP.ResultCodes.RS_4.BadRequest,
|
|
38
|
+
message: 'Invalid request',
|
|
39
|
+
extra: { details: { field: 'email' } },
|
|
35
40
|
});
|
|
36
41
|
|
|
37
|
-
//
|
|
42
|
+
// Wrapping a lower-level failure - `cause` reaches the native Error.cause
|
|
38
43
|
throw getError({
|
|
39
44
|
statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
|
|
40
45
|
message: '[UserService][create] Database connection failed',
|
|
41
|
-
|
|
46
|
+
cause: error,
|
|
47
|
+
extra: { userId: requestedId },
|
|
42
48
|
});
|
|
43
49
|
```
|
|
44
50
|
|
|
51
|
+
> [!NOTE]
|
|
52
|
+
> Any key `getError` does not model rides into `extra` - that is how a throw site attaches context the framework knows nothing about. The trade is that a **misspelling** goes the same way: `getError({ message, statuscode: 503 })` compiles, `statusCode` stays `400`, and `503` sits in `extra.statuscode`. The framework cannot tell your context from your typo.
|
|
53
|
+
|
|
54
|
+
### Catalogue a domain failure, raise the rest free-form
|
|
55
|
+
|
|
56
|
+
The form above is right for a failure that carries no i18n code: an invariant, a misconfiguration, a seed guard. Nobody translates `'[UserService][create] Database connection failed'`.
|
|
57
|
+
|
|
58
|
+
A **domain** failure - one a client localizes and branches on - belongs in a catalog instead. Retyping its code and status at each throw is how two call sites end up raising `category.create.duplicate_name` and `category.duplicate_name` for the same thing:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { ErrorScopes, getError, HTTP } from '@venizia/ignis-helpers';
|
|
62
|
+
import type { TErrorDefinition, TRegisterErrors } from '@venizia/ignis-helpers';
|
|
63
|
+
|
|
64
|
+
export const UserErrors = {
|
|
65
|
+
CREATE_DUPLICATE_EMAIL: {
|
|
66
|
+
message: {
|
|
67
|
+
text: 'An account with %{email} already exists.',
|
|
68
|
+
code: 'server.core.user.create.duplicate_email',
|
|
69
|
+
},
|
|
70
|
+
statusCode: HTTP.ResultCodes.RS_4.Conflict,
|
|
71
|
+
category: ErrorScopes.VALIDATION,
|
|
72
|
+
description: 'Sign-up rejected because the email is already registered.',
|
|
73
|
+
},
|
|
74
|
+
} as const satisfies Record<string, TErrorDefinition>;
|
|
75
|
+
|
|
76
|
+
declare module '@venizia/ignis-helpers' {
|
|
77
|
+
interface IErrorKeyRegistry extends TRegisterErrors<typeof UserErrors> {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// At every throw site:
|
|
81
|
+
throw getError({ error: UserErrors.CREATE_DUPLICATE_EMAIL, messageArgs: { email } });
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Pass the definition as `error` - **never spread it**. `getError({ ...UserErrors.CREATE_DUPLICATE_EMAIL, messageArgs: { email } })` reads naturally and is wrong: spreading skips the `error:` field entirely, so `category` and `description` - fields only modeled inside a definition - fall through into `extra` instead of staying structured. `statusCode` and `message` happen to still resolve correctly only because their shapes collide with what `getError` expects standalone; nothing catches the rest for you.
|
|
85
|
+
|
|
86
|
+
See the [Error helper reference](/extensions/helpers/error/) for the full surface.
|
|
87
|
+
|
|
45
88
|
## 2. HTTP Status Code Reference
|
|
46
89
|
|
|
47
90
|
Use the correct status code for each error type:
|
|
@@ -90,9 +133,9 @@ export class UserService extends BaseService {
|
|
|
90
133
|
try {
|
|
91
134
|
await this.emailService.sendWelcome(data.email);
|
|
92
135
|
} catch (error) {
|
|
93
|
-
// Log but don't fail user creation
|
|
136
|
+
// Log but don't fail user creation - never a silent catch
|
|
94
137
|
this.logger.error('[createUser] Failed to send welcome email | email: %s | error: %s',
|
|
95
|
-
data.email, error
|
|
138
|
+
data.email, error);
|
|
96
139
|
}
|
|
97
140
|
|
|
98
141
|
// create returns { count, data }
|
|
@@ -158,9 +201,18 @@ Database errors in SQLSTATE classes `22` (data exception), `23` (integrity const
|
|
|
158
201
|
```json
|
|
159
202
|
{
|
|
160
203
|
"message": "Unique constraint violation\nDetail: Key (email)=(test@example.com) already exists.\nTable: User\nConstraint: UQ_User_email",
|
|
161
|
-
"messageCode": "core.system_error",
|
|
162
204
|
"statusCode": 400,
|
|
163
|
-
"
|
|
205
|
+
"normalized": {
|
|
206
|
+
"text": "Unique constraint violation\nDetail: Key (email)=(test@example.com) already exists.\nTable: User\nConstraint: UQ_User_email",
|
|
207
|
+
"code": "core.system_error",
|
|
208
|
+
"args": {}
|
|
209
|
+
},
|
|
210
|
+
"requestId": "abc123",
|
|
211
|
+
"details": {
|
|
212
|
+
"url": "http://localhost:3000/users",
|
|
213
|
+
"path": "/users",
|
|
214
|
+
"stack": "Error: Unique constraint violation\n at ..."
|
|
215
|
+
}
|
|
164
216
|
}
|
|
165
217
|
```
|
|
166
218
|
|
|
@@ -168,18 +220,24 @@ Database errors in SQLSTATE classes `22` (data exception), `23` (integrity const
|
|
|
168
220
|
In production the message is the **base message only** - `Detail:` (which echoes row values like emails), `Table:`, and `Constraint:` are stripped, and `details.stack`/`details.cause` are omitted. Unexpected (non-client) database errors and connection failures return a generic `"Internal Server Error"`, so SQL, schema names, and connection host/port never leak. Use `requestId` + server logs to diagnose.
|
|
169
221
|
|
|
170
222
|
```json
|
|
171
|
-
{
|
|
223
|
+
{
|
|
224
|
+
"message": "Unique constraint violation",
|
|
225
|
+
"statusCode": 400,
|
|
226
|
+
"normalized": { "text": "Unique constraint violation", "code": "core.system_error", "args": {} },
|
|
227
|
+
"requestId": "abc123",
|
|
228
|
+
"details": { "url": "http://localhost:3000/users", "path": "/users" }
|
|
229
|
+
}
|
|
172
230
|
```
|
|
173
231
|
:::
|
|
174
232
|
|
|
175
233
|
You don't need to wrap repository calls in try-catch for constraint errors. If you need custom error messages, you can still handle them explicitly:
|
|
176
234
|
|
|
177
235
|
```typescript
|
|
178
|
-
import {
|
|
236
|
+
import { DefaultRelationalRepository, type TCount } from '@venizia/ignis';
|
|
179
237
|
import { getError, HTTP } from '@venizia/ignis-helpers';
|
|
180
238
|
|
|
181
|
-
export class UserRepository extends
|
|
182
|
-
async createWithCustomError(data: TCreateUser): Promise<
|
|
239
|
+
export class UserRepository extends DefaultRelationalRepository<typeof User.schema> {
|
|
240
|
+
async createWithCustomError(data: TCreateUser): Promise<TCount & { data: TUser }> {
|
|
183
241
|
try {
|
|
184
242
|
return await this.create({ data });
|
|
185
243
|
} catch (error) {
|
|
@@ -198,41 +256,43 @@ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
|
|
|
198
256
|
|
|
199
257
|
## 4. Global Error Handler
|
|
200
258
|
|
|
201
|
-
IGNIS
|
|
259
|
+
IGNIS wires a built-in handler by default - `AppErrorMiddleware` from `@venizia/ignis` is a class, registered as `new AppErrorMiddleware({ logger, rootKey }).value()`, and `value()` returns the Hono `ErrorHandler`. You rarely need to replace it; when you do, keep the same response contract:
|
|
202
260
|
|
|
203
261
|
```typescript
|
|
204
262
|
import { BaseApplication } from '@venizia/ignis';
|
|
205
|
-
import {
|
|
263
|
+
import { isApplicationError, MessageCode } from '@venizia/ignis-helpers';
|
|
206
264
|
|
|
207
265
|
export class Application extends BaseApplication {
|
|
208
266
|
override setupMiddlewares(): void {
|
|
209
267
|
super.setupMiddlewares();
|
|
210
268
|
|
|
211
|
-
// Custom error handler (optional)
|
|
212
|
-
this.server
|
|
269
|
+
// Custom error handler (optional) - the default AppErrorMiddleware already does this.
|
|
270
|
+
// `this.server` is `{ hono, runtime, instance }` - reach the Hono app via getServer().
|
|
271
|
+
this.getServer().onError((error, c) => {
|
|
213
272
|
const requestId = c.get('requestId') ?? 'unknown';
|
|
214
273
|
|
|
215
274
|
// Log all errors
|
|
216
|
-
this.logger.error('[%s] Error | %s', requestId, error
|
|
275
|
+
this.logger.error('[%s] Error | %s', requestId, error);
|
|
217
276
|
|
|
218
|
-
// Handle known application errors
|
|
219
|
-
|
|
277
|
+
// Handle known application errors - isApplicationError(), never `instanceof
|
|
278
|
+
// ApplicationError` (unreliable across package boundaries)
|
|
279
|
+
if (isApplicationError(error)) {
|
|
220
280
|
return c.json({
|
|
221
281
|
statusCode: error.statusCode,
|
|
222
282
|
message: error.message,
|
|
223
|
-
|
|
224
|
-
|
|
283
|
+
normalized: error.normalized, // { text, code, args } - never undefined
|
|
284
|
+
extra: error.extra,
|
|
225
285
|
requestId,
|
|
226
|
-
}, error.statusCode as
|
|
286
|
+
}, error.statusCode as Parameters<typeof c.json>[1]);
|
|
227
287
|
}
|
|
228
288
|
|
|
229
|
-
// Handle Zod validation errors
|
|
289
|
+
// Handle Zod validation errors - Zod v4 exposes `issues`, not `errors`
|
|
230
290
|
if (error.name === 'ZodError') {
|
|
231
291
|
return c.json({
|
|
232
292
|
statusCode: 422,
|
|
233
293
|
message: 'Validation failed',
|
|
234
|
-
|
|
235
|
-
details: { cause: error.
|
|
294
|
+
normalized: { text: 'Validation failed', code: MessageCode.DEFAULT, args: {} },
|
|
295
|
+
details: { cause: error.issues },
|
|
236
296
|
requestId,
|
|
237
297
|
}, 422);
|
|
238
298
|
}
|
|
@@ -241,7 +301,7 @@ export class Application extends BaseApplication {
|
|
|
241
301
|
return c.json({
|
|
242
302
|
statusCode: 500,
|
|
243
303
|
message: 'Internal server error',
|
|
244
|
-
|
|
304
|
+
normalized: { text: 'Internal server error', code: MessageCode.DEFAULT, args: {} },
|
|
245
305
|
requestId,
|
|
246
306
|
}, 500);
|
|
247
307
|
});
|
|
@@ -257,51 +317,61 @@ All errors should follow a consistent format:
|
|
|
257
317
|
interface ErrorResponse {
|
|
258
318
|
statusCode: number;
|
|
259
319
|
message: string;
|
|
260
|
-
|
|
320
|
+
normalized: {
|
|
321
|
+
text: string;
|
|
322
|
+
code: string; // stable, localizable code (validation: from params.code or the raw Zod code)
|
|
323
|
+
args: Record<string, unknown>;
|
|
324
|
+
};
|
|
261
325
|
requestId: string;
|
|
262
326
|
extra?: Record<string, unknown>; // structured context attached via getError(...)
|
|
263
|
-
details
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}>;
|
|
327
|
+
details: {
|
|
328
|
+
url: string;
|
|
329
|
+
path: string;
|
|
330
|
+
stack?: string; // non-production only
|
|
331
|
+
cause?: unknown; // non-production only, or the Zod issue list for 422s
|
|
269
332
|
[key: string]: unknown;
|
|
270
333
|
};
|
|
271
334
|
}
|
|
272
335
|
```
|
|
273
336
|
|
|
337
|
+
There is no top-level `messageCode` - read `normalized.code`. `extra` never mirrors `messageArgs`; the resolved interpolation values live at `normalized.args`.
|
|
338
|
+
|
|
274
339
|
**Example Responses:**
|
|
275
340
|
|
|
276
341
|
```json
|
|
277
342
|
// 400 Bad Request
|
|
278
343
|
{
|
|
279
|
-
"statusCode": 400,
|
|
280
344
|
"message": "Invalid request body",
|
|
281
|
-
"
|
|
282
|
-
"
|
|
345
|
+
"statusCode": 400,
|
|
346
|
+
"normalized": { "text": "Invalid request body", "code": "core.system_error", "args": {} },
|
|
347
|
+
"requestId": "abc123",
|
|
348
|
+
"details": { "url": "http://localhost:3000/users", "path": "/users" }
|
|
283
349
|
}
|
|
284
350
|
|
|
285
351
|
// 404 Not Found
|
|
286
352
|
// Extra keys passed to getError(...) (e.g. `details`) surface under `extra`;
|
|
287
353
|
// the top-level `details` object is reserved for middleware context (url, path, stack, cause).
|
|
288
354
|
{
|
|
289
|
-
"statusCode": 404,
|
|
290
355
|
"message": "User not found",
|
|
291
|
-
"
|
|
356
|
+
"statusCode": 404,
|
|
357
|
+
"normalized": { "text": "User not found", "code": "core.system_error", "args": {} },
|
|
292
358
|
"requestId": "abc123",
|
|
293
|
-
"extra": { "details": { "id": "user-uuid" } }
|
|
359
|
+
"extra": { "details": { "id": "user-uuid" } },
|
|
360
|
+
"details": { "url": "http://localhost:3000/users/user-uuid", "path": "/users/:id" }
|
|
294
361
|
}
|
|
295
362
|
|
|
296
363
|
// 422 Validation Error
|
|
297
|
-
// `message`/`
|
|
298
|
-
// one, otherwise the raw Zod code (e.g. `invalid_type`, `too_small`).
|
|
364
|
+
// `message`/`normalized.code` come from the first failing issue - its `params.code` if the schema
|
|
365
|
+
// set one, otherwise the raw Zod code (e.g. `invalid_type`, `too_small`). `normalized.args` is
|
|
366
|
+
// always empty for a Zod issue; the full list of issues stays in `details.cause`.
|
|
299
367
|
{
|
|
300
|
-
"statusCode": 422,
|
|
301
368
|
"message": "Invalid email format",
|
|
302
|
-
"
|
|
369
|
+
"statusCode": 422,
|
|
370
|
+
"normalized": { "text": "Invalid email format", "code": "user.email.invalid", "args": {} },
|
|
303
371
|
"requestId": "abc123",
|
|
304
372
|
"details": {
|
|
373
|
+
"url": "http://localhost:3000/users",
|
|
374
|
+
"path": "/users",
|
|
305
375
|
"cause": [
|
|
306
376
|
{
|
|
307
377
|
"path": "email",
|
|
@@ -314,10 +384,11 @@ interface ErrorResponse {
|
|
|
314
384
|
|
|
315
385
|
// 500 Internal Error (production)
|
|
316
386
|
{
|
|
317
|
-
"statusCode": 500,
|
|
318
387
|
"message": "Internal server error",
|
|
319
|
-
"
|
|
320
|
-
"
|
|
388
|
+
"statusCode": 500,
|
|
389
|
+
"normalized": { "text": "Internal server error", "code": "core.system_error", "args": {} },
|
|
390
|
+
"requestId": "abc123",
|
|
391
|
+
"details": { "url": "http://localhost:3000/orders", "path": "/orders" }
|
|
321
392
|
}
|
|
322
393
|
```
|
|
323
394
|
|
|
@@ -325,27 +396,27 @@ interface ErrorResponse {
|
|
|
325
396
|
|
|
326
397
|
### `%s`, Never `%j`, for an `Error`
|
|
327
398
|
|
|
328
|
-
`message` and `stack` are non-enumerable properties on a native `Error` (and on `ApplicationError`, which extends it). `%j` serializes via `JSON.stringify`, which only visits enumerable own properties - so `logger.error('... | error: %j', error)` silently drops both
|
|
399
|
+
`message` and `stack` are non-enumerable properties on a native `Error` (and on `ApplicationError`, which extends it). `%j` serializes via `JSON.stringify`, which only visits enumerable own properties - so `logger.error('... | error: %j', error)` silently drops both. Always use `%s` to log an `Error` instance; reserve `%j`/`%o` for plain data objects.
|
|
329
400
|
|
|
330
401
|
```typescript
|
|
331
402
|
// ✅ Good - %s prints message + stack
|
|
332
403
|
this.logger.error('[createOrder] Failed | error: %s', error);
|
|
333
404
|
|
|
334
|
-
// ❌ Bad - %j drops
|
|
405
|
+
// ❌ Bad - %j drops the stack
|
|
335
406
|
this.logger.error('[createOrder] Failed | error: %j', error);
|
|
336
407
|
```
|
|
337
408
|
|
|
409
|
+
> [!NOTE]
|
|
410
|
+
> An `ApplicationError` logged with `%j` does show its message text, because the text rides inside the enumerable `normalized.text`. That is incidental, not a reprieve: the **stack** is still gone, which is the reason the rule exists. A plain `Error` under `%j` still logs little more than `{}`.
|
|
411
|
+
|
|
338
412
|
### What to Log
|
|
339
413
|
|
|
340
414
|
```typescript
|
|
341
|
-
// ✅ Good - Context for debugging
|
|
415
|
+
// ✅ Good - Context for debugging. `%s` on the error itself carries message + stack
|
|
342
416
|
this.logger.error('[createOrder] Failed | userId: %s | orderId: %s | error: %s',
|
|
343
|
-
userId, orderId, error
|
|
344
|
-
|
|
345
|
-
// ✅ Good - Include stack trace for unexpected errors
|
|
346
|
-
this.logger.error('[createOrder] Unexpected error | %s', error.stack);
|
|
417
|
+
userId, orderId, error);
|
|
347
418
|
|
|
348
|
-
// ❌ Bad - No context
|
|
419
|
+
// ❌ Bad - No context, and `.message` throws the stack away
|
|
349
420
|
this.logger.error(error.message);
|
|
350
421
|
|
|
351
422
|
// ❌ Bad - Sensitive data
|
|
@@ -354,12 +425,15 @@ this.logger.error('Login failed for user | password: %s', password);
|
|
|
354
425
|
|
|
355
426
|
### Log Levels
|
|
356
427
|
|
|
428
|
+
`ILogger` has exactly five levels, each a direct method. `alert`, `http`, `verbose` and `silly` do not exist.
|
|
429
|
+
|
|
357
430
|
| Level | Use For |
|
|
358
431
|
|-------|---------|
|
|
432
|
+
| `emerg` | The process cannot continue - unrecoverable |
|
|
359
433
|
| `error` | Exceptions that need attention |
|
|
360
434
|
| `warn` | Recoverable issues, deprecation warnings |
|
|
361
435
|
| `info` | Important business events |
|
|
362
|
-
| `debug` | Detailed debugging information |
|
|
436
|
+
| `debug` | Detailed debugging information (gated on `DEBUG`) |
|
|
363
437
|
|
|
364
438
|
```typescript
|
|
365
439
|
// Error - requires attention
|
|
@@ -381,30 +455,29 @@ this.logger.debug('[query] Executing | sql: %s | params: %j', sql, params);
|
|
|
381
455
|
|
|
382
456
|
```typescript
|
|
383
457
|
// ✅ Good - Errors propagate naturally with async/await
|
|
384
|
-
async
|
|
458
|
+
const processOrder = async (orderId: string) => {
|
|
385
459
|
const order = await orderRepository.findById({ id: orderId }); // Throws if fails
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
460
|
+
return paymentService.charge(order); // Throws if fails
|
|
461
|
+
};
|
|
389
462
|
|
|
390
463
|
// ✅ Good - Explicit catch when you need to handle
|
|
391
|
-
async
|
|
464
|
+
const processOrderWithFallback = async (order: TOrder) => {
|
|
392
465
|
try {
|
|
393
466
|
return await paymentService.charge(order);
|
|
394
467
|
} catch (error) {
|
|
395
|
-
this.logger.warn('[processOrder] Primary payment failed, trying backup');
|
|
396
|
-
return
|
|
468
|
+
this.logger.warn('[processOrder] Primary payment failed, trying backup | error: %s', error);
|
|
469
|
+
return backupPaymentService.charge(order);
|
|
397
470
|
}
|
|
398
|
-
}
|
|
471
|
+
};
|
|
399
472
|
|
|
400
473
|
// ❌ Bad - Swallowing errors
|
|
401
|
-
async
|
|
474
|
+
const processOrderSilently = async () => {
|
|
402
475
|
try {
|
|
403
476
|
await dangerousOperation();
|
|
404
477
|
} catch (error) {
|
|
405
478
|
// Error is swallowed - no one knows it happened!
|
|
406
479
|
}
|
|
407
|
-
}
|
|
480
|
+
};
|
|
408
481
|
```
|
|
409
482
|
|
|
410
483
|
### Fire-and-Forget with Error Handling
|
|
@@ -412,7 +485,7 @@ async function processOrder(orderId: string) {
|
|
|
412
485
|
```typescript
|
|
413
486
|
// ✅ Good - Log errors from fire-and-forget operations
|
|
414
487
|
this.sendNotification(userId).catch(error => {
|
|
415
|
-
this.logger.error('[notify] Failed | userId: %s | error: %s', userId, error
|
|
488
|
+
this.logger.error('[notify] Failed | userId: %s | error: %s', userId, error);
|
|
416
489
|
});
|
|
417
490
|
|
|
418
491
|
// ✅ Good - Use void to indicate intentional fire-and-forget
|
|
@@ -425,35 +498,44 @@ this.sendNotification(userId); // If this rejects, crash!
|
|
|
425
498
|
## 8. Transaction Error Handling
|
|
426
499
|
|
|
427
500
|
```typescript
|
|
428
|
-
|
|
429
|
-
|
|
501
|
+
import { getError, HTTP } from '@venizia/ignis-helpers';
|
|
502
|
+
|
|
503
|
+
const transferFunds = async (opts: { from: string; to: string; amount: number }) => {
|
|
504
|
+
const { from, to, amount } = opts;
|
|
505
|
+
const transaction = await accountRepository.beginTransaction();
|
|
430
506
|
|
|
431
507
|
try {
|
|
432
|
-
await accountRepository.debit({ id: from, amount, options: { transaction
|
|
433
|
-
await accountRepository.credit({ id: to, amount, options: { transaction
|
|
508
|
+
await accountRepository.debit({ id: from, amount, options: { transaction } });
|
|
509
|
+
await accountRepository.credit({ id: to, amount, options: { transaction } });
|
|
434
510
|
|
|
435
|
-
await
|
|
511
|
+
await transaction.commit();
|
|
436
512
|
return { success: true };
|
|
437
513
|
} catch (error) {
|
|
438
|
-
await
|
|
514
|
+
await transaction.rollback();
|
|
439
515
|
|
|
440
516
|
// Re-throw with context
|
|
441
517
|
throw getError({
|
|
442
518
|
statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
|
|
443
519
|
message: '[transferFunds] Transaction failed',
|
|
444
|
-
|
|
520
|
+
cause: error,
|
|
521
|
+
extra: { from, to, amount },
|
|
445
522
|
});
|
|
446
523
|
}
|
|
447
|
-
}
|
|
524
|
+
};
|
|
448
525
|
```
|
|
449
526
|
|
|
527
|
+
> [!NOTE]
|
|
528
|
+
> `rollback()` throws on failure, so it belongs in the `catch` exactly as above - never after a
|
|
529
|
+
> `commit()` you already awaited outside one. A rollback that follows a **failed** commit is a
|
|
530
|
+
> deliberate no-op: the transaction is already torn down, so this canonical shape stays safe.
|
|
531
|
+
|
|
450
532
|
## 9. Client-Side Error Handling
|
|
451
533
|
|
|
452
534
|
Guide for API consumers:
|
|
453
535
|
|
|
454
536
|
```typescript
|
|
455
537
|
// TypeScript client example
|
|
456
|
-
async
|
|
538
|
+
const createUser = async (data: TCreateUserRequest): Promise<TUser> => {
|
|
457
539
|
const response = await fetch('/api/users', {
|
|
458
540
|
method: 'POST',
|
|
459
541
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -463,31 +545,38 @@ async function createUser(data: CreateUserRequest): Promise<User> {
|
|
|
463
545
|
if (!response.ok) {
|
|
464
546
|
const error = await response.json();
|
|
465
547
|
|
|
548
|
+
// Branch on `normalized.code` - it is always present. There is no top-level `messageCode`.
|
|
466
549
|
switch (response.status) {
|
|
467
|
-
case 400:
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
550
|
+
case 400: {
|
|
551
|
+
// Context passed to getError(...) arrives under `extra`, not `details`
|
|
552
|
+
throw new ValidationError(error.message, error.extra);
|
|
553
|
+
}
|
|
554
|
+
case 401: {
|
|
471
555
|
window.location.href = '/login';
|
|
472
556
|
throw new AuthError('Please log in');
|
|
473
|
-
|
|
557
|
+
}
|
|
558
|
+
case 404: {
|
|
474
559
|
throw new NotFoundError(error.message);
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
560
|
+
}
|
|
561
|
+
case 422: {
|
|
562
|
+
// Field-level issues live in details.cause
|
|
563
|
+
const fieldErrors = error.details?.cause?.reduce((accumulator, issue) => {
|
|
564
|
+
accumulator[issue.path] = issue.message;
|
|
565
|
+
return accumulator;
|
|
480
566
|
}, {});
|
|
481
|
-
throw new ValidationError(
|
|
482
|
-
|
|
567
|
+
throw new ValidationError(error.normalized.code, fieldErrors);
|
|
568
|
+
}
|
|
569
|
+
case 429: {
|
|
483
570
|
throw new RateLimitError('Too many requests. Try again later.');
|
|
484
|
-
|
|
571
|
+
}
|
|
572
|
+
default: {
|
|
485
573
|
throw new ApiError(error.message || 'Something went wrong');
|
|
574
|
+
}
|
|
486
575
|
}
|
|
487
576
|
}
|
|
488
577
|
|
|
489
578
|
return response.json();
|
|
490
|
-
}
|
|
579
|
+
};
|
|
491
580
|
```
|
|
492
581
|
|
|
493
582
|
## Error Handling Checklist
|
|
@@ -495,7 +584,7 @@ async function createUser(data: CreateUserRequest): Promise<User> {
|
|
|
495
584
|
| Category | Check |
|
|
496
585
|
|----------|-------|
|
|
497
586
|
| **Services** | Business rule violations throw appropriate errors |
|
|
498
|
-
| **Repositories** |
|
|
587
|
+
| **Repositories** | Constraint errors left to the global handler; only custom messages wrapped |
|
|
499
588
|
| **Controllers** | Errors propagate to global handler |
|
|
500
589
|
| **Async** | All promises have error handling |
|
|
501
590
|
| **Transactions** | Always rollback on error |
|
|
@@ -102,6 +102,9 @@ Controller → Service → Repository → DataSource
|
|
|
102
102
|
// ✅ Error Handling
|
|
103
103
|
throw getError({ statusCode: 404, message: 'User not found' });
|
|
104
104
|
|
|
105
|
+
// ✅ Scoped Logging (ILogger - never names a provider)
|
|
106
|
+
this.logger.for('createUser').info('User created | id: %s', user.id);
|
|
107
|
+
|
|
105
108
|
// ✅ Input Validation
|
|
106
109
|
request: { body: jsonContent({ schema: z.object({ email: z.string().email() }) }) }
|
|
107
110
|
```
|
|
@@ -120,6 +123,9 @@ async getUser(c: Context) {
|
|
|
120
123
|
// ❌ Catching all errors silently
|
|
121
124
|
try { await riskyOperation(); } catch (e) { /* swallowed */ }
|
|
122
125
|
|
|
126
|
+
// ❌ Raw Error - loses statusCode, messageCode and the normalized response shape
|
|
127
|
+
throw new Error('User not found'); // Use getError()
|
|
128
|
+
|
|
123
129
|
// ❌ Using `any` type
|
|
124
130
|
const data: any = await fetchData(); // Use proper types!
|
|
125
131
|
```
|
|
@@ -9,6 +9,7 @@ Identify bottlenecks before optimizing:
|
|
|
9
9
|
```typescript
|
|
10
10
|
import { executeWithPerformanceMeasure } from '@venizia/ignis-helpers';
|
|
11
11
|
|
|
12
|
+
// `logger` is optional and typed `ILogger` - it falls back to `console` when omitted.
|
|
12
13
|
await executeWithPerformanceMeasure({
|
|
13
14
|
logger: this.logger,
|
|
14
15
|
scope: 'DataProcessing',
|
|
@@ -148,13 +149,22 @@ if (!cached) {
|
|
|
148
149
|
|
|
149
150
|
| Setting | Value | Why |
|
|
150
151
|
|---------|-------|-----|
|
|
151
|
-
| `NODE_ENV` | `production` | Enables library optimizations |
|
|
152
|
-
|
|
|
153
|
-
|
|
|
152
|
+
| `NODE_ENV` | `production` | Enables library optimizations; also gates error-detail leakage (unset is treated as production) |
|
|
153
|
+
| `APP_ENV_LOGGER_LEVEL` | `info` or `warn` | Keep hot-path logging off the critical path |
|
|
154
|
+
| Process Manager | systemd, Docker, Kubernetes | Auto-restart, supervision |
|
|
155
|
+
| Horizontal Scaling | One process per CPU core | Utilize all CPUs |
|
|
156
|
+
|
|
157
|
+
Bun has no PM2-style cluster mode, and `Bun.serve` is started without `reusePort` - two processes cannot share a port. Scale out with one process per port behind a reverse proxy, or with container replicas:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Each replica binds its own port; nginx / a load balancer fans out across them
|
|
161
|
+
APP_ENV_SERVER_PORT=3000 NODE_ENV=production bun run dist/index.js &
|
|
162
|
+
APP_ENV_SERVER_PORT=3001 NODE_ENV=production bun run dist/index.js &
|
|
163
|
+
```
|
|
154
164
|
|
|
155
|
-
**PM2 Cluster Mode:**
|
|
156
165
|
```bash
|
|
157
|
-
|
|
166
|
+
# Or let the orchestrator do it
|
|
167
|
+
docker compose up -d --scale app=4
|
|
158
168
|
```
|
|
159
169
|
|
|
160
170
|
## 6. Transaction Support
|
|
@@ -208,12 +218,12 @@ Connection pooling significantly improves performance by reusing database connec
|
|
|
208
218
|
```typescript
|
|
209
219
|
import { Pool } from 'pg';
|
|
210
220
|
import { datasource } from '@venizia/ignis';
|
|
211
|
-
import {
|
|
221
|
+
import { BaseRelationalDataSource } from '@venizia/ignis/postgres';
|
|
212
222
|
import { NodePostgresDriver } from '@venizia/ignis/postgres/node-postgres';
|
|
213
223
|
|
|
214
224
|
// IDataSourceConfigs: your settings interface (host/port/user/password/database)
|
|
215
225
|
@datasource({ driver: NodePostgresDriver })
|
|
216
|
-
export class PostgresDataSource extends
|
|
226
|
+
export class PostgresDataSource extends BaseRelationalDataSource<IDataSourceConfigs> {
|
|
217
227
|
override configure(): void {
|
|
218
228
|
// Keep the pool on `this.client` - NodePostgresDriver above wires the driver and Drizzle
|
|
219
229
|
// connector from it lazily, on first getConnector()/beginTransaction()
|
|
@@ -385,9 +395,9 @@ const logger = HfLogger.get('OrderEngine');
|
|
|
385
395
|
const MSG_ORDER_SENT = HfLogger.encodeMessage('Order sent');
|
|
386
396
|
const MSG_ORDER_FILLED = HfLogger.encodeMessage('Order filled');
|
|
387
397
|
|
|
388
|
-
// Start background flusher
|
|
398
|
+
// Start background flusher (writes to stdout by default; pass { filePath } or { sink })
|
|
389
399
|
const flusher = new HfLogFlusher();
|
|
390
|
-
flusher.start(100); // Flush
|
|
400
|
+
flusher.start(100); // Flush interval in ms - 100 is also the default
|
|
391
401
|
|
|
392
402
|
// In hot path (~100-300ns, zero allocation):
|
|
393
403
|
logger.log('info', MSG_ORDER_SENT);
|
|
@@ -395,11 +405,14 @@ logger.log('info', MSG_ORDER_FILLED);
|
|
|
395
405
|
```
|
|
396
406
|
|
|
397
407
|
**Key points:**
|
|
408
|
+
- `log(level, bytes)` is the zero-allocation path. Passing a string, or any `...args`, falls back to formatting
|
|
409
|
+
- Levels are the same five as `ILogger`: `debug`, `info`, `warn`, `error`, `emerg`
|
|
398
410
|
- Pre-encode messages at initialization, not in hot path
|
|
399
|
-
-
|
|
400
|
-
-
|
|
411
|
+
- HfLogger uses a lock-free ring buffer (64K entries x 256 bytes = 16MB), allocated lazily on first use
|
|
412
|
+
- Scope truncates at 32 UTF-8 bytes, message at 213 - silently
|
|
413
|
+
- For the standard logger, narrow output with `APP_ENV_LOGGER_LEVEL` in production and keep debug lines behind the `DEBUG` gate (pre-computed at module load, near-zero cost when off)
|
|
401
414
|
|
|
402
|
-
> **Deep Dive:** See [
|
|
415
|
+
> **Deep Dive:** See [HfLogger](../extensions/helpers/logger/hf-logger.md) for the complete API.
|
|
403
416
|
|
|
404
417
|
## Performance Checklist
|
|
405
418
|
|
|
@@ -414,7 +427,7 @@ logger.log('info', MSG_ORDER_FILLED);
|
|
|
414
427
|
| **Memory** | Large datasets processed in batches | High |
|
|
415
428
|
| **Caching** | Expensive queries cached | High |
|
|
416
429
|
| **Workers** | CPU-intensive tasks offloaded | High |
|
|
417
|
-
| **Logging** | HfLogger for hot paths (HFT) | High |
|
|
430
|
+
| **Logging** | HfLogger for hot paths (HFT); `APP_ENV_LOGGER_LEVEL` in production | High |
|
|
418
431
|
| **Monitoring** | Performance logging enabled | Low |
|
|
419
432
|
|
|
420
433
|
## See Also
|