@venizia/ignis-docs 0.2.0 → 0.2.1-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +22 -11
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +6 -2
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +182 -93
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +107 -322
- package/content/extensions/components/authentication/api.md +454 -603
- package/content/extensions/components/authentication/errors.md +121 -498
- package/content/extensions/components/authentication/index.md +88 -801
- package/content/extensions/components/authentication/usage.md +207 -956
- package/content/extensions/components/authorization/api.md +736 -656
- package/content/extensions/components/authorization/errors.md +168 -206
- package/content/extensions/components/authorization/index.md +82 -797
- package/content/extensions/components/authorization/usage.md +194 -527
- package/content/extensions/components/health-check.md +71 -243
- package/content/extensions/components/mail/api.md +504 -287
- package/content/extensions/components/mail/errors.md +73 -61
- package/content/extensions/components/mail/index.md +96 -467
- package/content/extensions/components/mail/usage.md +130 -172
- package/content/extensions/components/request-tracker.md +66 -173
- package/content/extensions/components/socket-io/api.md +195 -13
- package/content/extensions/components/socket-io/errors.md +3 -3
- package/content/extensions/components/socket-io/index.md +50 -337
- package/content/extensions/components/socket-io/usage.md +143 -26
- package/content/extensions/components/static-asset/api.md +410 -142
- package/content/extensions/components/static-asset/errors.md +110 -53
- package/content/extensions/components/static-asset/index.md +79 -608
- package/content/extensions/components/static-asset/usage.md +180 -300
- package/content/extensions/components/websocket/api.md +275 -399
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +74 -407
- package/content/extensions/components/websocket/usage.md +110 -341
- package/content/extensions/helpers/cron/index.md +51 -160
- package/content/extensions/helpers/crypto/index.md +62 -483
- package/content/extensions/helpers/crypto/reference.md +456 -0
- package/content/extensions/helpers/env/index.md +60 -178
- package/content/extensions/helpers/error/index.md +221 -207
- package/content/extensions/helpers/inversion/index.md +65 -556
- package/content/extensions/helpers/inversion/reference.md +522 -0
- package/content/extensions/helpers/kafka/admin.md +20 -1
- package/content/extensions/helpers/kafka/compile-binary.md +41 -35
- package/content/extensions/helpers/kafka/consumer.md +54 -20
- package/content/extensions/helpers/kafka/examples.md +21 -16
- package/content/extensions/helpers/kafka/index.md +80 -610
- package/content/extensions/helpers/kafka/producer.md +134 -5
- package/content/extensions/helpers/kafka/schema-registry.md +45 -70
- package/content/extensions/helpers/logger/hf-logger.md +193 -0
- package/content/extensions/helpers/logger/index.md +64 -563
- package/content/extensions/helpers/logger/pino.md +85 -0
- package/content/extensions/helpers/logger/reference.md +746 -0
- package/content/extensions/helpers/network/api.md +241 -195
- package/content/extensions/helpers/network/index.md +72 -530
- package/content/extensions/helpers/queue/index.md +72 -900
- package/content/extensions/helpers/queue/reference.md +467 -0
- package/content/extensions/helpers/redis/index.md +73 -645
- package/content/extensions/helpers/redis/reference.md +727 -0
- package/content/extensions/helpers/secrets/index.md +66 -0
- package/content/extensions/helpers/socket-io/api.md +305 -203
- package/content/extensions/helpers/socket-io/index.md +66 -432
- package/content/extensions/helpers/storage/api.md +564 -462
- package/content/extensions/helpers/storage/index.md +77 -573
- package/content/extensions/helpers/types/index.md +66 -499
- package/content/extensions/helpers/types/reference.md +650 -0
- package/content/extensions/helpers/uid/index.md +58 -227
- package/content/extensions/helpers/websocket/api.md +329 -216
- package/content/extensions/helpers/websocket/index.md +65 -503
- package/content/extensions/helpers/worker-thread/index.md +58 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
- package/content/guides/core-concepts/persistent/transactions.md +1 -1
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/migrations/redis-helpers-migration.md +1 -1
- package/content/guides/migrations/unified-connectors-migration.md +2 -2
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/references/base/application.md +1 -1
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/datasources-reference.md +599 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +17 -39
- package/content/references/base/filter-system/application-usage.md +69 -121
- package/content/references/base/filter-system/array-operators.md +12 -0
- package/content/references/base/filter-system/comparison-operators.md +12 -0
- package/content/references/base/filter-system/default-filter.md +136 -348
- package/content/references/base/filter-system/fields-order-pagination.md +38 -16
- package/content/references/base/filter-system/index.md +106 -257
- package/content/references/base/filter-system/json-filtering.md +12 -2
- package/content/references/base/filter-system/list-operators.md +16 -2
- package/content/references/base/filter-system/logical-operators.md +13 -0
- package/content/references/base/filter-system/null-operators.md +13 -0
- package/content/references/base/filter-system/pattern-matching.md +12 -0
- package/content/references/base/filter-system/quick-reference.md +11 -2
- package/content/references/base/filter-system/range-operators.md +12 -0
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +156 -233
- package/content/references/base/middlewares.md +35 -21
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +80 -1452
- package/content/references/base/repositories/advanced.md +156 -192
- package/content/references/base/repositories/index.md +77 -650
- package/content/references/base/repositories/mixins.md +22 -18
- package/content/references/base/repositories/relations.md +123 -171
- package/content/references/base/repositories/soft-deletable.md +58 -56
- package/content/references/base/secrets.md +263 -0
- package/content/references/base/services.md +2 -2
- package/content/references/configuration/environment-variables.md +48 -4
- package/content/references/configuration/index.md +49 -31
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/index.md +1 -1
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +29 -62
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +57 -218
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- package/package.json +8 -8
|
@@ -1,27 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
>
|
|
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
|
-
|
|
61
|
+
## Every shape and its output
|
|
51
62
|
|
|
52
|
-
|
|
63
|
+
Every row below is the real output, and the catalogued rows all use this definition:
|
|
53
64
|
|
|
54
65
|
```typescript
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
76
|
+
**Free-form, flat** - the historical shape, unchanged:
|
|
72
77
|
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
106
|
+
**Context and cause:**
|
|
98
107
|
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
import { isApplicationError } from '@venizia/ignis-helpers';
|
|
116
|
+
**Transform** - `message` in the snapshot IS the default being replaced:
|
|
103
117
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
140
|
+
### Throw a free-form error
|
|
143
141
|
|
|
144
|
-
The
|
|
142
|
+
The most common shape - a `message` and a status.
|
|
145
143
|
|
|
146
144
|
```typescript
|
|
147
145
|
throw getError({
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
###
|
|
151
|
+
### Recognize an already-shaped error in a catch block
|
|
155
152
|
|
|
156
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
###
|
|
169
|
+
### Catalog a reusable error
|
|
197
170
|
|
|
198
|
-
|
|
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 {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
###
|
|
198
|
+
### Register catalog keys for `messageCode` autocomplete
|
|
228
199
|
|
|
229
|
-
|
|
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
|
-
|
|
202
|
+
```typescript
|
|
203
|
+
import type { TRegisterErrors } from '@venizia/ignis-helpers';
|
|
240
204
|
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
message: 'Resource not found',
|
|
250
|
-
statusCode: HTTP.ResultCodes.RS_4.NotFound,
|
|
251
|
-
});
|
|
252
|
-
```
|
|
220
|
+
import { MessageCode } from '@venizia/ignis-helpers';
|
|
253
221
|
|
|
254
|
-
|
|
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
|
-
|
|
226
|
+
### Read the error response shape
|
|
257
227
|
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
+
`rootKey` (e.g. `new AppErrorMiddleware({ logger, rootKey: 'error' }).value()`) nests any of these under that key.
|
|
265
241
|
|
|
266
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
message:
|
|
273
|
-
statusCode:
|
|
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
|
-
|
|
261
|
+
`extra` is absent entirely when the throw site attached no context of its own.
|
|
278
262
|
|
|
279
|
-
|
|
280
|
-
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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`
|