@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.
Files changed (142) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +22 -11
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +6 -2
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +182 -93
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +107 -322
  26. package/content/extensions/components/authentication/api.md +454 -603
  27. package/content/extensions/components/authentication/errors.md +121 -498
  28. package/content/extensions/components/authentication/index.md +88 -801
  29. package/content/extensions/components/authentication/usage.md +207 -956
  30. package/content/extensions/components/authorization/api.md +736 -656
  31. package/content/extensions/components/authorization/errors.md +168 -206
  32. package/content/extensions/components/authorization/index.md +82 -797
  33. package/content/extensions/components/authorization/usage.md +194 -527
  34. package/content/extensions/components/health-check.md +71 -243
  35. package/content/extensions/components/mail/api.md +504 -287
  36. package/content/extensions/components/mail/errors.md +73 -61
  37. package/content/extensions/components/mail/index.md +96 -467
  38. package/content/extensions/components/mail/usage.md +130 -172
  39. package/content/extensions/components/request-tracker.md +66 -173
  40. package/content/extensions/components/socket-io/api.md +195 -13
  41. package/content/extensions/components/socket-io/errors.md +3 -3
  42. package/content/extensions/components/socket-io/index.md +50 -337
  43. package/content/extensions/components/socket-io/usage.md +143 -26
  44. package/content/extensions/components/static-asset/api.md +410 -142
  45. package/content/extensions/components/static-asset/errors.md +110 -53
  46. package/content/extensions/components/static-asset/index.md +79 -608
  47. package/content/extensions/components/static-asset/usage.md +180 -300
  48. package/content/extensions/components/websocket/api.md +275 -399
  49. package/content/extensions/components/websocket/errors.md +47 -56
  50. package/content/extensions/components/websocket/index.md +74 -407
  51. package/content/extensions/components/websocket/usage.md +110 -341
  52. package/content/extensions/helpers/cron/index.md +51 -160
  53. package/content/extensions/helpers/crypto/index.md +62 -483
  54. package/content/extensions/helpers/crypto/reference.md +456 -0
  55. package/content/extensions/helpers/env/index.md +60 -178
  56. package/content/extensions/helpers/error/index.md +221 -207
  57. package/content/extensions/helpers/inversion/index.md +65 -556
  58. package/content/extensions/helpers/inversion/reference.md +522 -0
  59. package/content/extensions/helpers/kafka/admin.md +20 -1
  60. package/content/extensions/helpers/kafka/compile-binary.md +41 -35
  61. package/content/extensions/helpers/kafka/consumer.md +54 -20
  62. package/content/extensions/helpers/kafka/examples.md +21 -16
  63. package/content/extensions/helpers/kafka/index.md +80 -610
  64. package/content/extensions/helpers/kafka/producer.md +134 -5
  65. package/content/extensions/helpers/kafka/schema-registry.md +45 -70
  66. package/content/extensions/helpers/logger/hf-logger.md +193 -0
  67. package/content/extensions/helpers/logger/index.md +64 -563
  68. package/content/extensions/helpers/logger/pino.md +85 -0
  69. package/content/extensions/helpers/logger/reference.md +746 -0
  70. package/content/extensions/helpers/network/api.md +241 -195
  71. package/content/extensions/helpers/network/index.md +72 -530
  72. package/content/extensions/helpers/queue/index.md +72 -900
  73. package/content/extensions/helpers/queue/reference.md +467 -0
  74. package/content/extensions/helpers/redis/index.md +73 -645
  75. package/content/extensions/helpers/redis/reference.md +727 -0
  76. package/content/extensions/helpers/secrets/index.md +66 -0
  77. package/content/extensions/helpers/socket-io/api.md +305 -203
  78. package/content/extensions/helpers/socket-io/index.md +66 -432
  79. package/content/extensions/helpers/storage/api.md +564 -462
  80. package/content/extensions/helpers/storage/index.md +77 -573
  81. package/content/extensions/helpers/types/index.md +66 -499
  82. package/content/extensions/helpers/types/reference.md +650 -0
  83. package/content/extensions/helpers/uid/index.md +58 -227
  84. package/content/extensions/helpers/websocket/api.md +329 -216
  85. package/content/extensions/helpers/websocket/index.md +65 -503
  86. package/content/extensions/helpers/worker-thread/index.md +58 -396
  87. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  88. package/content/guides/core-concepts/persistent/models.md +1 -1
  89. package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
  90. package/content/guides/core-concepts/persistent/transactions.md +1 -1
  91. package/content/guides/core-concepts/secrets-vault.md +177 -0
  92. package/content/guides/core-concepts/services.md +1 -1
  93. package/content/guides/migrations/redis-helpers-migration.md +1 -1
  94. package/content/guides/migrations/unified-connectors-migration.md +2 -2
  95. package/content/guides/tutorials/ecommerce-api.md +3 -8
  96. package/content/references/base/application.md +1 -1
  97. package/content/references/base/connectors.md +79 -136
  98. package/content/references/base/datasources-reference.md +599 -0
  99. package/content/references/base/datasources.md +84 -444
  100. package/content/references/base/dependency-injection.md +17 -39
  101. package/content/references/base/filter-system/application-usage.md +69 -121
  102. package/content/references/base/filter-system/array-operators.md +12 -0
  103. package/content/references/base/filter-system/comparison-operators.md +12 -0
  104. package/content/references/base/filter-system/default-filter.md +136 -348
  105. package/content/references/base/filter-system/fields-order-pagination.md +38 -16
  106. package/content/references/base/filter-system/index.md +106 -257
  107. package/content/references/base/filter-system/json-filtering.md +12 -2
  108. package/content/references/base/filter-system/list-operators.md +16 -2
  109. package/content/references/base/filter-system/logical-operators.md +13 -0
  110. package/content/references/base/filter-system/null-operators.md +13 -0
  111. package/content/references/base/filter-system/pattern-matching.md +12 -0
  112. package/content/references/base/filter-system/quick-reference.md +11 -2
  113. package/content/references/base/filter-system/range-operators.md +12 -0
  114. package/content/references/base/filter-system/tips.md +70 -133
  115. package/content/references/base/filter-system/use-cases.md +156 -233
  116. package/content/references/base/middlewares.md +35 -21
  117. package/content/references/base/models-reference.md +886 -0
  118. package/content/references/base/models.md +80 -1452
  119. package/content/references/base/repositories/advanced.md +156 -192
  120. package/content/references/base/repositories/index.md +77 -650
  121. package/content/references/base/repositories/mixins.md +22 -18
  122. package/content/references/base/repositories/relations.md +123 -171
  123. package/content/references/base/repositories/soft-deletable.md +58 -56
  124. package/content/references/base/secrets.md +263 -0
  125. package/content/references/base/services.md +2 -2
  126. package/content/references/configuration/environment-variables.md +48 -4
  127. package/content/references/configuration/index.md +49 -31
  128. package/content/references/quick-reference.md +3 -16
  129. package/content/references/utilities/crypto.md +35 -76
  130. package/content/references/utilities/date.md +33 -73
  131. package/content/references/utilities/index.md +1 -1
  132. package/content/references/utilities/jsx-reference.md +298 -0
  133. package/content/references/utilities/jsx.md +82 -525
  134. package/content/references/utilities/module.md +29 -62
  135. package/content/references/utilities/parse.md +34 -64
  136. package/content/references/utilities/performance.md +33 -58
  137. package/content/references/utilities/promise.md +28 -62
  138. package/content/references/utilities/request.md +57 -218
  139. package/content/references/utilities/schema.md +43 -137
  140. package/content/references/utilities/statuses-reference.md +361 -0
  141. package/content/references/utilities/statuses.md +63 -667
  142. package/package.json +8 -8
@@ -1,27 +1,14 @@
1
- # Error
2
-
3
- Standardized error class and factory for throwing HTTP-aware errors with machine-readable codes across the application.
4
-
5
- ## Quick Reference
1
+ ---
2
+ title: Error
3
+ description: ApplicationError, the getError factory, and a catalog pattern for machine-readable error codes
4
+ difficulty: beginner
5
+ ---
6
6
 
7
- | Item | Value |
8
- |------|-------|
9
- | **Package** | `@venizia/ignis-helpers` |
10
- | **Class** | `ApplicationError` |
11
- | **Extends** | `Error` (native) |
12
- | **Runtimes** | Both |
13
-
14
- #### Import Paths
15
-
16
- ```typescript
17
- import { ApplicationError, getError, isApplicationError } from '@venizia/ignis-helpers';
18
- import { ErrorSchema } from '@venizia/ignis-helpers';
19
- import type { TError } from '@venizia/ignis-helpers';
20
- ```
7
+ # Error
21
8
 
22
- ## Creating an Instance
9
+ `getError()` builds an `ApplicationError` carrying an HTTP status and a machine-readable code - the house rule is `getError`, never `new Error`.
23
10
 
24
- The canonical way to raise an error in IGNIS is the standalone `getError()` factory - use it everywhere (house rule: `getError`, never `new Error`). `ApplicationError` extends the native `Error` class with an HTTP `statusCode` and an optional `messageCode` for machine-readable error identification; `getError()` constructs one for you.
11
+ ## In one example
25
12
 
26
13
  ```typescript
27
14
  import { getError, HTTP } from '@venizia/ignis-helpers';
@@ -33,251 +20,278 @@ throw getError({
33
20
  });
34
21
  ```
35
22
 
36
- #### Options (`TError`)
37
-
38
- The same options apply to `getError()`, the `ApplicationError` constructor, and the static factory:
39
-
40
- | Option | Type | Default | Description |
41
- |--------|------|---------|-------------|
42
- | `message` | `string` | -- (required) | Human-readable error message |
43
- | `statusCode` | `number` | `400` | HTTP status code |
44
- | `messageCode` | `string` | `MessageCode.DEFAULT` (`'core.system_error'`) | Machine-readable error code for client-side handling. Always resolved through `MessageCode.resolve()` -- never left `undefined`, and lower-cased regardless of what was passed in |
45
- | `name` | `string` | `undefined` | Accepted by the schema but discarded by the constructor (the native `Error` name is kept) |
23
+ The framework's `AppErrorMiddleware` catches `ApplicationError` instances and formats them into a consistent JSON response - see [Common tasks](#common-tasks) below.
24
+
25
+ ## How it works
26
+
27
+ - **Three equivalent entry points.** `getError(opts)`, `new ApplicationError(opts)`, and the static `ApplicationError.getError(opts)` all take the same input and build the same object - use the class form only when a direct reference reads better.
28
+ - **Two input shapes.** Free-form (`{ message, statusCode?, messageCode? }`) covers one-off failures - most throw sites. Catalogued (`{ error: TErrorDefinition }`) raises a failure declared once at module scope, so its code, status, and default text cannot drift across the call sites that raise it.
29
+ - **One message shape everywhere.** `message` is either the historical string (with sibling `messageCode?`/`messageArgs?`) or an object mirroring `normalized`: `{ text, code?, args? }`. Both resolve to the same `normalized`:
30
+
31
+ ```typescript
32
+ getError({ message: 'Only %{n} left', messageCode: 'stock.low', messageArgs: { n: 2 } }); // flat
33
+ getError({ message: { text: 'Only %{n} left', code: 'stock.low', args: { n: 2 } } }); // nested
34
+ getError({ error: StockErrors.LOW, messageArgs: { n: 2 } }); // catalogued
35
+ ```
36
+
37
+ On the catalogued form, `message` is a **partial** override - `{ message: { args } }` amends just the args and keeps the definition's `text`/`code`.
38
+ - **Precedence, most specific first.** `code`: `message.code` -> the definition's `message.code` -> `messageCode`. `args`: `message.args` -> `messageArgs` -> the definition's `message.args`.
39
+ - **`messageCode` always resolves to something.** `MessageCode.resolve()` lower-cases it and falls back to `MessageCode.DEFAULT` (`'core.system_error'`) when none is given or it is empty - `error.normalized.code` is never `undefined`.
40
+ - **`normalized` is the single source, and there is no flat duplicate.** `{ text, code, args }` - `text` defaults to `message`, `code`/`args` resolve per the precedence above. `args` is always populated (`{}` when empty), so no consumer needs a null check. A client renders any error with one lookup: `translate(error.normalized.code, error.normalized.args)`. `messageCode` and `messageArgs` are INPUTS only: there is no `error.messageCode` field, and `extra` never mirrors `messageArgs`. Pass `transform` to build `normalized` yourself in place of the default.
41
+ - **Any key the input does not declare rides into `extra`.** Attach whatever context your clients need - `getError({ message, transaction: {...} })` lands at `error.extra.transaction`. Passing `extra` explicitly works too, and the two merge with the explicit one winning. `extra` carries caller context ONLY; it is `undefined` when there is nothing to carry.
42
+ - **`cause` reaches the native `Error.cause`,** not `extra` - wrap a lower-level failure with `getError({ message, cause: originalError })` and every tool that reads `.cause` sees it. `error` is **refused** on the free-form branch (`error?: never`) precisely so this mistake fails at compile time: `getError({ message, error: caughtError })` does not compile - use `cause`.
43
+ - **`isApplicationError()` checks shape, not class identity.** There is one `ApplicationError` - it lives in `@venizia/ignis-inversion` so a browser application can raise and read the same errors the server does, and `helpers` re-exports it. `instanceof` still fails across a package boundary: inversion ships dual CJS+ESM builds, so one source class has two runtime constructors. Test the shape.
44
+
45
+ **Options shared by both forms**
46
+
47
+ | Option | Type | Description |
48
+ | --------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
49
+ | `message` | `string \| { text, code?, args? }` | Required on the free-form branch; a **partial** override on the catalogued branch (definition supplies what's omitted) |
50
+ | `messageCode` | `string` | Free-form only, sibling to a string `message`. Lowest precedence - `message.code` and the definition's `message.code` both win over it |
51
+ | `statusCode` | `number` | Defaults to `400`, or the definition's `statusCode` for the catalogued form |
52
+ | `messageArgs` | `Record<string, unknown>` | Interpolation values. Reaches `normalized.args` - never `extra`. Lowest precedence, same rule as `messageCode` |
53
+ | `cause` | `unknown` | The wrapped failure - reaches `Error.cause`. Use this to wrap a caught error; `error` is refused on the free-form branch for exactly this case |
54
+ | `extra` | `Record<string, unknown>` | Explicit context, merged with any swept keys - and it wins on a clash |
55
+ | `transform` | `TErrorNormalizeTransformFn` | Builds `normalized` in place of the default. Receives `{ message: TErrorNormalized, statusCode, extra? }` - `message` is the default normalized being replaced, so `s => ({ ...s.message, text: 'x' })` amends one field |
56
+ | _anything else_ | `unknown` | Rides into `extra` under its own name. `getError({ message, transaction })` lands at `error.extra.transaction` |
46
57
 
47
58
  > [!TIP]
48
- > The `TError` type is derived from `ErrorSchema` (a Zod schema) and uses `.catchall(z.any())`, so you can pass additional arbitrary properties beyond the four listed above. Extra properties are collected into the `extra` field on the resulting `ApplicationError` instance.
59
+ > Spreading a definition now resolves identically to passing it as `error`: `getError({ ...CategoryErrors.CREATE_DUPLICATE_NAME })` and `getError({ error: CategoryErrors.CREATE_DUPLICATE_NAME })` build the same `ApplicationError`. A definition's `message` is `{ text, code, args? }` - the same shape the free-form input accepts - so the spread degrades to nothing. Prefer `{ error: DEF }` anyway; it reads as "raise this catalogued error" instead of "raise these loose fields."
49
60
 
50
- #### `ApplicationError` Constructor and Static Factory
61
+ ## Every shape and its output
51
62
 
52
- `getError()` is the canonical form. The class constructor and the static `ApplicationError.getError()` are legal equivalents - use them only when a direct class reference reads better:
63
+ Every row below is the real output, and the catalogued rows all use this definition:
53
64
 
54
65
  ```typescript
55
- import { ApplicationError, HTTP } from '@venizia/ignis-helpers';
56
-
57
- // Class constructor (equivalent to getError)
58
- const error = new ApplicationError({
59
- message: 'Configuration missing',
60
- statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
61
- });
62
-
63
- // Static factory (equivalent to getError)
64
- throw ApplicationError.getError({
65
- message: 'Invalid credentials',
66
- statusCode: HTTP.ResultCodes.RS_4.Unauthorized,
67
- messageCode: 'core.auth.invalid_credentials',
68
- });
66
+ const DEF = {
67
+ message: {
68
+ text: 'A category named "%{name}" already exists.',
69
+ code: 'server.commerce.category.duplicate',
70
+ args: { name: '?' },
71
+ },
72
+ statusCode: 409,
73
+ } as const satisfies TErrorDefinition;
69
74
  ```
70
75
 
71
- ### MessageCode
76
+ **Free-form, flat** - the historical shape, unchanged:
72
77
 
73
- `MessageCode` builds and normalizes the machine-readable codes carried on `ApplicationError.messageCode`. Every code an application throws should be constructed with `MessageCode.build()` rather than typed as a raw string literal -- a malformed code fails at module load (import time) instead of shipping dead into production.
78
+ | Input | `message` | `statusCode` | `normalized` |
79
+ |-------|-----------|--------------|--------------|
80
+ | `{ message: 'Broke' }` | `Broke` | `400` | `{ text: 'Broke', code: 'core.system_error', args: {} }` |
81
+ | `{ message: 'Broke', messageCode: 'a.b' }` | `Broke` | `400` | `{ text: 'Broke', code: 'a.b', args: {} }` |
82
+ | `{ message: 'Only %{n} left', messageArgs: { n: 2 } }` | `Only %{n} left` | `400` | `{ text: 'Only %{n} left', code: 'core.system_error', args: { n: 2 } }` |
83
+ | `{ message, messageCode: 'stock.low', messageArgs: { n: 2 }, statusCode: 409 }` | `Only %{n} left` | `409` | `{ text: 'Only %{n} left', code: 'stock.low', args: { n: 2 } }` |
74
84
 
75
- ```typescript
76
- import { MessageCode } from '@venizia/ignis-helpers';
85
+ **Free-form, object** - `text` is the only required field. Each row is identical to its flat twin above:
77
86
 
78
- export class UserErrorCodes {
79
- static readonly NOT_FOUND = MessageCode.build({ parts: ['core', 'user', 'not_found'] });
80
- static readonly DUPLICATE_EMAIL = MessageCode.build({ parts: ['core', 'user', 'duplicate_email'] });
81
- }
82
- ```
87
+ | Input | `normalized` |
88
+ |-------|--------------|
89
+ | `{ message: { text: 'Broke' } }` | `{ text: 'Broke', code: 'core.system_error', args: {} }` |
90
+ | `{ message: { text: 'Broke', code: 'a.b' } }` | `{ text: 'Broke', code: 'a.b', args: {} }` |
91
+ | `{ message: { text: 'Only %{n} left', code: 'stock.low', args: { n: 2 } } }` | `{ text: 'Only %{n} left', code: 'stock.low', args: { n: 2 } }` |
83
92
 
84
- | Member | Type | Value / Signature | Description |
85
- |--------|------|--------------------|-------------|
86
- | `DEFAULT` | `string` | `'core.system_error'` | The code `ApplicationError` falls back to when no `messageCode` is supplied |
87
- | `SEPARATOR` | `string` | `'.'` | Joins segments into a dotted code (e.g. `core.mail.send_failed`) |
88
- | `SEGMENT_PATTERN` | `RegExp` | `/^[a-z0-9]+(_[a-z0-9]+)*$/` | Each segment must be lower snake_case -- `a-z`, `0-9`, `_` |
89
- | `MIN_SEGMENTS` | `number` | `2` | A code needs at least a namespace and a reason (e.g. `core.not_found` is valid, `not_found` alone is not) |
90
- | `build(opts: { parts: Array<string> })` | `string` | -- | Joins `parts` with `SEPARATOR` and lower-cases the result. Throws (via `getError`) if `parts.length < MIN_SEGMENTS`, or if any segment fails `SEGMENT_PATTERN` |
91
- | `isValid(code: string)` | `boolean` | -- | Cheap structural check for a code arriving from outside the process |
92
- | `resolve(code?: string)` | `string` | -- | Normalizes an absent or empty code to `DEFAULT`; otherwise lower-cases `code` |
93
+ **Catalogued** - omit a field and the definition supplies it:
93
94
 
94
- > [!IMPORTANT]
95
- > `ApplicationError`'s constructor always calls `MessageCode.resolve(messageCode)` -- so `error.messageCode` is **never** `undefined`, and it is **always lower-cased** regardless of the casing passed to `getError()`. A comparison like `error.messageCode === 'DUPLICATE_EMAIL'` is always false; compare against `'duplicate_email'` (or the exact string returned by `MessageCode.build()`).
95
+ | Input | `message` | `statusCode` | `normalized.code` | `normalized.args` |
96
+ |-------|-----------|--------------|-------------------|-------------------|
97
+ | `{ error: DEF }` | the definition's text | `409` | `server.commerce.category.duplicate` | `{ name: '?' }` |
98
+ | `{ error: DEF, messageArgs: { name: 'Vé' } }` | the definition's text | `409` | `server.commerce.category.duplicate` | `{ name: 'Vé' }` |
99
+ | `{ error: DEF, message: { args: { name: 'Vé' } } }` | the definition's text | `409` | `server.commerce.category.duplicate` | `{ name: 'Vé' }` |
100
+ | `{ error: DEF, message: { text: 'Custom' } }` | `Custom` | `409` | `server.commerce.category.duplicate` | `{ name: '?' }` |
101
+ | `{ error: DEF, message: { code: 'override.code' } }` | the definition's text | `409` | `override.code` | `{ name: '?' }` |
102
+ | `{ error: DEF, message: 'Custom' }` | `Custom` | `409` | `server.commerce.category.duplicate` | `{ name: '?' }` |
103
+ | `{ error: DEF, statusCode: 410 }` | the definition's text | `410` | `server.commerce.category.duplicate` | `{ name: '?' }` |
104
+ | `{ ...DEF }` (spread) | the definition's text | `409` | `server.commerce.category.duplicate` | `{ name: '?' }` |
96
105
 
97
- ## Checking for an Application Error
106
+ **Context and cause:**
98
107
 
99
- Use `isApplicationError(error)` to recognize an application error by **shape** - an `Error` instance carrying a numeric `statusCode` - rather than by class identity:
108
+ | Input | Output |
109
+ |-------|--------|
110
+ | `{ message, extra: { categoryId: 42 } }` | `extra: { categoryId: 42 }` |
111
+ | `{ message, userId: 7, transaction: { id: 12 } }` | `extra: { userId: 7, transaction: { id: 12 } }` - swept |
112
+ | `{ message, userId: 7, extra: { userId: 9 } }` | `extra: { userId: 9 }` - explicit wins |
113
+ | `{ message, cause: err }` | `Error.cause = err`; `extra` stays `undefined` |
114
+ | `{ message, error: caughtError }` | **does not compile** - use `cause` |
100
115
 
101
- ```typescript
102
- import { isApplicationError } from '@venizia/ignis-helpers';
116
+ **Transform** - `message` in the snapshot IS the default being replaced:
103
117
 
104
- try {
105
- await someOperation();
106
- } catch (error) {
107
- if (isApplicationError(error)) {
108
- // already shaped (has a statusCode) - surface as-is
109
- throw error;
110
- }
111
- // an unknown failure - sanitize before rethrowing
112
- throw getError({ message: 'Operation failed', statusCode: HTTP.ResultCodes.RS_5.InternalServerError });
113
- }
118
+ ```typescript
119
+ getError({
120
+ message: { text: 'Only %{n} left', code: 'stock.low', args: { n: 2 } },
121
+ transform: snapshot => ({ ...snapshot.message, text: 'Chỉ còn 2 vé.' }),
122
+ });
123
+ // error.message -> 'Only %{n} left' (the raw text stays)
124
+ // error.normalized.text -> 'Chỉ còn 2 vé.'
125
+ // error.normalized.code -> 'stock.low'
126
+ // error.normalized.args -> { n: 2 }
114
127
  ```
115
128
 
116
- > [!WARNING]
117
- > Never compare `ApplicationError` with `instanceof` across a package boundary. `inversion` ships **dual CJS + ESM** builds (its DI powers frontend libraries), so its error class deliberately has more than one runtime identity, and `helpers` keeps its own `ApplicationError` for the backend stack. Two objects that are both "an application error" can be instances of different classes, so `instanceof` gives false negatives. `isApplicationError` checks the shape and works regardless of which package threw. The search connectors use it to decide what is already shaped versus what must be sanitized as a `503`.
118
-
119
- ## Usage
120
-
121
- ### Throwing Errors in Services
129
+ **Precedence,** most specific first - note a definition's `message.code` beats a flat `messageCode`:
122
130
 
123
- The most common pattern is throwing `ApplicationError` from service methods to signal HTTP-level failures. The framework's error handling middleware catches these and formats the response automatically.
131
+ | Resolves | Order |
132
+ |----------|-------|
133
+ | `normalized.code` | `message.code` -> the definition's `message.code` -> `messageCode` -> `MessageCode.DEFAULT` |
134
+ | `normalized.args` | `message.args` -> `messageArgs` -> the definition's `message.args` -> `{}` |
135
+ | `normalized.text` | `message.text` (or a string `message`) -> the definition's `message.text` -> `''` |
136
+ | `statusCode` | `statusCode` -> the definition's `statusCode` -> `400` |
124
137
 
125
- ```typescript
126
- import { getError, HTTP } from '@venizia/ignis-helpers';
127
-
128
- class AuthenticationService {
129
- async signUp(opts: { username: string; credential: string }) {
130
- const existingUser = await this.userRepository.findByUsername(opts.username);
131
- if (existingUser) {
132
- throw getError({
133
- statusCode: HTTP.ResultCodes.RS_4.Conflict,
134
- message: 'Username already exists',
135
- });
136
- }
137
- // ...
138
- }
139
- }
140
- ```
138
+ ## Common tasks
141
139
 
142
- ### Using `messageCode` for Client-Side Handling
140
+ ### Throw a free-form error
143
141
 
144
- The `messageCode` field allows frontend applications to map errors to localized messages or specific UI behaviors without parsing the human-readable `message` string.
142
+ The most common shape - a `message` and a status.
145
143
 
146
144
  ```typescript
147
145
  throw getError({
148
- message: 'Email verification required before login',
149
- statusCode: HTTP.ResultCodes.RS_4.Forbidden,
150
- messageCode: 'auth.email_not_verified',
146
+ statusCode: HTTP.ResultCodes.RS_4.Conflict,
147
+ message: 'Username already exists',
151
148
  });
152
149
  ```
153
150
 
154
- ### Error Response Format
151
+ ### Recognize an already-shaped error in a catch block
155
152
 
156
- The built-in `appErrorHandler` middleware (from `@venizia/ignis`) catches `ApplicationError` instances and formats them into consistent JSON responses. The response shape differs by environment.
157
-
158
- #### Sanitized (Production-Class) Response
159
-
160
- The handler is **fail-closed**: it exposes internals only when `NODE_ENV` names a development environment - one of `local`, `debug`, `development`, `dev`, `sit` (`Environment.DEVELOPMENT_ENVS`). Everything else is sanitized as production: `production`, and also `alpha`, `beta`, `uat`, `staging`, an unrecognized name, and an unset `NODE_ENV`.
161
-
162
- In a sanitized response, `stack` and `cause` are omitted from `details`. For unexpected errors without a `statusCode` (i.e., not thrown via `getError`), the raw message is replaced with a generic `"Internal Server Error"`, and a database error keeps only its base message - no SQL detail, no table or constraint name.
163
-
164
- ```json
165
- {
166
- "message": "User not found",
167
- "messageCode": "core.user.not_found",
168
- "statusCode": 404,
169
- "requestId": "abc-123-def",
170
- "details": {
171
- "url": "http://localhost:3000/api/users/123",
172
- "path": "/api/users/123"
173
- }
174
- }
175
- ```
176
-
177
- #### Development Response
178
-
179
- In a development environment (see the list above), `details` additionally includes debugging fields:
153
+ ```typescript
154
+ import { isApplicationError } from '@venizia/ignis-helpers';
180
155
 
181
- ```json
182
- {
183
- "message": "User not found",
184
- "messageCode": "core.user.not_found",
185
- "statusCode": 404,
186
- "requestId": "abc-123-def",
187
- "details": {
188
- "url": "http://localhost:3000/api/users/123",
189
- "path": "/api/users/123",
190
- "stack": "Error: User not found\n at ...",
191
- "cause": "..."
156
+ try {
157
+ await someOperation();
158
+ } catch (error) {
159
+ if (isApplicationError(error)) {
160
+ throw error; // already shaped - surface as-is
192
161
  }
162
+ throw getError({
163
+ message: 'Operation failed',
164
+ statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
165
+ });
193
166
  }
194
167
  ```
195
168
 
196
- ### ErrorSchema (Zod)
169
+ ### Catalog a reusable error
197
170
 
198
- `ErrorSchema` is a Zod object schema used for OpenAPI response documentation. It is typically referenced in route definitions to describe error responses.
171
+ Declare it once; every call site raises it by reference instead of retyping the code and status.
199
172
 
200
173
  ```typescript
201
- import { ErrorSchema, HTTP } from '@venizia/ignis-helpers';
202
-
203
- // In route definition responses
204
- const responses = {
205
- [HTTP.ResultCodes.RS_4.NotFound]: {
206
- description: 'Resource not found',
207
- content: {
208
- 'application/json': { schema: ErrorSchema },
174
+ import { ErrorScopes, getError, HTTP } from '@venizia/ignis-helpers';
175
+ import type { TErrorDefinition } from '@venizia/ignis-helpers';
176
+
177
+ const CategoryErrors = {
178
+ CREATE_DUPLICATE_NAME: {
179
+ message: {
180
+ text: 'A category named "%{name}" already exists.',
181
+ code: 'server.commerce.category.create.duplicate_name',
209
182
  },
183
+ statusCode: HTTP.ResultCodes.RS_4.Conflict,
184
+ category: ErrorScopes.VALIDATION,
210
185
  },
211
- };
186
+ } as const satisfies Record<string, TErrorDefinition>;
187
+
188
+ throw getError({ error: CategoryErrors.CREATE_DUPLICATE_NAME, messageArgs: { name: 'Vé' } });
189
+ // Equivalent, using the catalogued form's partial override instead:
190
+ throw getError({ error: CategoryErrors.CREATE_DUPLICATE_NAME, message: { args: { name: 'Vé' } } });
212
191
  ```
213
192
 
214
- The schema shape:
193
+ `ErrorScopes` groups a failure by intent - `AUTH`, `VALIDATION`, `BUSINESS`, `SYSTEM`, `INTEGRATION` - because `statusCode` cannot: a `409` is a business conflict in one place and a validation clash in another.
215
194
 
216
- ```typescript
217
- const ErrorSchema = z
218
- .object({
219
- name: z.string().optional(),
220
- statusCode: z.number().optional(),
221
- messageCode: z.string().optional(),
222
- message: z.string(),
223
- })
224
- .catchall(z.any());
225
- ```
195
+ > [!WARNING]
196
+ > `category` is catalog **metadata only** - it does not reach the error response. `getError` reads `message.text`, `message.code` and `statusCode` off a definition and ignores the rest. Use it to group and filter catalogs (ops dashboards, translator exports); do not expect a client to receive it.
226
197
 
227
- ### Common Status Code Patterns
198
+ ### Register catalog keys for `messageCode` autocomplete
228
199
 
229
- | Scenario | Status Code | `HTTP.ResultCodes` Path |
230
- |----------|-------------|-------------------------|
231
- | Invalid input / bad request | 400 | `RS_4.BadRequest` |
232
- | Missing or invalid auth | 401 | `RS_4.Unauthorized` |
233
- | Insufficient permissions | 403 | `RS_4.Forbidden` |
234
- | Resource not found | 404 | `RS_4.NotFound` |
235
- | Duplicate resource | 409 | `RS_4.Conflict` |
236
- | Validation error | 422 | `RS_4.UnprocessableEntity` |
237
- | Server failure | 500 | `RS_5.InternalServerError` |
200
+ Augment the module the file already imports from. `IErrorKeyRegistry` is declared in `@venizia/ignis-inversion` and re-exported by `helpers`; merging follows the re-export, so either name populates the same registry.
238
201
 
239
- ## Troubleshooting
202
+ ```typescript
203
+ import type { TRegisterErrors } from '@venizia/ignis-helpers';
240
204
 
241
- ### `statusCode` defaults to 400
205
+ declare module '@venizia/ignis-helpers' {
206
+ interface IErrorKeyRegistry extends TRegisterErrors<typeof CategoryErrors> {}
207
+ }
208
+ ```
209
+
210
+ > [!WARNING]
211
+ > TypeScript only treats `declare module` as an **augmentation** when the file imports that module. Name a module the file never imports and it silently becomes an inert ambient declaration - no error, no keys registered, autocomplete quietly empty.
242
212
 
243
- **Cause:** `getError()` was called without specifying a `statusCode`. The `ApplicationError` constructor defaults to `400` (Bad Request).
213
+ Declare `message.code` as a literal string, not through `MessageCode.build()` - `build()` returns `string`, which would widen the registry to `Record<string, true>` and destroy the autocomplete.
244
214
 
245
- **Fix:** Always provide an explicit status code using `HTTP.ResultCodes`:
215
+ ### Build a code outside a catalog
216
+
217
+ `MessageCode.build()` validates at import time instead of shipping a malformed code into production.
246
218
 
247
219
  ```typescript
248
- throw getError({
249
- message: 'Resource not found',
250
- statusCode: HTTP.ResultCodes.RS_4.NotFound,
251
- });
252
- ```
220
+ import { MessageCode } from '@venizia/ignis-helpers';
253
221
 
254
- ### Error response missing `stack` and `cause`
222
+ const NOT_FOUND = MessageCode.build({ parts: ['core', 'user', 'not_found'] });
223
+ // 'core.user.not_found' - throws if a segment isn't lower snake_case, or fewer than 2 parts
224
+ ```
255
225
 
256
- **Cause:** `NODE_ENV` is not one of the development environments, so the handler sanitized the response (`url` and `path` are always included). Note this is what happens on `alpha`/`staging`, on a typo'd name, and when `NODE_ENV` is unset - not only on `production`.
226
+ ### Read the error response shape
257
227
 
258
- **Fix:** Set `NODE_ENV` to a development name - `development`, `dev`, `local`, `debug` or `sit`. Anything else stays sanitized by design.
228
+ `AppErrorMiddleware` (from `@venizia/ignis`) routes every thrown value into one of five shapes. All five carry `message`, `statusCode`, `normalized` and `details`; only an intentional error can carry `extra`. The code lives at `normalized.code` - there is no flat `messageCode` on the response.
259
229
 
260
- ### Errors returning 500 instead of expected status code
230
+ | What was thrown | Status | `normalized.code` | `message` | `extra` |
231
+ | ----------------------------------------- | ------- | ------------------------------------------------------ | --------------------------------------------------------------------- | ----------------------------------------------------- |
232
+ | `ZodError` (validation) | 422 | the first issue's `params.code`, else its raw Zod code | that issue's message; per-field list in `details.cause` | never |
233
+ | DB client error (SQLSTATE class 22/23/44) | 400 | `core.system_error` | a fixed, safe summary - never the driver's text | never |
234
+ | Transient DB conflict (40001/40P01) | 409 | `database.conflict` | a fixed retry message | never |
235
+ | `getError(...)` - intentional | its own | its own | its own | when the throw site attached `extra` or unknown keys |
236
+ | Anything else | 500 | `core.system_error` | `Internal Server Error` in production; the raw message in development | never |
261
237
 
262
- **Cause:** A plain `Error` (not `ApplicationError`) was thrown. The `appErrorHandler` middleware only reads `statusCode` from errors that have that property. Native `Error` instances default to `500`.
238
+ Only the intentional branch reports what the throw site wrote. The other four **replace** the message, because a driver error carries SQL, schema and constraint names - and `normalized` is built from the replacement, so it can never leak what `message` just scrubbed.
263
239
 
264
- **Fix:** Use `getError()` or `new ApplicationError()` instead of `new Error()`:
240
+ `rootKey` (e.g. `new AppErrorMiddleware({ logger, rootKey: 'error' }).value()`) nests any of these under that key.
265
241
 
266
- ```typescript
267
- // Incorrect -- will return 500
268
- throw new Error('Not found');
242
+ The handler is fail-closed on environment: it exposes `stack` and `cause` in `details` only when `NODE_ENV` is one of `local`, `debug`, `development`, `dev`, `sit` (`Environment.DEVELOPMENT_ENVS`). Everything else - including `alpha`, `staging`, a typo, or an unset `NODE_ENV` - gets the sanitized shape:
269
243
 
270
- // Correct -- will return 404
271
- throw getError({
272
- message: 'Not found',
273
- statusCode: HTTP.ResultCodes.RS_4.NotFound,
274
- });
244
+ ```json
245
+ {
246
+ "message": "Only %{available} left of %{variantId}.",
247
+ "statusCode": 409,
248
+ "normalized": {
249
+ "text": "Only %{available} left of %{variantId}.",
250
+ "code": "server.core.stock_reservation.reserve.unavailable",
251
+ "args": { "variantId": "V1", "available": 2 }
252
+ },
253
+ "extra": {
254
+ "details": { "locationId": "L9" }
255
+ },
256
+ "requestId": "abc-123-def",
257
+ "details": { "url": "http://localhost:3000/reservations", "path": "/reservations" }
258
+ }
275
259
  ```
276
260
 
277
- ## See Also
261
+ `extra` is absent entirely when the throw site attached no context of its own.
278
262
 
279
- - [Controllers](/references/base/controllers) -- Throwing errors in route handlers
280
- - [Services](/references/base/services) -- Error handling in business logic
281
- - [Middlewares](/references/base/middlewares) -- The `appErrorHandler` middleware
282
- - [Helpers Index](/extensions/helpers/) -- All available helpers
283
- - [Logger Helper](/extensions/helpers/logger/) -- Logging errors
263
+ > [!IMPORTANT]
264
+ > **`messageCode` and `extra.messageArgs` are GONE from the response.** They duplicated `normalized.code` and `normalized.args`. Read `normalized` - it is the only source. A client still reading either must migrate: `translate(error.messageCode, error.extra?.messageArgs)` becomes `translate(error.normalized.code, error.normalized.args)`.
265
+ >
266
+ > Note the two `details`: the inner one is context the throw site attached (it went through `extra`), the outer one is the middleware's own request info. They are unrelated despite the name.
267
+
268
+ > [!NOTE]
269
+ > `message` and `normalized.text` are the same string unless a `transform` deliberately makes them differ - `message` stays the raw text the throw site wrote, `normalized.text` is what a client shows. Most errors never set `transform`, so most of the time they match.
270
+
271
+ **Common status codes**
272
+
273
+ | Scenario | Status | `HTTP.ResultCodes` path |
274
+ | ------------------------ | ------ | -------------------------- |
275
+ | Invalid input | 400 | `RS_4.BadRequest` |
276
+ | Missing/invalid auth | 401 | `RS_4.Unauthorized` |
277
+ | Insufficient permissions | 403 | `RS_4.Forbidden` |
278
+ | Resource not found | 404 | `RS_4.NotFound` |
279
+ | Duplicate resource | 409 | `RS_4.Conflict` |
280
+ | Server failure | 500 | `RS_5.InternalServerError` |
281
+
282
+ ## See also
283
+
284
+ - [Controllers](/references/base/controllers) - throwing errors in route handlers
285
+ - [Services](/references/base/services) - error handling in business logic
286
+ - [Middlewares](/references/base/middlewares) - the `AppErrorMiddleware` handler
287
+ - [Environment](/extensions/helpers/env/) - `Environment.DEVELOPMENT_ENVS`, the error-detail boundary
288
+ - [Helpers Overview](/extensions/helpers/) - all available helpers
289
+
290
+ **Files:**
291
+
292
+ - [`packages/helpers/src/modules/error/index.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/error/index.ts) - module barrel
293
+ - [`packages/helpers/src/modules/error/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/error/types.ts) - `ErrorSchema`, `TErrorResponse` (the RESPONSE schema, for OpenAPI)
294
+ - [`packages/inversion/src/modules/error/app-error.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/inversion/src/modules/error/app-error.ts) - `ApplicationError`, `getError`, `isApplicationError`
295
+ - [`packages/inversion/src/modules/error/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/inversion/src/modules/error/types.ts) - `TError`, `TErrorDefinition`, `TErrorNormalized`, `IErrorKeyRegistry`, `TRegisterErrors`
296
+ - [`packages/inversion/src/modules/error/definition.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/inversion/src/modules/error/definition.ts) - `ErrorScopes`, `TErrorScope`
297
+ - [`packages/inversion/src/modules/error/message-code.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/inversion/src/modules/error/message-code.ts) - `MessageCode`