@nice-code/error 0.2.9 → 0.2.11

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 (3) hide show
  1. package/README.md +237 -5
  2. package/build/index.js +952 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,237 @@
1
- # @nice-code/error
2
-
3
- Typed, serializable errors with domain hierarchies, pattern matching, and safe transport across API boundaries.
4
-
5
- See the [main README](../../README.md) for full documentation and examples.
1
+ # @nice-code/error
2
+
3
+ Typed, serializable errors with domain hierarchies, pattern matching, and safe transport across API boundaries.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @nice-code/error
9
+ ```
10
+
11
+ ## Core concepts
12
+
13
+ - **Domain** — a named group of related errors (`err_user_auth`, `err_payment`)
14
+ - **Schema** — maps error id strings to message, HTTP status, and optional typed context
15
+ - **Error id** — identifies what went wrong within a domain; multiple ids can be active on one error
16
+ - **Context** — structured data attached to an id (e.g. `{ username: string }`)
17
+ - **Hydration** — deserializing context from a JSON round-trip back to the original typed value
18
+
19
+ ---
20
+
21
+ ## Defining an error domain
22
+
23
+ ```ts
24
+ import { defineNiceError, err } from "@nice-code/error";
25
+
26
+ export const err_auth = defineNiceError({
27
+ domain: "err_auth",
28
+ schema: {
29
+ // No context — second arg to fromId is not accepted
30
+ account_locked: err({
31
+ message: "Account is locked",
32
+ httpStatusCode: 403,
33
+ }),
34
+
35
+ // Required context — second arg to fromId is required
36
+ invalid_credentials: err<{ username: string }>({
37
+ message: ({ username }) => `Invalid credentials for: ${username}`,
38
+ httpStatusCode: 401,
39
+ context: { required: true },
40
+ }),
41
+
42
+ // Optional context
43
+ rate_limited: err<{ retryAfter: number }>({
44
+ message: (ctx) => ctx ? `Retry after ${ctx.retryAfter}s` : "Rate limited",
45
+ httpStatusCode: 429,
46
+ context: {},
47
+ }),
48
+ },
49
+ } as const);
50
+ ```
51
+
52
+ ### `err()` — schema entry helper
53
+
54
+ | Schema entry | `fromId("id")` | `fromId("id", ctx)` |
55
+ |---|---|---|
56
+ | `err()` / `err({ message, httpStatusCode })` | ✓ | ✗ |
57
+ | `err<C>({ context: {} })` | ✓ optional | ✓ optional |
58
+ | `err<C>({ context: { required: true } })` | ✗ | ✓ required |
59
+
60
+ ### Custom serialization (non-JSON-safe context)
61
+
62
+ ```ts
63
+ fs_error: err<{ cause: NodeJS.ErrnoException }>({
64
+ message: ({ cause }) => `FS error: ${cause.message}`,
65
+ context: {
66
+ required: true,
67
+ serialization: {
68
+ toJsonSerializable: ({ cause }) => ({ code: cause.code, message: cause.message }),
69
+ fromJsonSerializable: (obj) => ({ cause: Object.assign(new Error(obj.message), obj) }),
70
+ },
71
+ },
72
+ }),
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Creating errors
78
+
79
+ ```ts
80
+ // Single id — context required per schema
81
+ const err1 = err_auth.fromId("invalid_credentials", { username: "alice" });
82
+
83
+ // Multi-id in one error
84
+ const err2 = err_auth.fromContext({
85
+ invalid_credentials: { username: "bob" },
86
+ rate_limited: { retryAfter: 30 },
87
+ });
88
+
89
+ // Chain additional ids after construction
90
+ const err3 = err_auth.fromId("account_locked").addId("rate_limited");
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Reading errors
96
+
97
+ ```ts
98
+ // Check and narrow type
99
+ if (err1.hasId("invalid_credentials")) {
100
+ const { username } = err1.getContext("invalid_credentials"); // typed
101
+ }
102
+
103
+ // Check multiple ids at once
104
+ if (err2.hasOneOfIds(["invalid_credentials", "rate_limited"])) {
105
+ // ACTIVE_IDS narrowed to that subset
106
+ }
107
+
108
+ // Pattern match — calls first matching handler, returns its result
109
+ import { matchFirst } from "@nice-code/error";
110
+
111
+ const message = matchFirst(err1, {
112
+ invalid_credentials: ({ username }) => `Wrong password for ${username}`,
113
+ account_locked: () => "Account locked",
114
+ _: () => "Unknown auth error",
115
+ });
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Domain hierarchies
121
+
122
+ ```ts
123
+ const err_app = defineNiceError({ domain: "err_app", schema: {} } as const);
124
+
125
+ const err_auth = err_app.createChildDomain({
126
+ domain: "err_auth",
127
+ schema: { /* ... */ },
128
+ });
129
+
130
+ const err_auth_registration = err_auth.createChildDomain({
131
+ domain: "err_auth_registration",
132
+ schema: { /* ... */ },
133
+ });
134
+
135
+ // Ancestry checks
136
+ err_app.isParentOf(err_auth_registration); // true
137
+ err_auth.isParentOf(err_auth_registration); // true
138
+ err_auth_registration.isParentOf(err_auth); // false
139
+
140
+ // Type guard — exact domain match only
141
+ err_auth.isExact(someError); // true only for err_auth domain
142
+ err_auth.isThisOrChild(someError); // true for err_auth and all children
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Serialization & transport
148
+
149
+ ```ts
150
+ // Serialize to a plain JSON object (safe to send over HTTP)
151
+ const json = error.toJsonObject();
152
+ const str = error.toJsonString();
153
+ const response = error.toHttpResponse(); // Response with correct HTTP status
154
+
155
+ // On the receiving side — reconstruct from anything
156
+ import { castNiceError } from "@nice-code/error";
157
+
158
+ const caught = castNiceError(unknownValue); // always returns NiceError
159
+
160
+ if (err_auth.isExact(caught)) {
161
+ const hydrated = err_auth.hydrate(caught); // deserializes context
162
+ const { username } = hydrated.getContext("invalid_credentials");
163
+ }
164
+ ```
165
+
166
+ `castNiceError` handles: `NiceError` instances, serialized JSON objects, native `Error`, error-like objects, nullish values, and primitives.
167
+
168
+ ---
169
+
170
+ ## Error handling with handlers
171
+
172
+ ### Functional style (composable)
173
+
174
+ ```ts
175
+ import { forDomain, forId, forIds } from "@nice-code/error";
176
+
177
+ const handled = error.handleWithSync([
178
+ forId(err_auth, "invalid_credentials", (h) => {
179
+ const { username } = h.getContext("invalid_credentials");
180
+ return res.status(401).json({ error: `Bad credentials for ${username}` });
181
+ }),
182
+ forIds(err_auth, ["account_locked", "rate_limited"], (h) => {
183
+ return res.status(h.httpStatusCode).json({ error: h.message });
184
+ }),
185
+ forDomain(err_payment, (h) => {
186
+ return res.status(500).json({ error: "Payment failed" });
187
+ }),
188
+ ]);
189
+
190
+ // Async handlers
191
+ await error.handleWithAsync([
192
+ forDomain(err_payment, async (h) => {
193
+ await db.logFailure(h.message);
194
+ await notify(h.toJsonObject());
195
+ }),
196
+ ]);
197
+ ```
198
+
199
+ ### Class-based style (reusable handler)
200
+
201
+ ```ts
202
+ import { NiceErrorHandler } from "@nice-code/error";
203
+
204
+ const handler = new NiceErrorHandler()
205
+ .forId(err_auth, "invalid_credentials", (h) => "unauthorized")
206
+ .forDomain(err_payment, (h) => "payment_error")
207
+ .setDefaultHandler((e) => "unknown_error");
208
+
209
+ handler.handleErrorWithPromiseInspection(error);
210
+ ```
211
+
212
+ ---
213
+
214
+ ## Utility types
215
+
216
+ ```ts
217
+ import type { InferNiceError, InferNiceErrorHydrated } from "@nice-code/error";
218
+
219
+ type TAuthError = InferNiceError<typeof err_auth>;
220
+ // → NiceError<{ domain: "err_auth"; ... }, keyof schema>
221
+
222
+ type TAuthErrorHydrated = InferNiceErrorHydrated<typeof err_auth>;
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Packing for transport
228
+
229
+ Useful when errors must travel through systems that don't preserve custom properties (e.g. some RPC layers).
230
+
231
+ ```ts
232
+ import { EErrorPackType } from "@nice-code/error";
233
+
234
+ error.pack(EErrorPackType.msg_pack); // embeds JSON in error.message
235
+ error.pack(EErrorPackType.cause_pack); // embeds JSON in error.cause
236
+ error.unpack(); // restore original
237
+ ```
package/build/index.js CHANGED
@@ -1,19 +1,952 @@
1
- // src/index.ts
2
- export * from "./internal";
3
- export * from "./NiceError/NiceError";
4
- export * from "./NiceError/NiceError.types";
5
- export * from "./NiceError/NiceErrorHydrated";
6
- export * from "./NiceErrorDefined/defineNiceError";
7
- export * from "./NiceErrorDefined/err";
8
- export * from "./NiceErrorDefined/NiceErrorDefined";
9
- export * from "./NiceErrorHandler/handleWith";
10
- export * from "./NiceErrorHandler/NiceErrorHandler";
11
- export * from "./NiceErrorHandler/NiceErrorHandler.types";
12
- export * from "./utils/castAndHydrate";
13
- export * from "./utils/castNiceError";
14
- export * from "./utils/isNiceErrorObject";
15
- export * from "./utils/isRegularErrorObject";
16
- export * from "./utils/matchFirst";
17
- export * from "./utils/packError/causePack";
18
- export * from "./utils/packError/msgPack";
19
- export * from "./utils/packError/packError.enums";
1
+ // src/internal/nice_core_errors.ts
2
+ import { StatusCodes } from "http-status-codes";
3
+
4
+ // src/utils/jsErrorOrCastJsError.ts
5
+ function jsErrorOrCastJsError(error, logMessage = true) {
6
+ if (error instanceof Error) {
7
+ return Object.assign(error, {
8
+ message: error.message
9
+ });
10
+ }
11
+ const message = error?.message ?? (typeof error === "string" ? error : "No error message found");
12
+ if (logMessage) {
13
+ console.error(`An unknown and unstructured error was thrown: ${message}`, error);
14
+ }
15
+ return {
16
+ ...new Error(message),
17
+ ...error
18
+ };
19
+ }
20
+
21
+ // src/NiceError/nice_error.static.ts
22
+ var DUR_OBJ_PACK_PREFIX = "NE_DUROBJ[[";
23
+ var DUR_OBJ_PACK_SUFFIX = "]]NE_DUROBJ";
24
+
25
+ // src/utils/packError/packError.enums.ts
26
+ var EErrorPackType;
27
+ ((EErrorPackType2) => {
28
+ EErrorPackType2["no_pack"] = "no_pack";
29
+ EErrorPackType2["msg_pack"] = "msg_pack";
30
+ EErrorPackType2["cause_pack"] = "cause_pack";
31
+ })(EErrorPackType ||= {});
32
+
33
+ // src/utils/packError/causePack.ts
34
+ var causePack = (error) => {
35
+ error._packedState = { cause: error.cause, packedAs: "cause_pack" /* cause_pack */ };
36
+ error.cause = `${DUR_OBJ_PACK_PREFIX}${JSON.stringify(error.toJsonObject())}${DUR_OBJ_PACK_SUFFIX}`;
37
+ return error;
38
+ };
39
+
40
+ // src/utils/packError/msgPack.ts
41
+ var msgPack = (error) => {
42
+ error._packedState = { message: error.cleanMessage, packedAs: "msg_pack" /* msg_pack */ };
43
+ error.message = `${DUR_OBJ_PACK_PREFIX}${JSON.stringify(error.toJsonObject())}${DUR_OBJ_PACK_SUFFIX}`;
44
+ return error;
45
+ };
46
+
47
+ // src/utils/packError/packError.ts
48
+ var packError = (error, packType = "msg_pack") => {
49
+ if (packType === "no_pack") {
50
+ return error;
51
+ }
52
+ if (packType === "msg_pack") {
53
+ return msgPack(error);
54
+ }
55
+ return causePack(error);
56
+ };
57
+
58
+ // src/NiceError/NiceError.ts
59
+ class NiceError extends Error {
60
+ name = "NiceError";
61
+ def;
62
+ ids;
63
+ wasntNice;
64
+ httpStatusCode;
65
+ timeCreated;
66
+ cleanMessage;
67
+ originError;
68
+ _packedState;
69
+ _errorDataMap;
70
+ constructor(options) {
71
+ const messagePure = options.message;
72
+ const prefixedMessage = `[${options.def.domain}](${options.ids.join(",")}) ${messagePure}`;
73
+ super(prefixedMessage);
74
+ this.cleanMessage = messagePure;
75
+ this.def = options.def;
76
+ this.ids = options.ids;
77
+ this._errorDataMap = options.errorData;
78
+ this.wasntNice = options.wasntNice ?? false;
79
+ this.httpStatusCode = options.httpStatusCode ?? 500;
80
+ if (options.originError != null) {
81
+ this.originError = options.originError;
82
+ }
83
+ this.timeCreated = options.timeCreated ?? Date.now();
84
+ }
85
+ hasId(id) {
86
+ return id in this._errorDataMap;
87
+ }
88
+ hasOneOfIds(ids) {
89
+ return ids.some((id) => (id in this._errorDataMap));
90
+ }
91
+ get hasMultiple() {
92
+ return Object.keys(this._errorDataMap).length > 1;
93
+ }
94
+ getIds() {
95
+ return Object.keys(this._errorDataMap);
96
+ }
97
+ getContext(id) {
98
+ const errorData = this._errorDataMap[id];
99
+ const state = errorData?.contextState;
100
+ if (state == null) {
101
+ return;
102
+ }
103
+ if (state.kind === "unhydrated") {
104
+ throw new Error(`[NiceError.getContext] Context for id "${String(id)}" is in the "unhydrated" state. ` + `The error was reconstructed from a serialized payload but has not been deserialized yet. ` + `Call \`niceErrorDefined.hydrate(error)\` to reconstruct the typed context.`);
105
+ }
106
+ return state.value;
107
+ }
108
+ getErrorDataForId(id) {
109
+ return this._errorDataMap[id];
110
+ }
111
+ withOriginError(error) {
112
+ this.originError = jsErrorOrCastJsError(error);
113
+ if (this._packedState?.packedAs !== "cause_pack" /* cause_pack */) {
114
+ this.cause = this.originError;
115
+ }
116
+ return this;
117
+ }
118
+ matches(other) {
119
+ const myDef = this.def;
120
+ const otherDef = other.def;
121
+ if (myDef.domain !== otherDef.domain)
122
+ return false;
123
+ const myIds = this.getIds().map(String).sort();
124
+ const otherIds = other.getIds().map(String).sort();
125
+ if (myIds.length !== otherIds.length)
126
+ return false;
127
+ return myIds.every((id, i) => id === otherIds[i]);
128
+ }
129
+ toJsonObject() {
130
+ const originError = this.originError ? {
131
+ name: this.originError.name,
132
+ message: this.originError.cleanMessage ?? this.originError.message,
133
+ stack: this.originError.stack,
134
+ cause: this.originError.cause
135
+ } : undefined;
136
+ const def = {
137
+ domain: this.def.domain,
138
+ allDomains: this.def.allDomains
139
+ };
140
+ if (this.def.defaultHttpStatusCode != null) {
141
+ def["defaultHttpStatusCode"] = this.def.defaultHttpStatusCode;
142
+ }
143
+ if (this.def.defaultMessage != null) {
144
+ def["defaultMessage"] = this.def.defaultMessage;
145
+ }
146
+ const errorData = {};
147
+ for (const rawId of Object.keys(this._errorDataMap)) {
148
+ const id = rawId;
149
+ const data = this._errorDataMap[id];
150
+ if (data == null)
151
+ continue;
152
+ let contextState;
153
+ if (data.contextState.kind === "hydrated" /* hydrated */) {
154
+ contextState = {
155
+ kind: "unhydrated" /* unhydrated */,
156
+ serialized: data.contextState.serialized
157
+ };
158
+ } else {
159
+ contextState = data.contextState;
160
+ }
161
+ errorData[id] = {
162
+ contextState,
163
+ message: data.cleanMessage ?? data.message,
164
+ httpStatusCode: data.httpStatusCode,
165
+ timeAdded: data.timeAdded
166
+ };
167
+ }
168
+ return {
169
+ name: "NiceError",
170
+ def,
171
+ ids: this.ids,
172
+ errorData,
173
+ wasntNice: this.wasntNice,
174
+ message: this.cleanMessage,
175
+ httpStatusCode: this.httpStatusCode,
176
+ timeCreated: this.timeCreated,
177
+ ...this.stack != null ? { stack: this.stack } : {},
178
+ originError
179
+ };
180
+ }
181
+ toJsonString() {
182
+ return JSON.stringify(this.toJsonObject());
183
+ }
184
+ toHttpResponse() {
185
+ return new Response(this.toJsonString(), {
186
+ status: this.httpStatusCode,
187
+ headers: { "Content-Type": "application/json" }
188
+ });
189
+ }
190
+ hydrate(definedNiceError) {
191
+ return definedNiceError.hydrate(this);
192
+ }
193
+ handleWithSync(handlerInput, handlerOptions = {}) {
194
+ const handlersArray = Array.isArray(handlerInput) ? handlerInput : [handlerInput];
195
+ for (const handler of handlersArray) {
196
+ const result = handler.handleErrorWithPromiseInspection(this, handlerOptions);
197
+ if (result.matched) {
198
+ if (result.isPromise) {
199
+ console.warn(`[NiceError.handleWith] Handler ${result.target.identifier} returned a Promise but was called via \`handleWith\` (synchronous). ` + `The Promise will not be awaited. Use \`handleWithAsync\` for async handlers.`);
200
+ }
201
+ return result.isPromise ? undefined : result.handlerResponse;
202
+ }
203
+ }
204
+ if (handlerOptions.throwOnUnhandled === true)
205
+ throw this;
206
+ return;
207
+ }
208
+ async handleWithAsync(handlerInput, handlerOptions = {}) {
209
+ const handlersArray = Array.isArray(handlerInput) ? handlerInput : [handlerInput];
210
+ for (const handler of handlersArray) {
211
+ const result = handler.handleErrorWithPromiseInspection(this, handlerOptions);
212
+ if (result.matched) {
213
+ return result.isPromise ? await result.handlerPromise : result.handlerResponse;
214
+ }
215
+ }
216
+ if (handlerOptions.throwOnUnhandled === true)
217
+ throw this;
218
+ return;
219
+ }
220
+ get isPacked() {
221
+ return this._packedState != null;
222
+ }
223
+ pack(packType = "msg_pack") {
224
+ if (this.isPacked)
225
+ return this;
226
+ return packError(this, packType);
227
+ }
228
+ unpack() {
229
+ if (this._packedState == null)
230
+ return this;
231
+ if (this._packedState.packedAs === "msg_pack" /* msg_pack */) {
232
+ this.message = this._packedState.message;
233
+ }
234
+ if (this._packedState.packedAs === "cause_pack" /* cause_pack */) {
235
+ this.cause = this._packedState.cause;
236
+ }
237
+ this._packedState = undefined;
238
+ delete this._packedState;
239
+ return this;
240
+ }
241
+ }
242
+
243
+ // src/NiceError/NiceErrorHydrated.ts
244
+ class NiceErrorHydrated extends NiceError {
245
+ def;
246
+ niceErrorDefined;
247
+ constructor(options) {
248
+ super(options);
249
+ this.def = options.def;
250
+ this.niceErrorDefined = options.niceErrorDefined;
251
+ }
252
+ addContext(context) {
253
+ const newIds = Object.keys(context);
254
+ const newErrorData = {};
255
+ for (const id of newIds) {
256
+ newErrorData[id] = this.niceErrorDefined.reconcileErrorDataForId(id, context[id]);
257
+ }
258
+ const mergedErrorData = {
259
+ ...this._errorDataMap,
260
+ ...newErrorData
261
+ };
262
+ const mergedIds = Array.from(new Set([...this.getIds(), ...Object.keys(context)]));
263
+ return new NiceErrorHydrated({
264
+ def: this.def,
265
+ niceErrorDefined: this.niceErrorDefined,
266
+ ids: mergedIds,
267
+ errorData: mergedErrorData,
268
+ message: this.cleanMessage,
269
+ wasntNice: this.wasntNice,
270
+ httpStatusCode: this.httpStatusCode,
271
+ originError: this.originError
272
+ });
273
+ }
274
+ addId(...args) {
275
+ const [id, context] = args;
276
+ const reconciledData = this.niceErrorDefined.reconcileErrorDataForId(id, context);
277
+ const errorDataMap = {};
278
+ errorDataMap[id] = reconciledData;
279
+ const mergedContexts = {
280
+ ...this._errorDataMap,
281
+ ...errorDataMap
282
+ };
283
+ const mergedIds = Array.from(new Set([...this.getIds(), id]));
284
+ return new NiceErrorHydrated({
285
+ def: this.def,
286
+ niceErrorDefined: this.niceErrorDefined,
287
+ ids: mergedIds,
288
+ errorData: mergedContexts,
289
+ message: this.cleanMessage,
290
+ wasntNice: this.wasntNice,
291
+ httpStatusCode: this.httpStatusCode,
292
+ originError: this.originError
293
+ });
294
+ }
295
+ }
296
+
297
+ // src/NiceErrorDefined/NiceErrorDefined.ts
298
+ class NiceErrorDomain {
299
+ domain;
300
+ allDomains;
301
+ defaultHttpStatusCode;
302
+ defaultMessage;
303
+ _schema;
304
+ _definedChildNiceErrors = [];
305
+ _definedParentNiceError;
306
+ _setPack;
307
+ _packAsFn;
308
+ constructor(definition) {
309
+ this.domain = definition.domain;
310
+ this.allDomains = definition.allDomains;
311
+ this._schema = definition.schema;
312
+ if (definition.packAs != null) {
313
+ this._packAsFn = definition.packAs;
314
+ }
315
+ if (definition.defaultHttpStatusCode != null) {
316
+ this.defaultHttpStatusCode = definition.defaultHttpStatusCode;
317
+ }
318
+ if (definition.defaultMessage != null) {
319
+ this.defaultMessage = definition.defaultMessage;
320
+ }
321
+ }
322
+ createChildDomain(subErrorDef) {
323
+ const child = new NiceErrorDomain({
324
+ domain: subErrorDef.domain,
325
+ allDomains: [subErrorDef.domain, ...this.allDomains],
326
+ schema: subErrorDef.schema,
327
+ defaultHttpStatusCode: subErrorDef.defaultHttpStatusCode,
328
+ defaultMessage: subErrorDef.defaultMessage
329
+ });
330
+ this.addChildNiceErrorDefined(child);
331
+ child.addParentNiceErrorDefined(this);
332
+ if (subErrorDef.packAs != null) {
333
+ child._packAsFn = subErrorDef.packAs;
334
+ } else if (this._setPack) {
335
+ child.packAs(this._setPack);
336
+ } else if (this._packAsFn) {
337
+ child._packAsFn = this._packAsFn;
338
+ }
339
+ return child;
340
+ }
341
+ addParentNiceErrorDefined(parentError) {
342
+ if (this._definedParentNiceError?.domain === parentError.domain) {
343
+ return;
344
+ }
345
+ this._definedParentNiceError = {
346
+ domain: parentError.domain,
347
+ definedError: parentError
348
+ };
349
+ }
350
+ addChildNiceErrorDefined(child) {
351
+ if (this._definedChildNiceErrors.some((linked) => linked.domain === child.domain)) {
352
+ return;
353
+ }
354
+ this._definedChildNiceErrors.push({
355
+ domain: child.domain,
356
+ definedError: child
357
+ });
358
+ if (this._definedParentNiceError) {
359
+ this._definedParentNiceError.definedError.addChildNiceErrorDefined(child);
360
+ }
361
+ }
362
+ packAs(pack) {
363
+ this._setPack = pack;
364
+ return this;
365
+ }
366
+ createError(input) {
367
+ const err = new NiceErrorHydrated(input);
368
+ const packType = this._setPack ?? this._packAsFn?.();
369
+ if (packType != null && packType !== "no_pack") {
370
+ return err.pack(packType);
371
+ }
372
+ return err;
373
+ }
374
+ hydrate(error) {
375
+ const errDef = error.def;
376
+ if (errDef.domain !== this.domain) {
377
+ throw new Error(`[NiceErrorDefined.hydrate] Domain mismatch: this definition is "${this.domain}" ` + `but the error belongs to "${errDef.domain}". ` + `Call \`niceErrorDefined.is(error)\` before hydrating to ensure compatibility.`);
378
+ }
379
+ const finalError = error instanceof NiceError ? error : new NiceError(error);
380
+ const reconciledErrorData = {};
381
+ for (const id of finalError.getIds()) {
382
+ const existingData = finalError.getErrorDataForId(id);
383
+ if (existingData == null)
384
+ continue;
385
+ let contextState = existingData.contextState;
386
+ if (contextState.kind === "unhydrated") {
387
+ const entry = this._schema[id];
388
+ const deserialize = entry?.context?.serialization?.fromJsonSerializable;
389
+ if (deserialize != null) {
390
+ contextState = {
391
+ kind: "hydrated" /* hydrated */,
392
+ value: deserialize(contextState.serialized),
393
+ serialized: contextState.serialized
394
+ };
395
+ }
396
+ }
397
+ reconciledErrorData[id] = {
398
+ contextState,
399
+ message: existingData.cleanMessage ?? existingData.message,
400
+ httpStatusCode: existingData.httpStatusCode,
401
+ timeAdded: existingData.timeAdded
402
+ };
403
+ }
404
+ return new NiceErrorHydrated({
405
+ def: this._buildDef(),
406
+ niceErrorDefined: this,
407
+ ids: finalError.ids,
408
+ errorData: reconciledErrorData,
409
+ message: finalError.cleanMessage ?? finalError.message,
410
+ httpStatusCode: finalError.httpStatusCode,
411
+ wasntNice: finalError.wasntNice,
412
+ originError: finalError.originError,
413
+ timeCreated: finalError.timeCreated
414
+ });
415
+ }
416
+ fromId(...args) {
417
+ const [id, context] = args;
418
+ const reconciledData = this.reconcileErrorDataForId(id, context);
419
+ const errorData = {};
420
+ errorData[id] = reconciledData;
421
+ return this.createError({
422
+ def: this._buildDef(),
423
+ niceErrorDefined: this,
424
+ ids: [id],
425
+ errorData,
426
+ message: reconciledData.cleanMessage ?? reconciledData.message,
427
+ httpStatusCode: reconciledData.httpStatusCode
428
+ });
429
+ }
430
+ fromContext(context) {
431
+ const ids = Object.keys(context);
432
+ if (ids.length === 0) {
433
+ throw new Error("[NiceErrorDefined.fromContext] context object must contain at least one error id.");
434
+ }
435
+ const errorData = {};
436
+ for (const id of ids) {
437
+ errorData[id] = this.reconcileErrorDataForId(id, context[id]);
438
+ }
439
+ const primaryId = ids[0];
440
+ return this.createError({
441
+ def: this._buildDef(),
442
+ niceErrorDefined: this,
443
+ ids,
444
+ errorData,
445
+ message: errorData[primaryId].cleanMessage ?? errorData[primaryId].message,
446
+ httpStatusCode: errorData[primaryId].httpStatusCode
447
+ });
448
+ }
449
+ isExact(error) {
450
+ if (!(error instanceof NiceError))
451
+ return false;
452
+ const errDef = error.def;
453
+ return errDef.domain === this.domain;
454
+ }
455
+ isThisOrChild(error) {
456
+ if (!(error instanceof NiceError))
457
+ return false;
458
+ const errDef = error.def;
459
+ return errDef.domain === this.domain || this.allDomains.includes(errDef.domain);
460
+ }
461
+ isParentOf(target) {
462
+ const allDomains = target instanceof NiceError ? target.def.allDomains : target.allDomains;
463
+ return Array.isArray(allDomains) && allDomains.includes(this.domain);
464
+ }
465
+ _buildDef() {
466
+ return {
467
+ domain: this.domain,
468
+ allDomains: this.allDomains,
469
+ schema: this._schema
470
+ };
471
+ }
472
+ _resolveMessage(id, context) {
473
+ const entry = this._schema[id];
474
+ if (typeof entry?.message === "function") {
475
+ return entry.message(context);
476
+ }
477
+ if (typeof entry?.message === "string") {
478
+ return entry.message;
479
+ }
480
+ return this.defaultMessage ?? `[${this.domain}::${id}] An error occurred.`;
481
+ }
482
+ _resolveHttpStatusCode(id, context) {
483
+ const entry = this._schema[id];
484
+ let httpStatusCode;
485
+ if (typeof entry?.httpStatusCode === "function") {
486
+ httpStatusCode = entry.httpStatusCode(context);
487
+ }
488
+ if (typeof entry?.httpStatusCode === "number") {
489
+ httpStatusCode = entry.httpStatusCode;
490
+ }
491
+ return typeof httpStatusCode === "number" ? httpStatusCode : this.defaultHttpStatusCode ?? 500;
492
+ }
493
+ reconcileErrorDataForId(id, context) {
494
+ const message = this._resolveMessage(id, context);
495
+ const httpStatusCode = this._resolveHttpStatusCode(id, context);
496
+ const entry = this._schema[id];
497
+ let contextState;
498
+ if (context != null && entry?.context?.serialization != null) {
499
+ const serialized = entry.context.serialization.toJsonSerializable(context);
500
+ contextState = { kind: "hydrated" /* hydrated */, value: context, serialized };
501
+ } else {
502
+ contextState = { kind: "serde_unset" /* serde_unset */, value: context };
503
+ }
504
+ return { contextState, message, httpStatusCode, timeAdded: Date.now() };
505
+ }
506
+ }
507
+
508
+ // src/NiceErrorDefined/defineNiceError.ts
509
+ var defineNiceError = (definition) => {
510
+ return new NiceErrorDomain({
511
+ domain: definition.domain,
512
+ allDomains: [definition.domain],
513
+ schema: definition.schema,
514
+ ...definition.packAs != null ? { packAs: definition.packAs } : {}
515
+ });
516
+ };
517
+
518
+ // src/NiceErrorDefined/err.ts
519
+ function err(meta) {
520
+ return meta ?? {};
521
+ }
522
+
523
+ // src/internal/nice_core_errors.ts
524
+ var err_nice = defineNiceError({
525
+ domain: "err_nice",
526
+ schema: {}
527
+ });
528
+ var EErrId_CastNotNice;
529
+ ((EErrId_CastNotNice2) => {
530
+ EErrId_CastNotNice2["js_error"] = "native_error";
531
+ EErrId_CastNotNice2["js_error_like_object"] = "js_error_like_object";
532
+ EErrId_CastNotNice2["nullish_value"] = "nullish_value";
533
+ EErrId_CastNotNice2["js_data_type"] = "js_data_type";
534
+ EErrId_CastNotNice2["js_other"] = "js_other";
535
+ })(EErrId_CastNotNice ||= {});
536
+ var err_cast_not_nice = err_nice.createChildDomain({
537
+ domain: "err_cast_not_nice",
538
+ defaultHttpStatusCode: StatusCodes.UNPROCESSABLE_ENTITY,
539
+ schema: {
540
+ ["native_error" /* js_error */]: err({
541
+ context: {
542
+ required: true
543
+ },
544
+ message: ({ jsError }) => `A native JavaScript Error was encountered during casting: ${jsError.message}`,
545
+ httpStatusCode: StatusCodes.INTERNAL_SERVER_ERROR
546
+ }),
547
+ ["js_error_like_object" /* js_error_like_object */]: err({
548
+ context: {
549
+ required: true
550
+ },
551
+ message: ({ jsErrorObject }) => `An object resembling a JavaScript Error was encountered during casting: [${jsErrorObject.name}] ${jsErrorObject.message}`,
552
+ httpStatusCode: StatusCodes.INTERNAL_SERVER_ERROR
553
+ }),
554
+ ["nullish_value" /* nullish_value */]: err({
555
+ context: {
556
+ required: true
557
+ },
558
+ message: ({ value }) => `A nullish value [${value === null ? "null" : "undefined"}] was encountered during casting`
559
+ }),
560
+ ["js_data_type" /* js_data_type */]: err({
561
+ context: {
562
+ required: true
563
+ },
564
+ message: ({ jsDataType, jsDataValue }) => {
565
+ let inspectedValue;
566
+ try {
567
+ inspectedValue = JSON.stringify(jsDataValue);
568
+ } catch {}
569
+ return `A value of type [${jsDataType}] with value [${inspectedValue ?? "UNSERIALIZABLE"}] was encountered during casting, which is not a valid error type`;
570
+ }
571
+ }),
572
+ ["js_other" /* js_other */]: err({
573
+ context: {
574
+ required: true
575
+ },
576
+ message: ({ jsDataValue }) => {
577
+ let inspectedValue;
578
+ try {
579
+ inspectedValue = JSON.stringify(jsDataValue);
580
+ } catch {}
581
+ return `An unhandled type [${typeof jsDataValue}] with value [${inspectedValue ?? "UNSERIALIZABLE"}] was encountered during casting, which is not a valid error type`;
582
+ }
583
+ })
584
+ }
585
+ });
586
+ var err_nice_handler = err_nice.createChildDomain({
587
+ domain: "err_nice_handler",
588
+ schema: {}
589
+ });
590
+ // src/NiceErrorHandler/NiceErrorHandler.types.ts
591
+ var EErrorHandlerTargetType;
592
+ ((EErrorHandlerTargetType2) => {
593
+ EErrorHandlerTargetType2["ids"] = "ids";
594
+ EErrorHandlerTargetType2["domain"] = "domain";
595
+ EErrorHandlerTargetType2["default"] = "default";
596
+ })(EErrorHandlerTargetType ||= {});
597
+
598
+ // src/NiceErrorHandler/NiceErrorHandler.ts
599
+ class NiceErrorHandler {
600
+ handlerConfigs = [];
601
+ _defaultRequester;
602
+ handleErrorWithPromiseInspection(error, options) {
603
+ for (const handlerConfig of this.handlerConfigs) {
604
+ if (!handlerConfig._matcher(error))
605
+ continue;
606
+ const errorResult = handlerConfig._requester(error);
607
+ if (errorResult instanceof Promise) {
608
+ return {
609
+ isPromise: true,
610
+ matched: true,
611
+ target: handlerConfig.target,
612
+ handlerPromise: errorResult
613
+ };
614
+ }
615
+ return {
616
+ isPromise: false,
617
+ matched: true,
618
+ target: handlerConfig.target,
619
+ handlerResponse: errorResult
620
+ };
621
+ }
622
+ if (this._defaultRequester) {
623
+ const defaultResult = this._defaultRequester(error);
624
+ if (defaultResult instanceof Promise) {
625
+ return {
626
+ isPromise: true,
627
+ matched: true,
628
+ target: {
629
+ type: "default" /* default */,
630
+ identifier: "[matched:default]"
631
+ },
632
+ handlerPromise: defaultResult
633
+ };
634
+ }
635
+ return {
636
+ isPromise: false,
637
+ matched: true,
638
+ target: { type: "default" /* default */, identifier: "[matched:default]" },
639
+ handlerResponse: defaultResult
640
+ };
641
+ }
642
+ if (options?.throwOnUnhandled === true) {
643
+ throw error;
644
+ }
645
+ return {
646
+ matched: false,
647
+ attemptedTargets: this.handlerConfigs.map((config) => config.target)
648
+ };
649
+ }
650
+ forDomain(domain, handler) {
651
+ this.handlerConfigs.push({
652
+ target: {
653
+ type: "domain" /* domain */,
654
+ domain: domain.domain,
655
+ identifier: `[matched:domain:${domain.domain}]`
656
+ },
657
+ _matcher: (error) => domain.isExact(error),
658
+ _requester: (error) => handler(domain.hydrate(error))
659
+ });
660
+ return this;
661
+ }
662
+ forId(domain, id, handler) {
663
+ this.handlerConfigs.push({
664
+ target: {
665
+ type: "ids" /* ids */,
666
+ domain: domain.domain,
667
+ ids: [id],
668
+ identifier: `[matched:ids:${domain.domain}:${id}]`
669
+ },
670
+ _matcher: (error) => domain.isExact(error) && error.hasId(id),
671
+ _requester: (error) => handler(domain.hydrate(error))
672
+ });
673
+ return this;
674
+ }
675
+ forIds(domain, ids, handler) {
676
+ this.handlerConfigs.push({
677
+ target: {
678
+ type: "ids" /* ids */,
679
+ domain: domain.domain,
680
+ ids,
681
+ identifier: `[matched:ids:${domain.domain}:${ids.join(",")}]`
682
+ },
683
+ _matcher: (error) => domain.isExact(error) && ids.some((id) => error.hasId(id)),
684
+ _requester: (error) => handler(domain.hydrate(error))
685
+ });
686
+ return this;
687
+ }
688
+ setDefaultHandler(handler) {
689
+ this._defaultRequester = handler;
690
+ return this;
691
+ }
692
+ }
693
+
694
+ // src/NiceErrorHandler/handleWith.ts
695
+ function forDomain(domain, handler) {
696
+ return new NiceErrorHandler().forDomain(domain, handler);
697
+ }
698
+ function forId(domain, id, handler) {
699
+ return new NiceErrorHandler().forId(domain, id, handler);
700
+ }
701
+ function forIds(domain, ids, handler) {
702
+ return new NiceErrorHandler().forIds(domain, ids, handler);
703
+ }
704
+ // src/utils/isNiceErrorObject.ts
705
+ function isNiceErrorObject(obj) {
706
+ if (typeof obj !== "object" || obj == null)
707
+ return false;
708
+ const o = obj;
709
+ if (o["name"] !== "NiceError" || typeof o["message"] !== "string" || typeof o["wasntNice"] !== "boolean" || typeof o["httpStatusCode"] !== "number") {
710
+ return false;
711
+ }
712
+ const def = o["def"];
713
+ if (typeof def !== "object" || def == null)
714
+ return false;
715
+ const d = def;
716
+ if (typeof d["domain"] !== "string" || !Array.isArray(d["allDomains"]))
717
+ return false;
718
+ const errorData = o["errorData"];
719
+ if (errorData != null) {
720
+ if (typeof errorData !== "object")
721
+ return false;
722
+ for (const entry of Object.values(errorData)) {
723
+ if (entry == null)
724
+ continue;
725
+ if (typeof entry !== "object")
726
+ return false;
727
+ const e = entry;
728
+ const state = e["contextState"];
729
+ if (state == null || typeof state !== "object")
730
+ return false;
731
+ const kind = state["kind"];
732
+ if (kind !== "serde_unset" /* serde_unset */ && kind !== "unhydrated" /* unhydrated */)
733
+ return false;
734
+ }
735
+ }
736
+ return true;
737
+ }
738
+
739
+ // src/utils/isRegularErrorObject.ts
740
+ function isRegularErrorJsonObject(obj) {
741
+ if (typeof obj !== "object" || obj == null)
742
+ return false;
743
+ const o = obj;
744
+ return typeof o["name"] === "string" && typeof o["message"] === "string";
745
+ }
746
+
747
+ // src/utils/logger.ts
748
+ import { Logger } from "tslog";
749
+ var logger_NiceError = new Logger({
750
+ name: "NiceErrorLogger"
751
+ });
752
+ var logger_NiceError_testing = logger_NiceError.getSubLogger({
753
+ name: "NiceErrorTestingLogger"
754
+ });
755
+
756
+ // src/utils/inspectPotentialError/inspectPotentialError.ts
757
+ function interpretMessagePackedError(parsedError) {
758
+ let packedErrorStr;
759
+ if (typeof parsedError.message === "string" && parsedError.message.includes(DUR_OBJ_PACK_PREFIX) && parsedError.message.includes(DUR_OBJ_PACK_SUFFIX)) {
760
+ packedErrorStr = parsedError.message;
761
+ }
762
+ if (typeof parsedError.cause === "string" && parsedError.cause.includes(DUR_OBJ_PACK_PREFIX) && parsedError.cause.includes(DUR_OBJ_PACK_SUFFIX)) {
763
+ packedErrorStr = parsedError.cause;
764
+ }
765
+ if (packedErrorStr != null) {
766
+ const jsonStr = packedErrorStr.split(DUR_OBJ_PACK_PREFIX)[1].split(DUR_OBJ_PACK_SUFFIX)[0];
767
+ try {
768
+ const errorObj = JSON.parse(jsonStr);
769
+ if (isNiceErrorObject(errorObj)) {
770
+ return {
771
+ type: "niceErrorObject" /* niceErrorObject */,
772
+ niceErrorObject: errorObj
773
+ };
774
+ }
775
+ } catch {}
776
+ }
777
+ return null;
778
+ }
779
+ var inspectPotentialError = (potentialError) => {
780
+ if (potentialError == null) {
781
+ return {
782
+ type: "nullish" /* nullish */,
783
+ value: potentialError
784
+ };
785
+ }
786
+ if (typeof potentialError === "number") {
787
+ return {
788
+ type: "jsDataType" /* jsDataType */,
789
+ jsDataType: "number",
790
+ jsDataValue: potentialError
791
+ };
792
+ }
793
+ if (typeof potentialError === "boolean") {
794
+ return {
795
+ type: "jsDataType" /* jsDataType */,
796
+ jsDataType: "boolean",
797
+ jsDataValue: potentialError
798
+ };
799
+ }
800
+ let parsedError = potentialError;
801
+ if (typeof potentialError === "string") {
802
+ if (potentialError.includes("{") && potentialError.includes("name")) {
803
+ try {
804
+ parsedError = JSON.parse(potentialError);
805
+ } catch {
806
+ return {
807
+ type: "jsDataType" /* jsDataType */,
808
+ jsDataType: "string",
809
+ jsDataValue: potentialError
810
+ };
811
+ }
812
+ } else {
813
+ return {
814
+ type: "jsDataType" /* jsDataType */,
815
+ jsDataType: "string",
816
+ jsDataValue: potentialError
817
+ };
818
+ }
819
+ }
820
+ if (typeof parsedError !== "object" || parsedError == null) {
821
+ logger_NiceError.warn({
822
+ message: "Received a potential error that is a primitive data type other than string, number, or boolean. This is unexpected and may indicate an issue with error handling in the code.",
823
+ potentialError
824
+ });
825
+ return {
826
+ jsDataValue: potentialError,
827
+ type: "jsOther" /* jsOther */
828
+ };
829
+ }
830
+ if (parsedError instanceof NiceError) {
831
+ return {
832
+ type: "niceError" /* niceError */,
833
+ niceError: parsedError
834
+ };
835
+ }
836
+ if (isNiceErrorObject(parsedError)) {
837
+ return {
838
+ type: "niceErrorObject" /* niceErrorObject */,
839
+ niceErrorObject: parsedError
840
+ };
841
+ }
842
+ if (parsedError instanceof Error) {
843
+ const durObjResult = interpretMessagePackedError(parsedError);
844
+ if (durObjResult != null) {
845
+ return durObjResult;
846
+ }
847
+ return {
848
+ type: "jsError" /* jsError */,
849
+ jsError: parsedError
850
+ };
851
+ }
852
+ if (isRegularErrorJsonObject(parsedError)) {
853
+ const durObjResult = interpretMessagePackedError(parsedError);
854
+ if (durObjResult != null) {
855
+ return durObjResult;
856
+ }
857
+ return {
858
+ type: "jsErrorObject" /* jsErrorObject */,
859
+ jsErrorObject: parsedError
860
+ };
861
+ }
862
+ return {
863
+ type: "jsDataType" /* jsDataType */,
864
+ jsDataType: "object",
865
+ jsDataValue: parsedError
866
+ };
867
+ };
868
+
869
+ // src/utils/castNiceError.ts
870
+ var castNiceError = (error) => {
871
+ const inspected = inspectPotentialError(error);
872
+ switch (inspected.type) {
873
+ case "niceError" /* niceError */:
874
+ return inspected.niceError;
875
+ case "niceErrorObject" /* niceErrorObject */: {
876
+ const obj = inspected.niceErrorObject;
877
+ return new NiceError(obj);
878
+ }
879
+ case "jsError" /* jsError */: {
880
+ return err_cast_not_nice.fromContext({
881
+ ["native_error" /* js_error */]: inspected
882
+ }).withOriginError(inspected.jsError);
883
+ }
884
+ case "jsErrorObject" /* jsErrorObject */: {
885
+ const err2 = err_cast_not_nice.fromContext({
886
+ ["js_error_like_object" /* js_error_like_object */]: inspected
887
+ });
888
+ err2.cause = inspected.jsErrorObject;
889
+ return err2;
890
+ }
891
+ case "nullish" /* nullish */:
892
+ return err_cast_not_nice.fromContext({
893
+ ["nullish_value" /* nullish_value */]: inspected
894
+ });
895
+ case "jsDataType" /* jsDataType */: {
896
+ return err_cast_not_nice.fromContext({
897
+ ["js_data_type" /* js_data_type */]: inspected
898
+ });
899
+ }
900
+ default:
901
+ return err_cast_not_nice.fromContext({
902
+ ["js_other" /* js_other */]: inspected
903
+ });
904
+ }
905
+ };
906
+
907
+ // src/utils/castAndHydrate.ts
908
+ function castAndHydrate(value, niceErrorDefined) {
909
+ const casted = castNiceError(value);
910
+ if (niceErrorDefined.isExact(casted)) {
911
+ return niceErrorDefined.hydrate(casted);
912
+ }
913
+ return casted;
914
+ }
915
+ // src/utils/matchFirst.ts
916
+ function matchFirst(error, handlers) {
917
+ for (const id of error.getIds()) {
918
+ const handler = handlers[id];
919
+ if (typeof handler === "function") {
920
+ const context = error.getContext(id);
921
+ return handler(context);
922
+ }
923
+ }
924
+ if (typeof handlers._ === "function") {
925
+ return handlers._();
926
+ }
927
+ return;
928
+ }
929
+ export {
930
+ msgPack,
931
+ matchFirst,
932
+ isRegularErrorJsonObject,
933
+ isNiceErrorObject,
934
+ forIds,
935
+ forId,
936
+ forDomain,
937
+ err_nice_handler,
938
+ err_nice,
939
+ err_cast_not_nice,
940
+ err,
941
+ defineNiceError,
942
+ causePack,
943
+ castNiceError,
944
+ castAndHydrate,
945
+ NiceErrorHydrated,
946
+ NiceErrorHandler,
947
+ NiceErrorDomain,
948
+ NiceError,
949
+ EErrorPackType,
950
+ EErrorHandlerTargetType,
951
+ EErrId_CastNotNice
952
+ };
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "module": "build/index.js",
5
5
  "types": "build/types/index.d.ts",
6
6
  "type": "module",
7
- "version": "0.2.9",
7
+ "version": "0.2.11",
8
8
  "exports": {
9
9
  ".": {
10
10
  "types": "./build/types/index.d.ts",