@venizia/ignis-docs 0.2.0 → 0.2.1-1

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 (174) 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 +24 -13
  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 +27 -3
  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 +8 -4
  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 +247 -153
  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 +109 -323
  26. package/content/extensions/components/authentication/api.md +489 -602
  27. package/content/extensions/components/authentication/errors.md +135 -498
  28. package/content/extensions/components/authentication/index.md +89 -801
  29. package/content/extensions/components/authentication/usage.md +231 -955
  30. package/content/extensions/components/authorization/api.md +991 -644
  31. package/content/extensions/components/authorization/errors.md +204 -208
  32. package/content/extensions/components/authorization/getting-started.md +227 -0
  33. package/content/extensions/components/authorization/index.md +88 -795
  34. package/content/extensions/components/authorization/usage.md +219 -528
  35. package/content/extensions/components/health-check.md +77 -243
  36. package/content/extensions/components/index.md +24 -90
  37. package/content/extensions/components/mail/api.md +553 -285
  38. package/content/extensions/components/mail/errors.md +76 -62
  39. package/content/extensions/components/mail/index.md +111 -463
  40. package/content/extensions/components/mail/usage.md +139 -173
  41. package/content/extensions/components/request-tracker.md +70 -173
  42. package/content/extensions/components/socket-io/api.md +462 -785
  43. package/content/extensions/components/socket-io/errors.md +49 -51
  44. package/content/extensions/components/socket-io/index.md +69 -372
  45. package/content/extensions/components/socket-io/usage.md +212 -105
  46. package/content/extensions/components/static-asset/api.md +461 -141
  47. package/content/extensions/components/static-asset/errors.md +121 -53
  48. package/content/extensions/components/static-asset/index.md +84 -606
  49. package/content/extensions/components/static-asset/usage.md +184 -299
  50. package/content/extensions/components/template/index.md +3 -3
  51. package/content/extensions/components/websocket/api.md +311 -404
  52. package/content/extensions/components/websocket/errors.md +47 -56
  53. package/content/extensions/components/websocket/index.md +75 -407
  54. package/content/extensions/components/websocket/usage.md +120 -338
  55. package/content/extensions/helpers/cron/index.md +52 -160
  56. package/content/extensions/helpers/crypto/index.md +67 -480
  57. package/content/extensions/helpers/crypto/reference.md +528 -0
  58. package/content/extensions/helpers/env/index.md +64 -178
  59. package/content/extensions/helpers/error/index.md +286 -196
  60. package/content/extensions/helpers/index.md +61 -47
  61. package/content/extensions/helpers/inversion/index.md +76 -550
  62. package/content/extensions/helpers/inversion/reference.md +530 -0
  63. package/content/extensions/helpers/kafka/admin.md +23 -3
  64. package/content/extensions/helpers/kafka/compile-binary.md +83 -52
  65. package/content/extensions/helpers/kafka/consumer.md +65 -28
  66. package/content/extensions/helpers/kafka/examples.md +46 -253
  67. package/content/extensions/helpers/kafka/index.md +55 -617
  68. package/content/extensions/helpers/kafka/producer.md +156 -22
  69. package/content/extensions/helpers/kafka/schema-registry.md +59 -79
  70. package/content/extensions/helpers/logger/hf-logger.md +220 -0
  71. package/content/extensions/helpers/logger/index.md +78 -552
  72. package/content/extensions/helpers/logger/pino.md +105 -0
  73. package/content/extensions/helpers/logger/reference.md +937 -0
  74. package/content/extensions/helpers/network/api.md +276 -195
  75. package/content/extensions/helpers/network/index.md +87 -524
  76. package/content/extensions/helpers/queue/index.md +80 -897
  77. package/content/extensions/helpers/queue/reference.md +494 -0
  78. package/content/extensions/helpers/redis/index.md +86 -640
  79. package/content/extensions/helpers/redis/reference.md +784 -0
  80. package/content/extensions/helpers/secrets/index.md +136 -0
  81. package/content/extensions/helpers/socket-io/api.md +325 -204
  82. package/content/extensions/helpers/socket-io/index.md +79 -429
  83. package/content/extensions/helpers/storage/api.md +584 -464
  84. package/content/extensions/helpers/storage/index.md +80 -573
  85. package/content/extensions/helpers/types/index.md +75 -495
  86. package/content/extensions/helpers/types/reference.md +689 -0
  87. package/content/extensions/helpers/uid/index.md +176 -189
  88. package/content/extensions/helpers/websocket/api.md +366 -214
  89. package/content/extensions/helpers/websocket/index.md +68 -503
  90. package/content/extensions/helpers/worker-thread/index.md +62 -396
  91. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  92. package/content/extensions/index.md +38 -39
  93. package/content/extensions/src-details/mcp-server.md +96 -548
  94. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  95. package/content/guides/core-concepts/persistent/index.md +5 -1
  96. package/content/guides/core-concepts/persistent/models.md +1 -1
  97. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  98. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  99. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  100. package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
  101. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  102. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  103. package/content/guides/core-concepts/secrets-vault.md +177 -0
  104. package/content/guides/core-concepts/services.md +1 -1
  105. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  106. package/content/guides/get-started/philosophy.md +135 -670
  107. package/content/guides/get-started/setup.md +53 -74
  108. package/content/guides/migrations/redis-helpers-migration.md +5 -4
  109. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  110. package/content/guides/migrations/unified-connectors-migration.md +8 -7
  111. package/content/guides/tutorials/ecommerce-api.md +3 -8
  112. package/content/guides/tutorials/realtime-chat.md +1 -1
  113. package/content/references/base/application.md +64 -22
  114. package/content/references/base/components.md +3 -3
  115. package/content/references/base/connectors.md +79 -136
  116. package/content/references/base/controllers.md +12 -12
  117. package/content/references/base/datasources-reference.md +600 -0
  118. package/content/references/base/datasources.md +84 -444
  119. package/content/references/base/dependency-injection.md +25 -46
  120. package/content/references/base/filter-system/application-usage.md +95 -123
  121. package/content/references/base/filter-system/array-operators.md +33 -43
  122. package/content/references/base/filter-system/comparison-operators.md +55 -63
  123. package/content/references/base/filter-system/default-filter.md +144 -350
  124. package/content/references/base/filter-system/fields-order-pagination.md +116 -148
  125. package/content/references/base/filter-system/index.md +114 -253
  126. package/content/references/base/filter-system/json-filtering.md +52 -181
  127. package/content/references/base/filter-system/list-operators.md +28 -44
  128. package/content/references/base/filter-system/logical-operators.md +69 -114
  129. package/content/references/base/filter-system/null-operators.md +41 -98
  130. package/content/references/base/filter-system/pattern-matching.md +48 -51
  131. package/content/references/base/filter-system/quick-reference.md +93 -196
  132. package/content/references/base/filter-system/range-operators.md +22 -38
  133. package/content/references/base/filter-system/tips.md +70 -133
  134. package/content/references/base/filter-system/use-cases.md +173 -232
  135. package/content/references/base/grpc-controllers.md +53 -17
  136. package/content/references/base/index.md +5 -3
  137. package/content/references/base/middlewares.md +43 -28
  138. package/content/references/base/models-reference.md +886 -0
  139. package/content/references/base/models.md +81 -1452
  140. package/content/references/base/providers.md +8 -8
  141. package/content/references/base/repositories/advanced.md +281 -419
  142. package/content/references/base/repositories/index.md +84 -644
  143. package/content/references/base/repositories/mixins.md +25 -21
  144. package/content/references/base/repositories/relations.md +194 -375
  145. package/content/references/base/repositories/soft-deletable.md +67 -55
  146. package/content/references/base/secrets.md +267 -0
  147. package/content/references/base/services.md +8 -6
  148. package/content/references/configuration/environment-variables.md +73 -21
  149. package/content/references/configuration/index.md +51 -31
  150. package/content/references/index.md +1 -1
  151. package/content/references/quick-reference.md +3 -16
  152. package/content/references/utilities/crypto.md +35 -76
  153. package/content/references/utilities/date.md +33 -73
  154. package/content/references/utilities/duration.md +85 -0
  155. package/content/references/utilities/index.md +6 -2
  156. package/content/references/utilities/jsx-reference.md +298 -0
  157. package/content/references/utilities/jsx.md +82 -525
  158. package/content/references/utilities/module.md +81 -61
  159. package/content/references/utilities/parse.md +34 -64
  160. package/content/references/utilities/performance.md +33 -58
  161. package/content/references/utilities/promise.md +28 -62
  162. package/content/references/utilities/request.md +58 -218
  163. package/content/references/utilities/retry.md +139 -0
  164. package/content/references/utilities/schema.md +43 -137
  165. package/content/references/utilities/statuses-reference.md +361 -0
  166. package/content/references/utilities/statuses.md +63 -667
  167. package/dist/mcp-server/index.js +0 -0
  168. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  169. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  171. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  172. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  173. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  174. package/package.json +24 -23
@@ -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,70 +20,163 @@ throw getError({
33
20
  });
34
21
  ```
35
22
 
36
- #### Options (`TError`)
23
+ The framework's `AppErrorMiddleware` catches `ApplicationError` instances and formats them into a consistent JSON response.
24
+
25
+ ## Find what you need
26
+
27
+ | You want to | Go to |
28
+ |---|---|
29
+ | Throw a one-off error with a status code | [Throw a free-form error](#throw-a-free-form-error) |
30
+ | Re-throw an error you already caught and shaped | [Recognize an already-shaped error in a catch block](#recognize-an-already-shaped-error-in-a-catch-block) |
31
+ | Declare a reusable, i18n-ready error once | [Catalog a reusable error](#catalog-a-reusable-error) |
32
+ | Get autocomplete on `messageCode` | [Register catalog keys for messageCode autocomplete](#register-catalog-keys-for-messagecode-autocomplete) |
33
+ | Build a message code outside a catalog | [Build a code outside a catalog](#build-a-code-outside-a-catalog) |
34
+ | See the exact JSON shape a client receives | [Read the error response shape](#read-the-error-response-shape) |
35
+ | Read or rehydrate a server error in a browser app | [Consume the error response from a browser client](#consume-the-error-response-from-a-browser-client) |
36
+ | Look up what a specific input produces | [Every shape and its output](#every-shape-and-its-output) |
37
+ | Confirm you're checking errors the safe way | [How it works](#how-it-works) - `isApplicationError()`, never `instanceof` |
38
+
39
+ ## How it works
40
+
41
+ - **Three equivalent entry points.** `getError(opts)`, `new ApplicationError(opts)`, and the static `ApplicationError.getError(opts)` all build the same object from the same input. Use the class form only when a direct reference reads better.
42
+ - **Two input shapes.** Free-form covers one-off failures - most throw sites. Catalogued raises a failure declared once at module scope, so its code, status, and default text can't drift across the call sites that raise it.
37
43
 
38
- The same options apply to `getError()`, the `ApplicationError` constructor, and the static factory:
44
+ | Shape | Input |
45
+ |---|---|
46
+ | Free-form | `{ message, statusCode?, messageCode? }` |
47
+ | Catalogued | `{ error: TErrorDefinition }` |
39
48
 
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) |
49
+ - **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`:
50
+
51
+ ```typescript
52
+ getError({ message: 'Only %{n} left', messageCode: 'stock.low', messageArgs: { n: 2 } }); // flat
53
+ getError({ message: { text: 'Only %{n} left', code: 'stock.low', args: { n: 2 } } }); // nested
54
+ getError({ error: StockErrors.LOW, messageArgs: { n: 2 } }); // catalogued
55
+ ```
56
+
57
+ On the catalogued form, `message` is a **partial** override. `{ message: { args } }` amends just the args and keeps the definition's `text`/`code`.
58
+
59
+ - **Precedence resolves most-specific-first.** See the [full precedence table](#every-shape-and-its-output) below - a definition's own `message.code` beats a flat `messageCode`.
60
+ - **`messageCode` always resolves to something.** `MessageCode.resolve()` lower-cases it and falls back to `MessageCode.DEFAULT` (`'core.system_error'`) when none is given or empty. `error.normalized.code` is never `undefined`.
61
+ - **`normalized` is the single source - there's no flat duplicate.** It's `{ text, code, args }`. `text` defaults to `message`; `code` and `args` resolve per the precedence table.
62
+ - **`args` is always populated, so no consumer needs a null check.** One lookup renders any error: `translate(error.normalized.code, error.normalized.args)`.
63
+ - **`messageCode` and `messageArgs` are INPUTS only.** There's no `error.messageCode` field, and `extra` never mirrors `messageArgs`. Pass `transform` to build `normalized` yourself in place of the default.
64
+ - **Any key the input doesn't declare rides into `extra`.** Attach whatever context your clients need: `getError({ message, transaction: {...} })` lands at `error.extra.transaction`. Passing `extra` explicitly works too - the two merge, and the explicit one wins on a clash.
65
+ - **`extra` carries caller context only.** It's `undefined` when there is nothing to carry.
66
+ - **`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.
67
+ - **`error` is refused on the free-form branch (`error?: never`).** This makes `getError({ message, error: caughtError })` fail to compile, so the mistake is caught before runtime - use `cause` instead.
68
+
69
+ > [!IMPORTANT]
70
+ > **Use `isApplicationError()`, never `instanceof ApplicationError`.** There is one `ApplicationError` class, defined in `@venizia/ignis-inversion`. `helpers` re-exports it, so a browser app can raise and read the same errors the server does. But `instanceof` still fails across a package boundary: inversion ships dual CJS+ESM builds, so one source class has two runtime constructors. `isApplicationError()` tests shape, not identity, so it works everywhere.
71
+
72
+ **Options shared by both forms**
73
+
74
+ | Option | Type | Description |
75
+ | --------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
76
+ | `message` | `string \| { text, code?, args? }` | Required on the free-form branch; a **partial** override on the catalogued branch (definition supplies what's omitted) |
77
+ | `messageCode` | `string` | Free-form only, sibling to a string `message`. Lowest precedence - `message.code` and the definition's `message.code` both win over it |
78
+ | `statusCode` | `number` | Defaults to `400`, or the definition's `statusCode` for the catalogued form |
79
+ | `messageArgs` | `Record<string, unknown>` | Interpolation values. Reaches `normalized.args` - never `extra`. Lowest precedence, same rule as `messageCode` |
80
+ | `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 |
81
+ | `extra` | `Record<string, unknown>` | Explicit context, merged with any swept keys - and it wins on a clash |
82
+ | `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 |
83
+ | `logLevel` | `error \| emerg \| warn \| info \| debug` | The level the error handler logs this at. Defaults to `error`; lower it for an expected failure (`getError({ message, statusCode: 404, logLevel: 'warn' })`), or raise it to `emerg`. Steers the server log only - never the response |
84
+ | _anything else_ | `unknown` | Rides into `extra` under its own name. `getError({ message, transaction })` lands at `error.extra.transaction` |
46
85
 
47
86
  > [!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.
87
+ > 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", not "raise these loose fields."
49
88
 
50
- #### `ApplicationError` Constructor and Static Factory
89
+ ## Every shape and its output
51
90
 
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:
91
+ Every row below is the real output, and the catalogued rows all use this definition:
53
92
 
54
93
  ```typescript
55
- import { ApplicationError, HTTP } from '@venizia/ignis-helpers';
94
+ const DEF = {
95
+ message: {
96
+ text: 'A category named "%{name}" already exists.',
97
+ code: 'server.commerce.category.duplicate',
98
+ args: { name: '?' },
99
+ },
100
+ statusCode: 409,
101
+ } as const satisfies TErrorDefinition;
102
+ ```
56
103
 
57
- // Class constructor (equivalent to getError)
58
- const error = new ApplicationError({
59
- message: 'Configuration missing',
60
- statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
61
- });
104
+ **Free-form, flat** - the historical shape, unchanged:
62
105
 
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
- });
69
- ```
106
+ | Input | `message` | `statusCode` | `normalized` |
107
+ |-------|-----------|--------------|--------------|
108
+ | `{ message: 'Broke' }` | `Broke` | `400` | `{ text: 'Broke', code: 'core.system_error', args: {} }` |
109
+ | `{ message: 'Broke', messageCode: 'a.b' }` | `Broke` | `400` | `{ text: 'Broke', code: 'a.b', args: {} }` |
110
+ | `{ message: 'Only %{n} left', messageArgs: { n: 2 } }` | `Only %{n} left` | `400` | `{ text: 'Only %{n} left', code: 'core.system_error', args: { n: 2 } }` |
111
+ | `{ message, messageCode: 'stock.low', messageArgs: { n: 2 }, statusCode: 409 }` | `Only %{n} left` | `409` | `{ text: 'Only %{n} left', code: 'stock.low', args: { n: 2 } }` |
70
112
 
71
- ### MessageCode
113
+ **Free-form, object** - `text` is the only required field. Each row is identical to its flat twin above:
72
114
 
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.
115
+ | Input | `normalized` |
116
+ |-------|--------------|
117
+ | `{ message: { text: 'Broke' } }` | `{ text: 'Broke', code: 'core.system_error', args: {} }` |
118
+ | `{ message: { text: 'Broke', code: 'a.b' } }` | `{ text: 'Broke', code: 'a.b', args: {} }` |
119
+ | `{ message: { text: 'Only %{n} left', code: 'stock.low', args: { n: 2 } } }` | `{ text: 'Only %{n} left', code: 'stock.low', args: { n: 2 } }` |
74
120
 
75
- ```typescript
76
- import { MessageCode } from '@venizia/ignis-helpers';
121
+ **Catalogued** - omit a field and the definition supplies it:
77
122
 
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
- }
123
+ | Input | `message` | `statusCode` | `normalized.code` | `normalized.args` |
124
+ |-------|-----------|--------------|-------------------|-------------------|
125
+ | `{ error: DEF }` | the definition's text | `409` | `server.commerce.category.duplicate` | `{ name: '?' }` |
126
+ | `{ error: DEF, messageArgs: { name: 'Vé' } }` | the definition's text | `409` | `server.commerce.category.duplicate` | `{ name: 'Vé' }` |
127
+ | `{ error: DEF, message: { args: { name: 'Vé' } } }` | the definition's text | `409` | `server.commerce.category.duplicate` | `{ name: 'Vé' }` |
128
+ | `{ error: DEF, message: { text: 'Custom' } }` | `Custom` | `409` | `server.commerce.category.duplicate` | `{ name: '?' }` |
129
+ | `{ error: DEF, message: { code: 'override.code' } }` | the definition's text | `409` | `override.code` | `{ name: '?' }` |
130
+ | `{ error: DEF, message: 'Custom' }` | `Custom` | `409` | `server.commerce.category.duplicate` | `{ name: '?' }` |
131
+ | `{ error: DEF, statusCode: 410 }` | the definition's text | `410` | `server.commerce.category.duplicate` | `{ name: '?' }` |
132
+ | `{ ...DEF }` (spread) | the definition's text | `409` | `server.commerce.category.duplicate` | `{ name: '?' }` |
133
+
134
+ **Context and cause:**
135
+
136
+ | Input | Output |
137
+ |-------|--------|
138
+ | `{ message, extra: { categoryId: 42 } }` | `extra: { categoryId: 42 }` |
139
+ | `{ message, userId: 7, transaction: { id: 12 } }` | `extra: { userId: 7, transaction: { id: 12 } }` - swept |
140
+ | `{ message, userId: 7, extra: { userId: 9 } }` | `extra: { userId: 9 }` - explicit wins |
141
+ | `{ message, cause: err }` | `Error.cause = err`; `extra` stays `undefined` |
142
+ | `{ message, error: caughtError }` | **does not compile** - use `cause` |
143
+
144
+ **Transform** - `message` in the snapshot IS the default being replaced:
145
+
146
+ ```typescript
147
+ getError({
148
+ message: { text: 'Only %{n} left', code: 'stock.low', args: { n: 2 } },
149
+ transform: snapshot => ({ ...snapshot.message, text: 'Chỉ còn 2 vé.' }),
150
+ });
151
+ // error.message -> 'Only %{n} left' (the raw text stays)
152
+ // error.normalized.text -> 'Chỉ còn 2 vé.'
153
+ // error.normalized.code -> 'stock.low'
154
+ // error.normalized.args -> { n: 2 }
82
155
  ```
83
156
 
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` |
157
+ **Precedence,** most specific first - note a definition's `message.code` beats a flat `messageCode`:
93
158
 
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()`).
159
+ | Resolves | Order |
160
+ |----------|-------|
161
+ | `normalized.code` | `message.code` -> the definition's `message.code` -> `messageCode` -> `MessageCode.DEFAULT` |
162
+ | `normalized.args` | `message.args` -> `messageArgs` -> the definition's `message.args` -> `{}` |
163
+ | `normalized.text` | `message.text` (or a string `message`) -> the definition's `message.text` -> `''` |
164
+ | `statusCode` | `statusCode` -> the definition's `statusCode` -> `400` |
165
+
166
+ ## Common tasks
96
167
 
97
- ## Checking for an Application Error
168
+ ### Throw a free-form error
98
169
 
99
- Use `isApplicationError(error)` to recognize an application error by **shape** - an `Error` instance carrying a numeric `statusCode` - rather than by class identity:
170
+ The most common shape - a `message` and a status.
171
+
172
+ ```typescript
173
+ throw getError({
174
+ statusCode: HTTP.ResultCodes.RS_4.Conflict,
175
+ message: 'Username already exists',
176
+ });
177
+ ```
178
+
179
+ ### Recognize an already-shaped error in a catch block
100
180
 
101
181
  ```typescript
102
182
  import { isApplicationError } from '@venizia/ignis-helpers';
@@ -105,179 +185,189 @@ try {
105
185
  await someOperation();
106
186
  } catch (error) {
107
187
  if (isApplicationError(error)) {
108
- // already shaped (has a statusCode) - surface as-is
109
- throw error;
188
+ throw error; // already shaped - surface as-is
110
189
  }
111
- // an unknown failure - sanitize before rethrowing
112
- throw getError({ message: 'Operation failed', statusCode: HTTP.ResultCodes.RS_5.InternalServerError });
190
+ throw getError({
191
+ message: 'Operation failed',
192
+ statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
193
+ });
113
194
  }
114
195
  ```
115
196
 
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
197
+ ### Catalog a reusable error
122
198
 
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.
199
+ Declare it once; every call site raises it by reference instead of retyping the code and status.
124
200
 
125
201
  ```typescript
126
- import { getError, HTTP } from '@venizia/ignis-helpers';
202
+ import { ErrorScopes, getError, HTTP } from '@venizia/ignis-helpers';
203
+ import type { TErrorDefinition } from '@venizia/ignis-helpers';
204
+
205
+ const CategoryErrors = {
206
+ CREATE_DUPLICATE_NAME: {
207
+ message: {
208
+ text: 'A category named "%{name}" already exists.',
209
+ code: 'server.commerce.category.create.duplicate_name',
210
+ },
211
+ statusCode: HTTP.ResultCodes.RS_4.Conflict,
212
+ category: ErrorScopes.VALIDATION,
213
+ },
214
+ } as const satisfies Record<string, TErrorDefinition>;
127
215
 
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
- }
216
+ throw getError({ error: CategoryErrors.CREATE_DUPLICATE_NAME, messageArgs: { name: 'Vé' } });
217
+ // Equivalent, using the catalogued form's partial override instead:
218
+ throw getError({ error: CategoryErrors.CREATE_DUPLICATE_NAME, message: { args: { name: 'Vé' } } });
140
219
  ```
141
220
 
142
- ### Using `messageCode` for Client-Side Handling
221
+ `ErrorScopes` groups a failure by intent - `AUTH`, `VALIDATION`, `BUSINESS`, `SYSTEM`, `INTEGRATION`. `statusCode` can't do this job: a `409` is a business conflict in one place and a validation clash in another.
143
222
 
144
- The `messageCode` field allows frontend applications to map errors to localized messages or specific UI behaviors without parsing the human-readable `message` string.
223
+ > [!WARNING]
224
+ > `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). Don't expect a client to receive it.
225
+
226
+ ### Register catalog keys for `messageCode` autocomplete
227
+
228
+ 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.
145
229
 
146
230
  ```typescript
147
- throw getError({
148
- message: 'Email verification required before login',
149
- statusCode: HTTP.ResultCodes.RS_4.Forbidden,
150
- messageCode: 'auth.email_not_verified',
151
- });
231
+ import type { TRegisterErrors } from '@venizia/ignis-helpers';
232
+
233
+ declare module '@venizia/ignis-helpers' {
234
+ interface IErrorKeyRegistry extends TRegisterErrors<typeof CategoryErrors> {}
235
+ }
152
236
  ```
153
237
 
154
- ### Error Response Format
238
+ > [!WARNING]
239
+ > 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.
155
240
 
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.
241
+ 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.
157
242
 
158
- #### Sanitized (Production-Class) Response
243
+ ### Build a code outside a catalog
159
244
 
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`.
245
+ `MessageCode.build()` validates at import time instead of shipping a malformed code into production.
161
246
 
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.
247
+ ```typescript
248
+ import { MessageCode } from '@venizia/ignis-helpers';
163
249
 
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
- }
250
+ const NOT_FOUND = MessageCode.build({ parts: ['core', 'user', 'not_found'] });
251
+ // 'core.user.not_found' - throws if a segment isn't lower snake_case, or fewer than 2 parts
175
252
  ```
176
253
 
177
- #### Development Response
254
+ ### Read the error response shape
255
+
256
+ `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.
257
+
258
+ | What was thrown | Status | `normalized.code` | `message` | `extra` |
259
+ | ----------------------------------------- | ------- | ------------------------------------------------------ | --------------------------------------------------------------------- | ----------------------------------------------------- |
260
+ | `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 |
261
+ | DB client error (SQLSTATE class 22/23/44) | 400 | `core.system_error` | a fixed, safe summary - never the driver's text | never |
262
+ | Transient DB conflict (40001/40P01) | 409 | `database.conflict` | a fixed retry message | never |
263
+ | `getError(...)` - intentional | its own | its own | its own | when the throw site attached `extra` or unknown keys |
264
+ | Anything else | 500 | `core.system_error` | `Internal Server Error` in production; the raw message in development | never |
265
+
266
+ Only the intentional branch reports what the throw site wrote. The other four **replace** the message - a driver error can carry SQL, schema, and constraint names. `normalized` is built from that replacement, so it can never leak what `message` just scrubbed.
178
267
 
179
- In a development environment (see the list above), `details` additionally includes debugging fields:
268
+ `rootKey` (e.g. `new AppErrorMiddleware({ logger, rootKey: 'error' }).value()`) nests any of these under that key.
269
+
270
+ The handler is fail-closed on environment. It exposes `stack` and `cause` in `details` only for `Environment.DEVELOPMENT_ENVS`: `local`, `debug`, `development`, `dev`, `sit`. Everything else - including `alpha`, `staging`, a typo, or an unset `NODE_ENV` - gets the sanitized shape:
180
271
 
181
272
  ```json
182
273
  {
183
- "message": "User not found",
184
- "messageCode": "core.user.not_found",
185
- "statusCode": 404,
274
+ "message": "Only %{available} left of %{variantId}.",
275
+ "statusCode": 409,
276
+ "normalized": {
277
+ "text": "Only %{available} left of %{variantId}.",
278
+ "code": "server.core.stock_reservation.reserve.unavailable",
279
+ "args": { "variantId": "V1", "available": 2 }
280
+ },
281
+ "extra": {
282
+ "details": { "locationId": "L9" }
283
+ },
186
284
  "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": "..."
192
- }
285
+ "details": { "url": "http://localhost:3000/reservations", "path": "/reservations" }
193
286
  }
194
287
  ```
195
288
 
196
- ### ErrorSchema (Zod)
197
-
198
- `ErrorSchema` is a Zod object schema used for OpenAPI response documentation. It is typically referenced in route definitions to describe error responses.
289
+ `extra` is absent entirely when the throw site attached no context of its own.
199
290
 
200
- ```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 },
209
- },
210
- },
211
- };
212
- ```
213
-
214
- The schema shape:
291
+ > [!IMPORTANT]
292
+ > **`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)`.
293
+ >
294
+ > 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're unrelated despite sharing a name.
215
295
 
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
- ```
296
+ > [!NOTE]
297
+ > `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 the two match.
226
298
 
227
- ### Common Status Code Patterns
299
+ ### Consume the error response from a browser client
228
300
 
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` |
301
+ The error layer lives in `@venizia/ignis-inversion`, not in helpers, precisely so a browser app can
302
+ share it. It depends only on `lodash`, ships dual CJS+ESM, and pulls in no server module. A frontend
303
+ throws its own failures with the same `getError` the server uses, and reads the server's with the
304
+ same field names.
238
305
 
239
- ## Troubleshooting
306
+ **Reading is all most clients need.** `normalized` is on every error response:
240
307
 
241
- ### `statusCode` defaults to 400
308
+ - `getError` always builds one.
309
+ - A foreign error gets one synthesized.
310
+ - A `ZodError` builds its own.
242
311
 
243
- **Cause:** `getError()` was called without specifying a `statusCode`. The `ApplicationError` constructor defaults to `400` (Bad Request).
312
+ So there's no null-check and no parsing step: one lookup handles every case.
244
313
 
245
- **Fix:** Always provide an explicit status code using `HTTP.ResultCodes`:
314
+ ```ts
315
+ const { error } = await response.json(); // `error` is the rootKey, if one is configured
246
316
 
247
- ```typescript
248
- throw getError({
249
- message: 'Resource not found',
250
- statusCode: HTTP.ResultCodes.RS_4.NotFound,
251
- });
317
+ translate(error.normalized.code, error.normalized.args);
252
318
  ```
253
319
 
254
- ### Error response missing `stack` and `cause`
320
+ **Rehydrating** is for the client that wants one `catch` block for both a server failure and a
321
+ locally thrown one. `fromError` inverts the response the middleware emitted:
255
322
 
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`.
323
+ ```ts
324
+ import { fromError, isApplicationError } from '@venizia/ignis-inversion';
257
325
 
258
- **Fix:** Set `NODE_ENV` to a development name - `development`, `dev`, `local`, `debug` or `sit`. Anything else stays sanitized by design.
259
-
260
- ### Errors returning 500 instead of expected status code
261
-
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`.
263
-
264
- **Fix:** Use `getError()` or `new ApplicationError()` instead of `new Error()`:
265
-
266
- ```typescript
267
- // Incorrect -- will return 500
268
- throw new Error('Not found');
269
-
270
- // Correct -- will return 404
271
- throw getError({
272
- message: 'Not found',
273
- statusCode: HTTP.ResultCodes.RS_4.NotFound,
274
- });
326
+ const { error } = await response.json();
327
+ throw fromError({ error }); // now an ApplicationError - isApplicationError() is true
275
328
  ```
276
329
 
277
- ## See Also
330
+ | Wire field | Where it lands |
331
+ | ------------ | --------------------------------------------------------------------------------- |
332
+ | `normalized` | `normalized`, verbatim - `text`, `code` and `args` round-trip unchanged |
333
+ | `message` | `normalized.text`, but only when `normalized` is missing entirely |
334
+ | `statusCode` | `statusCode`; `400` when the payload carries none |
335
+ | `extra` | `extra`, verbatim |
336
+ | `requestId` | `extra.requestId` - it is the identifier a support ticket quotes, so it survives |
337
+ | `details` | dropped - `url` and `path` the client already knows, and `stack` is the server's |
278
338
 
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
339
+ Every field of `TResponsedError` is optional by design: a client parses what a gateway, a proxy or an
340
+ older server actually sent. A body that is not an IGNIS error at all still yields an
341
+ `ApplicationError`, degraded to `MessageCode.DEFAULT` and status `400`. No call site branches on
342
+ a parse failure.
343
+
344
+ > [!TIP]
345
+ > Same rule here: prefer `isApplicationError()` over `instanceof ApplicationError`. See the [callout above](#how-it-works) for why - a browser app bundles its own copy of the class, so `instanceof` fails across the boundary.
346
+
347
+ **Common status codes**
348
+
349
+ | Scenario | Status | `HTTP.ResultCodes` path |
350
+ | ------------------------ | ------ | -------------------------- |
351
+ | Invalid input | 400 | `RS_4.BadRequest` |
352
+ | Missing/invalid auth | 401 | `RS_4.Unauthorized` |
353
+ | Insufficient permissions | 403 | `RS_4.Forbidden` |
354
+ | Resource not found | 404 | `RS_4.NotFound` |
355
+ | Duplicate resource | 409 | `RS_4.Conflict` |
356
+ | Server failure | 500 | `RS_5.InternalServerError` |
357
+
358
+ ## See also
359
+
360
+ - [Controllers](/references/base/controllers) - throwing errors in route handlers
361
+ - [Services](/references/base/services) - error handling in business logic
362
+ - [Middlewares](/references/base/middlewares) - the `AppErrorMiddleware` handler
363
+ - [Environment](/extensions/helpers/env/) - `Environment.DEVELOPMENT_ENVS`, the error-detail boundary
364
+ - [Helpers Overview](/extensions/helpers/) - all available helpers
365
+
366
+ **Files:**
367
+
368
+ - [`packages/helpers/src/modules/error/index.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/error/index.ts) - module barrel
369
+ - [`packages/helpers/src/modules/error/schemas.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/error/schemas.ts) - `ErrorSchema`, `TErrorResponse` (the RESPONSE schema, for OpenAPI)
370
+ - [`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`, `fromError`, `isApplicationError`
371
+ - [`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`, `TResponsedError`, `IErrorKeyRegistry`, `TRegisterErrors`
372
+ - [`packages/inversion/src/modules/error/definition.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/inversion/src/modules/error/definition.ts) - `ErrorScopes`, `TErrorScope`
373
+ - [`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`