silgi 0.51.7 → 0.51.8
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 +47 -0
- package/dist/adapters/_fetch-adapter.d.mts +6 -0
- package/dist/adapters/_fetch-adapter.mjs +14 -8
- package/dist/adapters/astro.mjs +1 -1
- package/dist/adapters/nextjs.mjs +1 -1
- package/dist/adapters/remix.mjs +1 -1
- package/dist/adapters/solidstart.mjs +1 -1
- package/dist/adapters/sveltekit.mjs +1 -1
- package/dist/client/client.d.mts +42 -4
- package/dist/client/client.mjs +42 -4
- package/dist/client/server.d.mts +27 -2
- package/dist/client/server.mjs +27 -2
- package/dist/compile.d.mts +10 -1
- package/dist/compile.mjs +13 -4
- package/dist/core/context-bridge.d.mts +49 -0
- package/dist/core/context-bridge.mjs +43 -7
- package/dist/core/context.d.mts +26 -0
- package/dist/core/ctx-symbols.mjs +21 -0
- package/dist/core/error.d.mts +183 -2
- package/dist/core/error.mjs +259 -16
- package/dist/core/handler.d.mts +15 -1
- package/dist/core/handler.mjs +33 -17
- package/dist/core/schema-converter.d.mts +131 -0
- package/dist/core/schema-converter.mjs +82 -0
- package/dist/core/serve.d.mts +2 -2
- package/dist/core/serve.mjs +9 -2
- package/dist/core/task.mjs +2 -2
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +4 -2
- package/dist/integrations/better-auth/index.d.mts +22 -1
- package/dist/integrations/better-auth/index.mjs +79 -11
- package/dist/integrations/drizzle/index.mjs +22 -5
- package/dist/integrations/zod/converter.d.mts +1 -1
- package/dist/integrations/zod/index.d.mts +29 -2
- package/dist/integrations/zod/index.mjs +60 -1
- package/dist/lazy.d.mts +40 -3
- package/dist/lazy.mjs +40 -3
- package/dist/map-input.mjs +1 -1
- package/dist/plugins/analytics/collector.d.mts +1 -1
- package/dist/plugins/analytics/trace.mjs +1 -1
- package/dist/plugins/analytics/types.d.mts +3 -3
- package/dist/plugins/analytics/utils.mjs +1 -4
- package/dist/plugins/analytics.d.mts +5 -3
- package/dist/plugins/analytics.mjs +16 -29
- package/dist/plugins/cache.mjs +1 -1
- package/dist/plugins/coerce.mjs +1 -1
- package/dist/scalar.d.mts +2 -1
- package/dist/scalar.mjs +9 -30
- package/dist/silgi.d.mts +165 -18
- package/dist/silgi.mjs +47 -11
- package/package.json +6 -2
- package/dist/core/trace-map.d.mts +0 -13
- package/dist/core/trace-map.mjs +0 -13
package/dist/core/error.d.mts
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
1
|
//#region src/core/error.d.ts
|
|
2
2
|
/**
|
|
3
|
-
* SilgiError — unified RPC error
|
|
3
|
+
* SilgiError — unified RPC error type for the Silgi framework.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Every error thrown through the Silgi request pipeline is either a
|
|
7
|
+
* `SilgiError` or is wrapped into one by {@link toSilgiError} before it
|
|
8
|
+
* reaches the wire. The class carries an HTTP `status`, a machine-readable
|
|
9
|
+
* `code`, optional structured `data`, and a `defined` flag that signals
|
|
10
|
+
* whether the error was deliberately declared with `$errors()` (safe to
|
|
11
|
+
* expose verbatim to clients) or is an internal fault that should be
|
|
12
|
+
* redacted.
|
|
13
|
+
*
|
|
14
|
+
* ### Cross-realm `instanceof`
|
|
15
|
+
* `SilgiError` overrides `Symbol.hasInstance` to check a brand symbol
|
|
16
|
+
* (`Symbol.for('silgi.error.brand.v1')`) stamped on `SilgiError.prototype`.
|
|
17
|
+
* Because `Symbol.for` resolves through the global symbol registry shared
|
|
18
|
+
* across all V8 realms (worker threads, `node:vm` contexts), `instanceof
|
|
19
|
+
* SilgiError` works even when the `SilgiError` class used to construct the
|
|
20
|
+
* error originated in a different realm.
|
|
21
|
+
*
|
|
22
|
+
* ### Subclassing
|
|
23
|
+
* User-defined subclasses are fully supported:
|
|
24
|
+
* ```ts
|
|
25
|
+
* class AuthError extends SilgiError<'UNAUTHORIZED'> {
|
|
26
|
+
* constructor() { super('UNAUTHORIZED', { status: 401 }) }
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
* The brand is inherited through the prototype chain automatically — no
|
|
30
|
+
* extra steps are required in the subclass constructor.
|
|
31
|
+
*
|
|
32
|
+
* @category Errors
|
|
4
33
|
*/
|
|
34
|
+
/** Well-known HTTP error codes with their default status and message. */
|
|
5
35
|
declare const COMMON_ERRORS: Readonly<{
|
|
6
36
|
readonly BAD_REQUEST: {
|
|
7
37
|
readonly status: 400;
|
|
@@ -73,6 +103,7 @@ declare const COMMON_ERRORS: Readonly<{
|
|
|
73
103
|
};
|
|
74
104
|
}>;
|
|
75
105
|
type SilgiErrorCode = keyof typeof COMMON_ERRORS | (string & {});
|
|
106
|
+
/** Options passed to the {@link SilgiError} constructor. */
|
|
76
107
|
interface SilgiErrorOptions<TData = unknown> {
|
|
77
108
|
status?: number;
|
|
78
109
|
message?: string;
|
|
@@ -80,6 +111,7 @@ interface SilgiErrorOptions<TData = unknown> {
|
|
|
80
111
|
cause?: unknown;
|
|
81
112
|
defined?: boolean;
|
|
82
113
|
}
|
|
114
|
+
/** Wire-format representation of a {@link SilgiError}, safe to JSON-stringify and transmit to clients. */
|
|
83
115
|
interface SilgiErrorJSON<TCode extends string = string, TData = unknown> {
|
|
84
116
|
defined: boolean;
|
|
85
117
|
code: TCode;
|
|
@@ -87,18 +119,167 @@ interface SilgiErrorJSON<TCode extends string = string, TData = unknown> {
|
|
|
87
119
|
message: string;
|
|
88
120
|
data: TData;
|
|
89
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* A typed, serializable RPC error.
|
|
124
|
+
*
|
|
125
|
+
* @typeParam TCode - String literal type for the error code.
|
|
126
|
+
* @typeParam TData - Shape of the optional structured data payload.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* throw new SilgiError('NOT_FOUND', { message: 'User 42 not found' })
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
133
|
+
* @example Checking for Silgi errors in a catch block
|
|
134
|
+
* ```ts
|
|
135
|
+
* import { isSilgiError } from 'silgi'
|
|
136
|
+
*
|
|
137
|
+
* try {
|
|
138
|
+
* await client.users.get({ id: '42' })
|
|
139
|
+
* } catch (e) {
|
|
140
|
+
* if (isSilgiError(e)) {
|
|
141
|
+
* console.error(e.code, e.status)
|
|
142
|
+
* }
|
|
143
|
+
* }
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* @see {@link isSilgiError} Preferred type-guard over `instanceof` for cross-realm safety.
|
|
147
|
+
* @see {@link toSilgiError} Wraps any unknown error into a `SilgiError`.
|
|
148
|
+
* @see {@link fromSilgiErrorJSON} Reconstructs a `SilgiError` from a wire-format JSON object.
|
|
149
|
+
*
|
|
150
|
+
* @remarks
|
|
151
|
+
* **Do not mutate the prototype brand.** Removing or overwriting
|
|
152
|
+
* `SilgiError.prototype[Symbol.for('silgi.error.brand.v1')]` will break
|
|
153
|
+
* cross-realm detection for all instances in the current realm.
|
|
154
|
+
*
|
|
155
|
+
* **Subclass note.** If a subclass overrides the constructor and somehow
|
|
156
|
+
* replaces `Object.getPrototypeOf(this)` before construction completes,
|
|
157
|
+
* the inherited brand may be severed. This is an extremely unusual pattern
|
|
158
|
+
* and is not supported.
|
|
159
|
+
*
|
|
160
|
+
* @category Errors
|
|
161
|
+
*/
|
|
90
162
|
declare class SilgiError<TCode extends string = string, TData = unknown> extends Error {
|
|
163
|
+
/** Machine-readable error code (e.g. `'NOT_FOUND'`). */
|
|
91
164
|
readonly code: TCode;
|
|
165
|
+
/** HTTP status code associated with this error. */
|
|
92
166
|
readonly status: number;
|
|
167
|
+
/** Optional structured payload carrying additional error context. */
|
|
93
168
|
readonly data: TData;
|
|
169
|
+
/**
|
|
170
|
+
* `true` when this error was declared via `$errors()` and is safe to
|
|
171
|
+
* expose verbatim to clients; `false` for internal faults.
|
|
172
|
+
*/
|
|
94
173
|
readonly defined: boolean;
|
|
174
|
+
/**
|
|
175
|
+
* @param code - Machine-readable error code. Well-known codes (e.g.
|
|
176
|
+
* `'NOT_FOUND'`, `'UNAUTHORIZED'`) resolve default `status` and
|
|
177
|
+
* `message` values automatically.
|
|
178
|
+
* @param options - Override `status`, `message`, `data`, `cause`, and
|
|
179
|
+
* `defined` fields.
|
|
180
|
+
*/
|
|
95
181
|
constructor(code: TCode, options?: SilgiErrorOptions<TData>);
|
|
182
|
+
/**
|
|
183
|
+
* Serialize this error to a plain object suitable for JSON transmission.
|
|
184
|
+
*
|
|
185
|
+
* @remarks
|
|
186
|
+
* The brand symbol (`Symbol.for('silgi.error.brand.v1')`) is
|
|
187
|
+
* non-enumerable and lives on the prototype, so it is never included
|
|
188
|
+
* in the output of `toJSON()` or `JSON.stringify()`.
|
|
189
|
+
*
|
|
190
|
+
* @returns A {@link SilgiErrorJSON} with `defined`, `code`, `status`,
|
|
191
|
+
* `message`, and `data` fields — and nothing else.
|
|
192
|
+
*/
|
|
96
193
|
toJSON(): SilgiErrorJSON<TCode, TData>;
|
|
194
|
+
/**
|
|
195
|
+
* Custom `instanceof` check that works across V8 realms.
|
|
196
|
+
*
|
|
197
|
+
* @remarks
|
|
198
|
+
* Checks for the presence of `Symbol.for('silgi.error.brand.v1')` on
|
|
199
|
+
* the candidate value's prototype chain. Because `Symbol.for` resolves
|
|
200
|
+
* through the global symbol registry (shared across all realms), this
|
|
201
|
+
* correctly identifies `SilgiError` instances that were constructed in a
|
|
202
|
+
* worker thread or `node:vm` context.
|
|
203
|
+
*
|
|
204
|
+
* This is an O(1) property lookup — no prototype walk is performed.
|
|
205
|
+
*
|
|
206
|
+
* @param instance - The value to test.
|
|
207
|
+
* @returns `true` if `instance` carries the Silgi error brand.
|
|
208
|
+
*/
|
|
97
209
|
static [Symbol.hasInstance](instance: unknown): boolean;
|
|
98
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Type guard that returns `true` when `e` is a {@link SilgiError}.
|
|
213
|
+
*
|
|
214
|
+
* @remarks
|
|
215
|
+
* Prefer this function over `instanceof SilgiError` in application code.
|
|
216
|
+
* The brand check is realm-transparent and does not depend on the
|
|
217
|
+
* `SilgiError` class reference being the exact same object across module
|
|
218
|
+
* boundaries. It is also marginally faster than `instanceof` because it
|
|
219
|
+
* skips the `[Symbol.hasInstance]` dispatch and reads the brand directly.
|
|
220
|
+
*
|
|
221
|
+
* @param e - Any value — typically a caught `unknown` error.
|
|
222
|
+
* @returns `true` if `e` carries the `Symbol.for('silgi.error.brand.v1')` brand.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```ts
|
|
226
|
+
* catch (e) {
|
|
227
|
+
* if (isSilgiError(e)) {
|
|
228
|
+
* // e is SilgiError — access e.code, e.status, e.data safely
|
|
229
|
+
* }
|
|
230
|
+
* }
|
|
231
|
+
* ```
|
|
232
|
+
*
|
|
233
|
+
* @see {@link SilgiError}
|
|
234
|
+
* @category Errors
|
|
235
|
+
*/
|
|
236
|
+
declare function isSilgiError(e: unknown): e is SilgiError;
|
|
237
|
+
/**
|
|
238
|
+
* Returns `true` when `error` is a {@link SilgiError} that was explicitly
|
|
239
|
+
* declared with `$errors()` (i.e. `defined === true`).
|
|
240
|
+
*
|
|
241
|
+
* @remarks
|
|
242
|
+
* Defined errors are safe to expose verbatim to clients; undefined errors
|
|
243
|
+
* should be redacted to a generic `INTERNAL_SERVER_ERROR` message.
|
|
244
|
+
*
|
|
245
|
+
* @param error - Any value.
|
|
246
|
+
* @returns A type predicate narrowing to `TError & SilgiError & \{ defined: true \}`.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```ts
|
|
250
|
+
* if (isDefinedError(e)) {
|
|
251
|
+
* // e.defined === true — forward error details to the client
|
|
252
|
+
* }
|
|
253
|
+
* ```
|
|
254
|
+
*
|
|
255
|
+
* @see {@link isSilgiError}
|
|
256
|
+
* @category Errors
|
|
257
|
+
*/
|
|
99
258
|
declare function isDefinedError<TError>(error: TError): error is TError & SilgiError & {
|
|
100
259
|
defined: true;
|
|
101
260
|
};
|
|
261
|
+
/**
|
|
262
|
+
* Coerce any caught value into a {@link SilgiError}.
|
|
263
|
+
*
|
|
264
|
+
* @remarks
|
|
265
|
+
* If `error` is already a `SilgiError` it is returned unchanged.
|
|
266
|
+
* Any other value (plain `Error`, string, etc.) is wrapped in an
|
|
267
|
+
* `INTERNAL_SERVER_ERROR` with the original value preserved as `cause`.
|
|
268
|
+
* The wrapper message is intentionally generic to avoid leaking internal
|
|
269
|
+
* details to clients.
|
|
270
|
+
*
|
|
271
|
+
* @param error - Any value — typically the argument of a `catch` block.
|
|
272
|
+
* @returns The original `SilgiError`, or a new `SilgiError('INTERNAL_SERVER_ERROR')`.
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```ts
|
|
276
|
+
* const e = toSilgiError(err)
|
|
277
|
+
* res.status(e.status).json(e.toJSON())
|
|
278
|
+
* ```
|
|
279
|
+
*
|
|
280
|
+
* @see {@link isSilgiError}
|
|
281
|
+
* @category Errors
|
|
282
|
+
*/
|
|
102
283
|
declare function toSilgiError(error: unknown): SilgiError;
|
|
103
284
|
//#endregion
|
|
104
|
-
export { SilgiError, SilgiErrorCode, SilgiErrorJSON, SilgiErrorOptions, isDefinedError, toSilgiError };
|
|
285
|
+
export { SilgiError, SilgiErrorCode, SilgiErrorJSON, SilgiErrorOptions, isDefinedError, isSilgiError, toSilgiError };
|
package/dist/core/error.mjs
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
1
|
//#region src/core/error.ts
|
|
2
2
|
/**
|
|
3
|
-
* SilgiError — unified RPC error
|
|
3
|
+
* SilgiError — unified RPC error type for the Silgi framework.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Every error thrown through the Silgi request pipeline is either a
|
|
7
|
+
* `SilgiError` or is wrapped into one by {@link toSilgiError} before it
|
|
8
|
+
* reaches the wire. The class carries an HTTP `status`, a machine-readable
|
|
9
|
+
* `code`, optional structured `data`, and a `defined` flag that signals
|
|
10
|
+
* whether the error was deliberately declared with `$errors()` (safe to
|
|
11
|
+
* expose verbatim to clients) or is an internal fault that should be
|
|
12
|
+
* redacted.
|
|
13
|
+
*
|
|
14
|
+
* ### Cross-realm `instanceof`
|
|
15
|
+
* `SilgiError` overrides `Symbol.hasInstance` to check a brand symbol
|
|
16
|
+
* (`Symbol.for('silgi.error.brand.v1')`) stamped on `SilgiError.prototype`.
|
|
17
|
+
* Because `Symbol.for` resolves through the global symbol registry shared
|
|
18
|
+
* across all V8 realms (worker threads, `node:vm` contexts), `instanceof
|
|
19
|
+
* SilgiError` works even when the `SilgiError` class used to construct the
|
|
20
|
+
* error originated in a different realm.
|
|
21
|
+
*
|
|
22
|
+
* ### Subclassing
|
|
23
|
+
* User-defined subclasses are fully supported:
|
|
24
|
+
* ```ts
|
|
25
|
+
* class AuthError extends SilgiError<'UNAUTHORIZED'> {
|
|
26
|
+
* constructor() { super('UNAUTHORIZED', { status: 401 }) }
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
* The brand is inherited through the prototype chain automatically — no
|
|
30
|
+
* extra steps are required in the subclass constructor.
|
|
31
|
+
*
|
|
32
|
+
* @category Errors
|
|
4
33
|
*/
|
|
34
|
+
/** Well-known HTTP error codes with their default status and message. */
|
|
5
35
|
const COMMON_ERRORS = /* @__PURE__ */ Object.freeze({
|
|
6
36
|
BAD_REQUEST: {
|
|
7
37
|
status: 400,
|
|
@@ -72,13 +102,81 @@ const COMMON_ERRORS = /* @__PURE__ */ Object.freeze({
|
|
|
72
102
|
message: "Gateway Timeout"
|
|
73
103
|
}
|
|
74
104
|
});
|
|
75
|
-
|
|
76
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Non-enumerable brand symbol stamped on `SilgiError.prototype`.
|
|
107
|
+
*
|
|
108
|
+
* Using `Symbol.for` ensures the key is resolved from the global symbol
|
|
109
|
+
* registry, which is shared across all V8 realms (worker threads,
|
|
110
|
+
* `node:vm` contexts). This makes cross-realm `instanceof` and
|
|
111
|
+
* {@link isSilgiError} checks reliable without any module-level WeakSet
|
|
112
|
+
* or `globalThis` state.
|
|
113
|
+
*
|
|
114
|
+
* The `.brand.v1` suffix prevents accidental collision with user code that
|
|
115
|
+
* might use the shorter `Symbol.for('silgi.error')` for unrelated purposes,
|
|
116
|
+
* and reserves the `v1` slot for a future migration if brand semantics change.
|
|
117
|
+
*
|
|
118
|
+
* @internal
|
|
119
|
+
*/
|
|
120
|
+
const BRAND = Symbol.for("silgi.error.brand.v1");
|
|
121
|
+
/**
|
|
122
|
+
* A typed, serializable RPC error.
|
|
123
|
+
*
|
|
124
|
+
* @typeParam TCode - String literal type for the error code.
|
|
125
|
+
* @typeParam TData - Shape of the optional structured data payload.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* throw new SilgiError('NOT_FOUND', { message: 'User 42 not found' })
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @example Checking for Silgi errors in a catch block
|
|
133
|
+
* ```ts
|
|
134
|
+
* import { isSilgiError } from 'silgi'
|
|
135
|
+
*
|
|
136
|
+
* try {
|
|
137
|
+
* await client.users.get({ id: '42' })
|
|
138
|
+
* } catch (e) {
|
|
139
|
+
* if (isSilgiError(e)) {
|
|
140
|
+
* console.error(e.code, e.status)
|
|
141
|
+
* }
|
|
142
|
+
* }
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* @see {@link isSilgiError} Preferred type-guard over `instanceof` for cross-realm safety.
|
|
146
|
+
* @see {@link toSilgiError} Wraps any unknown error into a `SilgiError`.
|
|
147
|
+
* @see {@link fromSilgiErrorJSON} Reconstructs a `SilgiError` from a wire-format JSON object.
|
|
148
|
+
*
|
|
149
|
+
* @remarks
|
|
150
|
+
* **Do not mutate the prototype brand.** Removing or overwriting
|
|
151
|
+
* `SilgiError.prototype[Symbol.for('silgi.error.brand.v1')]` will break
|
|
152
|
+
* cross-realm detection for all instances in the current realm.
|
|
153
|
+
*
|
|
154
|
+
* **Subclass note.** If a subclass overrides the constructor and somehow
|
|
155
|
+
* replaces `Object.getPrototypeOf(this)` before construction completes,
|
|
156
|
+
* the inherited brand may be severed. This is an extremely unusual pattern
|
|
157
|
+
* and is not supported.
|
|
158
|
+
*
|
|
159
|
+
* @category Errors
|
|
160
|
+
*/
|
|
77
161
|
var SilgiError = class extends Error {
|
|
162
|
+
/** Machine-readable error code (e.g. `'NOT_FOUND'`). */
|
|
78
163
|
code;
|
|
164
|
+
/** HTTP status code associated with this error. */
|
|
79
165
|
status;
|
|
166
|
+
/** Optional structured payload carrying additional error context. */
|
|
80
167
|
data;
|
|
168
|
+
/**
|
|
169
|
+
* `true` when this error was declared via `$errors()` and is safe to
|
|
170
|
+
* expose verbatim to clients; `false` for internal faults.
|
|
171
|
+
*/
|
|
81
172
|
defined;
|
|
173
|
+
/**
|
|
174
|
+
* @param code - Machine-readable error code. Well-known codes (e.g.
|
|
175
|
+
* `'NOT_FOUND'`, `'UNAUTHORIZED'`) resolve default `status` and
|
|
176
|
+
* `message` values automatically.
|
|
177
|
+
* @param options - Override `status`, `message`, `data`, `cause`, and
|
|
178
|
+
* `defined` fields.
|
|
179
|
+
*/
|
|
82
180
|
constructor(code, options = {}) {
|
|
83
181
|
const defaults = COMMON_ERRORS[code];
|
|
84
182
|
const message = options.message ?? defaults?.message ?? code;
|
|
@@ -89,6 +187,17 @@ var SilgiError = class extends Error {
|
|
|
89
187
|
this.defined = options.defined ?? false;
|
|
90
188
|
this.name = "SilgiError";
|
|
91
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Serialize this error to a plain object suitable for JSON transmission.
|
|
192
|
+
*
|
|
193
|
+
* @remarks
|
|
194
|
+
* The brand symbol (`Symbol.for('silgi.error.brand.v1')`) is
|
|
195
|
+
* non-enumerable and lives on the prototype, so it is never included
|
|
196
|
+
* in the output of `toJSON()` or `JSON.stringify()`.
|
|
197
|
+
*
|
|
198
|
+
* @returns A {@link SilgiErrorJSON} with `defined`, `code`, `status`,
|
|
199
|
+
* `message`, and `data` fields — and nothing else.
|
|
200
|
+
*/
|
|
92
201
|
toJSON() {
|
|
93
202
|
return {
|
|
94
203
|
defined: this.defined,
|
|
@@ -98,35 +207,169 @@ var SilgiError = class extends Error {
|
|
|
98
207
|
data: this.data
|
|
99
208
|
};
|
|
100
209
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Custom `instanceof` check that works across V8 realms.
|
|
212
|
+
*
|
|
213
|
+
* @remarks
|
|
214
|
+
* Checks for the presence of `Symbol.for('silgi.error.brand.v1')` on
|
|
215
|
+
* the candidate value's prototype chain. Because `Symbol.for` resolves
|
|
216
|
+
* through the global symbol registry (shared across all realms), this
|
|
217
|
+
* correctly identifies `SilgiError` instances that were constructed in a
|
|
218
|
+
* worker thread or `node:vm` context.
|
|
219
|
+
*
|
|
220
|
+
* This is an O(1) property lookup — no prototype walk is performed.
|
|
221
|
+
*
|
|
222
|
+
* @param instance - The value to test.
|
|
223
|
+
* @returns `true` if `instance` carries the Silgi error brand.
|
|
224
|
+
*/
|
|
104
225
|
static [Symbol.hasInstance](instance) {
|
|
105
|
-
|
|
106
|
-
let proto = Object.getPrototypeOf(instance);
|
|
107
|
-
while (proto) {
|
|
108
|
-
if (proto.constructor && registry.has(proto.constructor)) return true;
|
|
109
|
-
proto = Object.getPrototypeOf(proto);
|
|
110
|
-
}
|
|
111
|
-
return false;
|
|
226
|
+
return typeof instance === "object" && instance !== null && instance[BRAND] === true;
|
|
112
227
|
}
|
|
113
228
|
};
|
|
229
|
+
Reflect.defineProperty(SilgiError.prototype, BRAND, {
|
|
230
|
+
value: true,
|
|
231
|
+
enumerable: false,
|
|
232
|
+
writable: false,
|
|
233
|
+
configurable: false
|
|
234
|
+
});
|
|
235
|
+
/**
|
|
236
|
+
* Type guard that returns `true` when `e` is a {@link SilgiError}.
|
|
237
|
+
*
|
|
238
|
+
* @remarks
|
|
239
|
+
* Prefer this function over `instanceof SilgiError` in application code.
|
|
240
|
+
* The brand check is realm-transparent and does not depend on the
|
|
241
|
+
* `SilgiError` class reference being the exact same object across module
|
|
242
|
+
* boundaries. It is also marginally faster than `instanceof` because it
|
|
243
|
+
* skips the `[Symbol.hasInstance]` dispatch and reads the brand directly.
|
|
244
|
+
*
|
|
245
|
+
* @param e - Any value — typically a caught `unknown` error.
|
|
246
|
+
* @returns `true` if `e` carries the `Symbol.for('silgi.error.brand.v1')` brand.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```ts
|
|
250
|
+
* catch (e) {
|
|
251
|
+
* if (isSilgiError(e)) {
|
|
252
|
+
* // e is SilgiError — access e.code, e.status, e.data safely
|
|
253
|
+
* }
|
|
254
|
+
* }
|
|
255
|
+
* ```
|
|
256
|
+
*
|
|
257
|
+
* @see {@link SilgiError}
|
|
258
|
+
* @category Errors
|
|
259
|
+
*/
|
|
260
|
+
function isSilgiError(e) {
|
|
261
|
+
return typeof e === "object" && e !== null && e[BRAND] === true;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Returns `true` when `error` is a {@link SilgiError} that was explicitly
|
|
265
|
+
* declared with `$errors()` (i.e. `defined === true`).
|
|
266
|
+
*
|
|
267
|
+
* @remarks
|
|
268
|
+
* Defined errors are safe to expose verbatim to clients; undefined errors
|
|
269
|
+
* should be redacted to a generic `INTERNAL_SERVER_ERROR` message.
|
|
270
|
+
*
|
|
271
|
+
* @param error - Any value.
|
|
272
|
+
* @returns A type predicate narrowing to `TError & SilgiError & \{ defined: true \}`.
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```ts
|
|
276
|
+
* if (isDefinedError(e)) {
|
|
277
|
+
* // e.defined === true — forward error details to the client
|
|
278
|
+
* }
|
|
279
|
+
* ```
|
|
280
|
+
*
|
|
281
|
+
* @see {@link isSilgiError}
|
|
282
|
+
* @category Errors
|
|
283
|
+
*/
|
|
114
284
|
function isDefinedError(error) {
|
|
115
|
-
return error
|
|
285
|
+
return isSilgiError(error) && error.defined === true;
|
|
116
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* Coerce any caught value into a {@link SilgiError}.
|
|
289
|
+
*
|
|
290
|
+
* @remarks
|
|
291
|
+
* If `error` is already a `SilgiError` it is returned unchanged.
|
|
292
|
+
* Any other value (plain `Error`, string, etc.) is wrapped in an
|
|
293
|
+
* `INTERNAL_SERVER_ERROR` with the original value preserved as `cause`.
|
|
294
|
+
* The wrapper message is intentionally generic to avoid leaking internal
|
|
295
|
+
* details to clients.
|
|
296
|
+
*
|
|
297
|
+
* @param error - Any value — typically the argument of a `catch` block.
|
|
298
|
+
* @returns The original `SilgiError`, or a new `SilgiError('INTERNAL_SERVER_ERROR')`.
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```ts
|
|
302
|
+
* const e = toSilgiError(err)
|
|
303
|
+
* res.status(e.status).json(e.toJSON())
|
|
304
|
+
* ```
|
|
305
|
+
*
|
|
306
|
+
* @see {@link isSilgiError}
|
|
307
|
+
* @category Errors
|
|
308
|
+
*/
|
|
117
309
|
function toSilgiError(error) {
|
|
118
|
-
if (error
|
|
310
|
+
if (isSilgiError(error)) return error;
|
|
119
311
|
return new SilgiError("INTERNAL_SERVER_ERROR", {
|
|
120
312
|
message: "Internal server error",
|
|
121
313
|
cause: error
|
|
122
314
|
});
|
|
123
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Returns `true` when `status` represents an HTTP error (4xx or 5xx).
|
|
318
|
+
*
|
|
319
|
+
* @param status - An HTTP status code.
|
|
320
|
+
* @returns `true` when `status >= 400`.
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```ts
|
|
324
|
+
* if (isErrorStatus(response.status)) handleError(response)
|
|
325
|
+
* ```
|
|
326
|
+
*
|
|
327
|
+
* @category Errors
|
|
328
|
+
*/
|
|
124
329
|
function isErrorStatus(status) {
|
|
125
330
|
return status >= 400;
|
|
126
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Type guard that returns `true` when `json` has the shape of a
|
|
334
|
+
* {@link SilgiErrorJSON} wire object.
|
|
335
|
+
*
|
|
336
|
+
* @param json - Any decoded JSON value.
|
|
337
|
+
* @returns `true` when `json` has a string `code` and a numeric `status`.
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* ```ts
|
|
341
|
+
* const body = await response.json()
|
|
342
|
+
* if (isSilgiErrorJSON(body)) {
|
|
343
|
+
* throw fromSilgiErrorJSON(body)
|
|
344
|
+
* }
|
|
345
|
+
* ```
|
|
346
|
+
*
|
|
347
|
+
* @see {@link fromSilgiErrorJSON}
|
|
348
|
+
* @category Errors
|
|
349
|
+
*/
|
|
127
350
|
function isSilgiErrorJSON(json) {
|
|
128
351
|
return typeof json === "object" && json !== null && "code" in json && "status" in json && typeof json.code === "string";
|
|
129
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Reconstruct a {@link SilgiError} from a {@link SilgiErrorJSON} wire object.
|
|
355
|
+
*
|
|
356
|
+
* @remarks
|
|
357
|
+
* Typically used on the client side after receiving an error response.
|
|
358
|
+
* The reconstructed error carries the brand and passes `isSilgiError()`.
|
|
359
|
+
*
|
|
360
|
+
* @param json - A validated {@link SilgiErrorJSON} object (use
|
|
361
|
+
* {@link isSilgiErrorJSON} to validate before calling this function).
|
|
362
|
+
* @returns A fully-formed `SilgiError` instance.
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* ```ts
|
|
366
|
+
* const body = await res.json()
|
|
367
|
+
* if (isSilgiErrorJSON(body)) throw fromSilgiErrorJSON(body)
|
|
368
|
+
* ```
|
|
369
|
+
*
|
|
370
|
+
* @see {@link isSilgiErrorJSON}
|
|
371
|
+
* @category Errors
|
|
372
|
+
*/
|
|
130
373
|
function fromSilgiErrorJSON(json) {
|
|
131
374
|
return new SilgiError(json.code, {
|
|
132
375
|
status: json.status,
|
|
@@ -136,4 +379,4 @@ function fromSilgiErrorJSON(json) {
|
|
|
136
379
|
});
|
|
137
380
|
}
|
|
138
381
|
//#endregion
|
|
139
|
-
export { SilgiError, fromSilgiErrorJSON, isDefinedError, isErrorStatus, isSilgiErrorJSON, toSilgiError };
|
|
382
|
+
export { SilgiError, fromSilgiErrorJSON, isDefinedError, isErrorStatus, isSilgiError, isSilgiErrorJSON, toSilgiError };
|
package/dist/core/handler.d.mts
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { AnalyticsOptions } from "../plugins/analytics/types.mjs";
|
|
2
|
+
import { SchemaRegistry } from "./schema-converter.mjs";
|
|
2
3
|
import { ScalarOptions } from "../scalar.mjs";
|
|
4
|
+
import { SilgiHooks } from "../silgi.mjs";
|
|
3
5
|
import { Hookable } from "hookable";
|
|
4
6
|
|
|
5
7
|
//#region src/core/handler.d.ts
|
|
6
8
|
type FetchHandler = (request: Request) => Response | Promise<Response>;
|
|
7
9
|
interface WrapHandlerOptions {
|
|
8
|
-
analytics?:
|
|
10
|
+
analytics?: AnalyticsOptions;
|
|
9
11
|
scalar?: boolean | ScalarOptions;
|
|
10
12
|
/** URL path prefix for the handler (e.g. "/api"). Requests not matching this prefix return 404. */
|
|
11
13
|
basePath?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Schema registry for OpenAPI / analytics schema conversion. Built from
|
|
16
|
+
* `schemaConverters` in the silgi instance config — do not set manually.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
schemaRegistry?: SchemaRegistry;
|
|
20
|
+
/**
|
|
21
|
+
* Hookable instance — threaded so `wrapWithAnalytics` can register
|
|
22
|
+
* lifecycle listeners on `request:prepare` / `response:finalize`.
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
hooks?: Hookable<SilgiHooks>;
|
|
12
26
|
}
|
|
13
27
|
//#endregion
|
|
14
28
|
export { FetchHandler, WrapHandlerOptions };
|