silgi 0.52.1 → 0.53.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/dist/adapters/aws-lambda.mjs +1 -1
- package/dist/broker/redis.mjs +1 -2
- package/dist/builder.d.mts +1 -21
- package/dist/builder.mjs +4 -2
- package/dist/caller.mjs +8 -1
- package/dist/client/plugins/retry.mjs +9 -4
- package/dist/compile.d.mts +2 -2
- package/dist/compile.mjs +5 -3
- package/dist/core/ctx-symbols.mjs +18 -1
- package/dist/core/handler.mjs +32 -17
- package/dist/core/input.mjs +29 -12
- package/dist/core/task.d.mts +24 -7
- package/dist/core/task.mjs +83 -47
- package/dist/core/url.mjs +19 -1
- package/dist/index.d.mts +2 -4
- package/dist/index.mjs +2 -4
- package/dist/integrations/better-auth/index.mjs +11 -2
- package/dist/lazy.mjs +3 -0
- package/dist/map-input.d.mts +8 -6
- package/dist/map-input.mjs +8 -6
- package/dist/plugins/analytics/routes.mjs +25 -13
- package/dist/plugins/cache.mjs +4 -10
- package/dist/plugins/coerce.d.mts +3 -2
- package/dist/plugins/coerce.mjs +25 -8
- package/dist/silgi.d.mts +35 -0
- package/dist/silgi.mjs +38 -7
- package/dist/ws.mjs +2 -2
- package/package.json +2 -4
- package/dist/error-mapper.d.mts +0 -32
- package/dist/error-mapper.mjs +0 -63
- package/dist/route-kit.d.mts +0 -48
- package/dist/route-kit.mjs +0 -12
|
@@ -26,7 +26,7 @@ function createHandler(router, options = {}) {
|
|
|
26
26
|
const isV2 = event.version === "2.0";
|
|
27
27
|
const method = isV2 ? event.requestContext?.http?.method ?? "GET" : event.httpMethod ?? "GET";
|
|
28
28
|
let pathname = isV2 ? event.rawPath ?? "/" : event.path ?? "/";
|
|
29
|
-
if (prefix && pathname.startsWith(prefix)) pathname = pathname.slice(prefix.length);
|
|
29
|
+
if (prefix && (pathname === prefix || pathname.startsWith(prefix + "/"))) pathname = pathname.slice(prefix.length);
|
|
30
30
|
if (pathname.startsWith("/")) pathname = pathname.slice(1);
|
|
31
31
|
const match = flatRouter(method, "/" + pathname);
|
|
32
32
|
if (!match) return {
|
package/dist/broker/redis.mjs
CHANGED
|
@@ -36,7 +36,6 @@ function ioredisTransport(pub, sub) {
|
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
let globalSeq = 0;
|
|
40
39
|
/**
|
|
41
40
|
* Create a Redis broker driver from a RedisTransport.
|
|
42
41
|
*
|
|
@@ -49,7 +48,7 @@ let globalSeq = 0;
|
|
|
49
48
|
* Wire format: `"inbox-channel\npayload"` (newline separator, no double-serialization)
|
|
50
49
|
*/
|
|
51
50
|
function redisBroker(transport, options = {}) {
|
|
52
|
-
const inboxPrefix = options.inbox ?? `silgi:inbox:${Date.now().toString(36)}:${(
|
|
51
|
+
const inboxPrefix = options.inbox ?? `silgi:inbox:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 10)}`;
|
|
53
52
|
let requestSeq = 0;
|
|
54
53
|
return {
|
|
55
54
|
async subscribe(subject, handler) {
|
package/dist/builder.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AnySchema, InferSchemaInput, InferSchemaOutput } from "./core/schema.mjs";
|
|
2
2
|
import { TaskDef, TaskEvent } from "./core/task.mjs";
|
|
3
|
-
import { ErrorDef, GuardDef, Meta,
|
|
3
|
+
import { ErrorDef, GuardDef, Meta, ProcedureDef, ProcedureType, ResolveContext, Route, WrapDef } from "./types.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/builder.d.ts
|
|
6
6
|
/** Initial builder — no input, no output, no errors yet */
|
|
@@ -9,17 +9,6 @@ interface ProcedureBuilder<TType extends ProcedureType, TBaseCtx extends Record<
|
|
|
9
9
|
$use<TReturn extends Record<string, unknown> | void, TGErrors extends ErrorDef = {}>(guard: GuardDef<any, TReturn, TGErrors>): ProcedureBuilder<TType, TBaseCtx, TReturn extends Record<string, unknown> ? TCtx & TReturn : TCtx, TInput, TGErrors & TErrors>;
|
|
10
10
|
/** Add a wrap middleware — does not change context type */
|
|
11
11
|
$use(wrap: WrapDef<any>): ProcedureBuilder<TType, TBaseCtx, TCtx, TInput, TErrors>;
|
|
12
|
-
/**
|
|
13
|
-
* Add a middleware of unknown variant (`GuardDef | WrapDef`).
|
|
14
|
-
*
|
|
15
|
-
* @remarks
|
|
16
|
-
* Used by factory-pattern builders that accept middleware through a
|
|
17
|
-
* dependency boundary where the concrete variant isn't known at the
|
|
18
|
-
* call site. Context is not enriched — if you need guard-added fields
|
|
19
|
-
* in `.$resolve()`, pass the guard with its concrete type or use
|
|
20
|
-
* `defineRouteKit` to bind the ctx shape up front.
|
|
21
|
-
*/
|
|
22
|
-
$use(mw: MiddlewareDef): ProcedureBuilder<TType, TBaseCtx, TCtx, TInput, TErrors>;
|
|
23
12
|
/** Set input schema */
|
|
24
13
|
$input<TSchema extends AnySchema>(schema: TSchema): ProcedureBuilder<TType, TBaseCtx, TCtx, InferSchemaOutput<TSchema>, TErrors>;
|
|
25
14
|
/** Set output schema — enables return type autocomplete */
|
|
@@ -46,15 +35,6 @@ interface ProcedureBuilderWithOutput<TType extends ProcedureType, TBaseCtx exten
|
|
|
46
35
|
$use<TReturn extends Record<string, unknown> | void, TGErrors extends ErrorDef = {}>(guard: GuardDef<any, TReturn, TGErrors>): ProcedureBuilderWithOutput<TType, TBaseCtx, TReturn extends Record<string, unknown> ? TCtx & TReturn : TCtx, TInput, TOutputResolved, TGErrors & TErrors>;
|
|
47
36
|
/** Add a wrap middleware — does not change context type */
|
|
48
37
|
$use(wrap: WrapDef<any>): ProcedureBuilderWithOutput<TType, TBaseCtx, TCtx, TInput, TOutputResolved, TErrors>;
|
|
49
|
-
/**
|
|
50
|
-
* Add a middleware of unknown variant (`GuardDef | WrapDef`).
|
|
51
|
-
*
|
|
52
|
-
* @remarks
|
|
53
|
-
* Used by factory-pattern builders that accept middleware through a
|
|
54
|
-
* dependency boundary where the concrete variant isn't known at the
|
|
55
|
-
* call site.
|
|
56
|
-
*/
|
|
57
|
-
$use(mw: MiddlewareDef): ProcedureBuilderWithOutput<TType, TBaseCtx, TCtx, TInput, TOutputResolved, TErrors>;
|
|
58
38
|
/** Set typed errors */
|
|
59
39
|
$errors<TNewErrors extends ErrorDef>(errors: TNewErrors): ProcedureBuilderWithOutput<TType, TBaseCtx, TCtx, TInput, TOutputResolved, TNewErrors & TErrors>;
|
|
60
40
|
/** Set route metadata */
|
package/dist/builder.mjs
CHANGED
|
@@ -22,6 +22,7 @@ var ProcBuilder = class {
|
|
|
22
22
|
route = null;
|
|
23
23
|
meta = null;
|
|
24
24
|
_contextFactory = null;
|
|
25
|
+
_rootWrapsGetter = null;
|
|
25
26
|
constructor(type) {
|
|
26
27
|
this.type = type;
|
|
27
28
|
}
|
|
@@ -58,12 +59,13 @@ var ProcBuilder = class {
|
|
|
58
59
|
return this;
|
|
59
60
|
}
|
|
60
61
|
$task(config) {
|
|
61
|
-
return createTaskFromProcedure(config, config.resolve, this.input, this.use, this._contextFactory);
|
|
62
|
+
return createTaskFromProcedure(config, config.resolve, this.input, this.use, this._contextFactory, this._rootWrapsGetter);
|
|
62
63
|
}
|
|
63
64
|
};
|
|
64
|
-
function createProcedureBuilder(type, contextFactory) {
|
|
65
|
+
function createProcedureBuilder(type, contextFactory, rootWrapsGetter) {
|
|
65
66
|
const b = new ProcBuilder(type);
|
|
66
67
|
if (contextFactory) b._contextFactory = contextFactory;
|
|
68
|
+
if (rootWrapsGetter) b._rootWrapsGetter = rootWrapsGetter;
|
|
67
69
|
return b;
|
|
68
70
|
}
|
|
69
71
|
//#endregion
|
package/dist/caller.mjs
CHANGED
|
@@ -23,6 +23,13 @@ import { applyContext } from "./core/dispatch.mjs";
|
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
25
|
/**
|
|
26
|
+
* Never-aborting signal used when `timeout: null` is opted into and the
|
|
27
|
+
* caller passes no per-call signal. Allocated once at module load; compiled
|
|
28
|
+
* handlers can still `.addEventListener('abort', …)` safely — the listener
|
|
29
|
+
* simply never fires.
|
|
30
|
+
*/
|
|
31
|
+
const NEVER = new AbortController().signal;
|
|
32
|
+
/**
|
|
26
33
|
* Create a direct caller for a router — no HTTP, no serialization.
|
|
27
34
|
*
|
|
28
35
|
* Returns a proxy that mirrors the router's nested structure.
|
|
@@ -73,7 +80,7 @@ function createCaller(routerDef, contextFactory, options) {
|
|
|
73
80
|
if (!match) throw new Error(`Procedure not found: ${path}`);
|
|
74
81
|
const ctx = await resolveContext(callOptions?.context);
|
|
75
82
|
if (match.params) ctx.params = match.params;
|
|
76
|
-
const signal = callOptions?.signal ?? (defaultTimeout !== null ? AbortSignal.timeout(defaultTimeout) :
|
|
83
|
+
const signal = callOptions?.signal ?? (defaultTimeout !== null ? AbortSignal.timeout(defaultTimeout) : NEVER);
|
|
77
84
|
try {
|
|
78
85
|
const result = match.data.handler(ctx, input, signal);
|
|
79
86
|
return result instanceof Promise ? await result : result;
|
|
@@ -65,11 +65,16 @@ function withRetry(link, options = {}) {
|
|
|
65
65
|
path
|
|
66
66
|
});
|
|
67
67
|
await new Promise((resolve, reject) => {
|
|
68
|
-
const
|
|
69
|
-
|
|
68
|
+
const signal = callOptions.signal;
|
|
69
|
+
const onAbort = () => {
|
|
70
70
|
clearTimeout(timer);
|
|
71
|
-
reject(
|
|
72
|
-
}
|
|
71
|
+
reject(signal.reason);
|
|
72
|
+
};
|
|
73
|
+
const timer = setTimeout(() => {
|
|
74
|
+
signal?.removeEventListener("abort", onAbort);
|
|
75
|
+
resolve();
|
|
76
|
+
}, delay);
|
|
77
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
73
78
|
});
|
|
74
79
|
}
|
|
75
80
|
throw new Error("Retry exhausted");
|
package/dist/compile.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProcedureDef } from "./types.mjs";
|
|
1
|
+
import { ProcedureDef, WrapDef } from "./types.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/compile.d.ts
|
|
4
4
|
/**
|
|
@@ -15,7 +15,7 @@ type CompiledHandler = (ctx: Record<string, unknown>, rawInput: unknown, signal:
|
|
|
15
15
|
* - Pre-computed fail function (singleton per procedure)
|
|
16
16
|
* - Sync fast path when all guards are sync
|
|
17
17
|
*/
|
|
18
|
-
declare function compileProcedure(procedure: ProcedureDef): CompiledHandler;
|
|
18
|
+
declare function compileProcedure(procedure: ProcedureDef, rootWraps?: readonly WrapDef[] | null): CompiledHandler;
|
|
19
19
|
interface CompiledRoute {
|
|
20
20
|
handler: CompiledHandler;
|
|
21
21
|
/** Pre-computed Cache-Control header value, or undefined if no caching */
|
package/dist/compile.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { validateSchema } from "./core/schema.mjs";
|
|
2
2
|
import { SilgiError } from "./core/error.mjs";
|
|
3
3
|
import { isProcedureDef } from "./core/router-utils.mjs";
|
|
4
|
-
import { RAW_INPUT } from "./core/ctx-symbols.mjs";
|
|
4
|
+
import { RAW_INPUT, ROOT_WRAPS } from "./core/ctx-symbols.mjs";
|
|
5
5
|
import { addRoute, createRouter, findRoute } from "rou3";
|
|
6
6
|
//#region src/compile.ts
|
|
7
7
|
/**
|
|
@@ -207,10 +207,11 @@ function _validateAndResolve(inputSchema, outputSchema, resolveFn, rawInput, ctx
|
|
|
207
207
|
* - Pre-computed fail function (singleton per procedure)
|
|
208
208
|
* - Sync fast path when all guards are sync
|
|
209
209
|
*/
|
|
210
|
-
function compileProcedure(procedure) {
|
|
210
|
+
function compileProcedure(procedure, rootWraps) {
|
|
211
211
|
const middlewares = procedure.use ?? [];
|
|
212
212
|
const guards = [];
|
|
213
213
|
const wraps = [];
|
|
214
|
+
if (rootWraps && rootWraps.length > 0) for (let i = 0; i < rootWraps.length; i++) wraps.push(rootWraps[i]);
|
|
214
215
|
for (const mw of middlewares) if (mw.kind === "guard") guards.push(mw);
|
|
215
216
|
else wraps.push(mw);
|
|
216
217
|
const inputSchema = procedure.input;
|
|
@@ -290,6 +291,7 @@ function compileProcedure(procedure) {
|
|
|
290
291
|
*/
|
|
291
292
|
function compileRouter(def) {
|
|
292
293
|
const router = createRouter();
|
|
294
|
+
const rootWraps = def[ROOT_WRAPS];
|
|
293
295
|
function walk(node, path) {
|
|
294
296
|
if (isProcedureDef(node)) {
|
|
295
297
|
const proc = node;
|
|
@@ -299,7 +301,7 @@ function compileRouter(def) {
|
|
|
299
301
|
let cacheControl;
|
|
300
302
|
if (route?.cache != null) cacheControl = typeof route.cache === "number" ? `public, max-age=${route.cache}` : route.cache;
|
|
301
303
|
const compiled = {
|
|
302
|
-
handler: compileProcedure(proc),
|
|
304
|
+
handler: compileProcedure(proc, rootWraps),
|
|
303
305
|
cacheControl,
|
|
304
306
|
passthrough: routePath.includes("**") || void 0,
|
|
305
307
|
method
|
|
@@ -17,5 +17,22 @@
|
|
|
17
17
|
* @internal
|
|
18
18
|
*/
|
|
19
19
|
const RAW_INPUT = Symbol.for("silgi.rawInput");
|
|
20
|
+
/**
|
|
21
|
+
* Brand stamped on a `RouterDef` by `silgi({ wraps }).router(def)` to carry
|
|
22
|
+
* the instance's root wraps along with the def itself.
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* Every compile site (`silgi.router`, `createCaller`, `createFetchHandler`,
|
|
26
|
+
* WS hooks, adapter `createHandler` variants) calls `compileRouter(def)`.
|
|
27
|
+
* Reading the brand off `def` inside `compileRouter` means root wraps
|
|
28
|
+
* reach every adapter without any per-adapter plumbing, without relying
|
|
29
|
+
* on `routerCache`, and without a second tree walk.
|
|
30
|
+
*
|
|
31
|
+
* The brand is a non-enumerable own property on the user's def (Symbol
|
|
32
|
+
* keys are skipped by `Object.entries`, so router traversal is unaffected).
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
const ROOT_WRAPS = Symbol.for("silgi.rootWraps");
|
|
20
37
|
//#endregion
|
|
21
|
-
export { RAW_INPUT };
|
|
38
|
+
export { RAW_INPUT, ROOT_WRAPS };
|
package/dist/core/handler.mjs
CHANGED
|
@@ -77,23 +77,31 @@ function makeResponse(output, route, format, ctx) {
|
|
|
77
77
|
*/
|
|
78
78
|
function wrapHandler(handler, router, options, prefix) {
|
|
79
79
|
if (!options?.scalar && !options?.analytics) return handler;
|
|
80
|
-
let wrapped;
|
|
80
|
+
let wrapped = handler;
|
|
81
|
+
let initDone = false;
|
|
81
82
|
let initPromise;
|
|
82
83
|
async function init() {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
try {
|
|
85
|
+
let h = handler;
|
|
86
|
+
if (options.scalar) {
|
|
87
|
+
const { wrapWithScalar } = await import("../scalar.mjs");
|
|
88
|
+
const scalarOpts = typeof options.scalar === "object" ? options.scalar : {};
|
|
89
|
+
h = wrapWithScalar(h, router, scalarOpts, prefix, options.schemaRegistry);
|
|
90
|
+
}
|
|
91
|
+
if (options.analytics) {
|
|
92
|
+
const { wrapWithAnalytics } = await import("../plugins/analytics.mjs");
|
|
93
|
+
h = wrapWithAnalytics(h, router, options.analytics, options.schemaRegistry, options.hooks);
|
|
94
|
+
}
|
|
95
|
+
wrapped = h;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error("[silgi] Failed to initialise scalar/analytics wrapper:", err);
|
|
98
|
+
wrapped = handler;
|
|
99
|
+
} finally {
|
|
100
|
+
initDone = true;
|
|
92
101
|
}
|
|
93
|
-
wrapped = h;
|
|
94
102
|
}
|
|
95
103
|
return (request) => {
|
|
96
|
-
if (
|
|
104
|
+
if (initDone) return wrapped(request);
|
|
97
105
|
initPromise ??= init();
|
|
98
106
|
return initPromise.then(() => wrapped(request));
|
|
99
107
|
};
|
|
@@ -111,25 +119,32 @@ function createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge) {
|
|
|
111
119
|
status: 404,
|
|
112
120
|
message: "Procedure not found"
|
|
113
121
|
});
|
|
122
|
+
function reportHookError(name, err) {
|
|
123
|
+
console.error(`[silgi] hook "${name}" threw:`, err);
|
|
124
|
+
}
|
|
114
125
|
function callHook(name, event) {
|
|
115
126
|
if (!hooks) return;
|
|
116
127
|
try {
|
|
117
128
|
const result = hooks.callHook(name, event);
|
|
118
|
-
if (result instanceof Promise) result.catch(() =>
|
|
119
|
-
} catch {
|
|
129
|
+
if (result instanceof Promise) result.catch((err) => reportHookError(name, err));
|
|
130
|
+
} catch (err) {
|
|
131
|
+
reportHookError(name, err);
|
|
132
|
+
}
|
|
120
133
|
}
|
|
121
134
|
function awaitHook(name, event) {
|
|
122
135
|
if (!hooks) return;
|
|
123
136
|
try {
|
|
124
137
|
const result = hooks.callHook(name, event);
|
|
125
|
-
if (result instanceof Promise) return result.catch(() =>
|
|
126
|
-
} catch {
|
|
138
|
+
if (result instanceof Promise) return result.catch((err) => reportHookError(name, err));
|
|
139
|
+
} catch (err) {
|
|
140
|
+
reportHookError(name, err);
|
|
141
|
+
}
|
|
127
142
|
}
|
|
128
143
|
return async function handleRequest(request) {
|
|
129
144
|
const url = request.url;
|
|
130
145
|
let fullPath = parseUrlPath(url);
|
|
131
146
|
if (prefix) {
|
|
132
|
-
if (!fullPath.startsWith(prefix)) return new Response(notFoundBody, {
|
|
147
|
+
if (fullPath !== prefix && !fullPath.startsWith(prefix + "/")) return new Response(notFoundBody, {
|
|
133
148
|
status: 404,
|
|
134
149
|
headers: jsonHeaders
|
|
135
150
|
});
|
package/dist/core/input.mjs
CHANGED
|
@@ -5,19 +5,36 @@ import { SilgiError } from "./error.mjs";
|
|
|
5
5
|
*/
|
|
6
6
|
let _msgpack;
|
|
7
7
|
let _devalue;
|
|
8
|
-
const isBun = typeof globalThis.Bun !== "undefined";
|
|
9
8
|
/** Max allowed size for GET ?data= parameter (bytes). Prevents JSON bomb via URL. */
|
|
10
9
|
const MAX_QUERY_DATA_LENGTH = 8192;
|
|
10
|
+
/**
|
|
11
|
+
* Find the value of the `data=` query parameter by key, not by substring.
|
|
12
|
+
*
|
|
13
|
+
* The previous `indexOf('data=')` implementation matched `userdata=...`,
|
|
14
|
+
* `xdata=...`, or any other key ending in `data` — silently returning the
|
|
15
|
+
* wrong value. This scans for `data=` at a parameter boundary (either the
|
|
16
|
+
* first param or after `&`).
|
|
17
|
+
*/
|
|
18
|
+
function findDataParam(searchStr) {
|
|
19
|
+
let i = 0;
|
|
20
|
+
while (i < searchStr.length) {
|
|
21
|
+
if (searchStr.startsWith("data=", i)) {
|
|
22
|
+
const valueStart = i + 5;
|
|
23
|
+
const valueEnd = searchStr.indexOf("&", valueStart);
|
|
24
|
+
return valueEnd === -1 ? searchStr.slice(valueStart) : searchStr.slice(valueStart, valueEnd);
|
|
25
|
+
}
|
|
26
|
+
const nextAmp = searchStr.indexOf("&", i);
|
|
27
|
+
if (nextAmp === -1) return null;
|
|
28
|
+
i = nextAmp + 1;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
11
32
|
/** Parse request input from body or query string */
|
|
12
33
|
async function parseInput(request, url, qMark) {
|
|
13
34
|
if (request.method === "GET" || !request.body) {
|
|
14
35
|
if (qMark !== -1) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
if (dataIdx !== -1) {
|
|
18
|
-
const valueStart = dataIdx + 5;
|
|
19
|
-
const valueEnd = searchStr.indexOf("&", valueStart);
|
|
20
|
-
const encoded = valueEnd === -1 ? searchStr.slice(valueStart) : searchStr.slice(valueStart, valueEnd);
|
|
36
|
+
const encoded = findDataParam(url.slice(qMark + 1));
|
|
37
|
+
if (encoded !== null) {
|
|
21
38
|
if (encoded.length > MAX_QUERY_DATA_LENGTH) throw new SilgiError("BAD_REQUEST", { message: "Query data parameter too large" });
|
|
22
39
|
return JSON.parse(decodeURIComponent(encoded));
|
|
23
40
|
}
|
|
@@ -37,13 +54,13 @@ async function parseInput(request, url, qMark) {
|
|
|
37
54
|
return text ? _devalue.decode(text) : void 0;
|
|
38
55
|
}
|
|
39
56
|
}
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
const text = await request.text();
|
|
58
|
+
if (!text) return void 0;
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(text);
|
|
42
61
|
} catch {
|
|
43
|
-
|
|
62
|
+
throw new SilgiError("BAD_REQUEST", { message: "Malformed JSON body" });
|
|
44
63
|
}
|
|
45
|
-
const text = await request.text();
|
|
46
|
-
return text ? JSON.parse(text) : void 0;
|
|
47
64
|
}
|
|
48
65
|
//#endregion
|
|
49
66
|
export { parseInput };
|
package/dist/core/task.d.mts
CHANGED
|
@@ -43,10 +43,6 @@ declare function collectCronTasks(def: Record<string, unknown>): Array<{
|
|
|
43
43
|
cron: string;
|
|
44
44
|
task: TaskDef<any, any>;
|
|
45
45
|
}>;
|
|
46
|
-
declare function startCronJobs(cronTasks: Array<{
|
|
47
|
-
cron: string;
|
|
48
|
-
task: TaskDef<any, any>;
|
|
49
|
-
}>): Promise<void>;
|
|
50
46
|
interface ScheduledTaskInfo {
|
|
51
47
|
name: string;
|
|
52
48
|
cron: string;
|
|
@@ -56,7 +52,28 @@ interface ScheduledTaskInfo {
|
|
|
56
52
|
runs: number;
|
|
57
53
|
errors: number;
|
|
58
54
|
}
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
interface CronRegistry {
|
|
56
|
+
start: (cronTasks: Array<{
|
|
57
|
+
cron: string;
|
|
58
|
+
task: TaskDef<any, any>;
|
|
59
|
+
}>) => Promise<void>;
|
|
60
|
+
stop: () => void;
|
|
61
|
+
list: () => ScheduledTaskInfo[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Create an isolated cron registry. Each silgi instance owns one, so
|
|
65
|
+
* `server.close()` on instance A never stops instance B's jobs and
|
|
66
|
+
* `list()` never returns jobs from another instance.
|
|
67
|
+
*/
|
|
68
|
+
declare function createCronRegistry(): CronRegistry;
|
|
69
|
+
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|
|
70
|
+
declare const startCronJobs: (cronTasks: Array<{
|
|
71
|
+
cron: string;
|
|
72
|
+
task: TaskDef<any, any>;
|
|
73
|
+
}>) => Promise<void>;
|
|
74
|
+
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|
|
75
|
+
declare const stopCronJobs: () => void;
|
|
76
|
+
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|
|
77
|
+
declare const getScheduledTasks: () => ScheduledTaskInfo[];
|
|
61
78
|
//#endregion
|
|
62
|
-
export { ScheduledTaskInfo, TaskDef, TaskEvent, collectCronTasks, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs };
|
|
79
|
+
export { CronRegistry, ScheduledTaskInfo, TaskDef, TaskEvent, collectCronTasks, createCronRegistry, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs };
|
package/dist/core/task.mjs
CHANGED
|
@@ -10,7 +10,7 @@ let _onTaskComplete = null;
|
|
|
10
10
|
function setTaskAnalytics(cb) {
|
|
11
11
|
_onTaskComplete = cb;
|
|
12
12
|
}
|
|
13
|
-
function createTaskFromProcedure(config, resolveFn, inputSchema, use, contextFactory) {
|
|
13
|
+
function createTaskFromProcedure(config, resolveFn, inputSchema, use, contextFactory, rootWrapsGetter = null) {
|
|
14
14
|
const { name, cron = null, description } = config;
|
|
15
15
|
if (!name) throw new TypeError("Task name is required");
|
|
16
16
|
const taskResolve = async (opts) => {
|
|
@@ -32,14 +32,25 @@ function createTaskFromProcedure(config, resolveFn, inputSchema, use, contextFac
|
|
|
32
32
|
reqTrace = new RequestTrace();
|
|
33
33
|
ctx.trace = reqTrace;
|
|
34
34
|
} catch {}
|
|
35
|
+
const rootWraps = rootWrapsGetter ? rootWrapsGetter() : null;
|
|
36
|
+
const runResolver = () => resolveFn({
|
|
37
|
+
input,
|
|
38
|
+
ctx,
|
|
39
|
+
name,
|
|
40
|
+
scheduledTime: void 0
|
|
41
|
+
});
|
|
35
42
|
const t0 = performance.now();
|
|
36
43
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
let output;
|
|
45
|
+
if (rootWraps && rootWraps.length > 0) {
|
|
46
|
+
let execute = () => Promise.resolve(runResolver());
|
|
47
|
+
for (let i = rootWraps.length - 1; i >= 0; i--) {
|
|
48
|
+
const wrapFn = rootWraps[i].fn;
|
|
49
|
+
const next = execute;
|
|
50
|
+
execute = () => wrapFn(ctx, next);
|
|
51
|
+
}
|
|
52
|
+
output = await execute();
|
|
53
|
+
} else output = await runResolver();
|
|
43
54
|
if (parentTrace) parentTrace.spans.push({
|
|
44
55
|
name: `task:${name}`,
|
|
45
56
|
kind: "queue",
|
|
@@ -121,45 +132,70 @@ function collectCronTasks(def) {
|
|
|
121
132
|
}
|
|
122
133
|
return result;
|
|
123
134
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Create an isolated cron registry. Each silgi instance owns one, so
|
|
137
|
+
* `server.close()` on instance A never stops instance B's jobs and
|
|
138
|
+
* `list()` never returns jobs from another instance.
|
|
139
|
+
*/
|
|
140
|
+
function createCronRegistry() {
|
|
141
|
+
const entries = [];
|
|
142
|
+
return {
|
|
143
|
+
async start(cronTasks) {
|
|
144
|
+
if (cronTasks.length === 0) return;
|
|
145
|
+
const { Cron } = await import("croner");
|
|
146
|
+
for (const { cron, task } of cronTasks) {
|
|
147
|
+
const entry = {
|
|
148
|
+
name: task.route?.summary || cron,
|
|
149
|
+
cron,
|
|
150
|
+
description: task.route?.summary,
|
|
151
|
+
job: null,
|
|
152
|
+
lastRun: null,
|
|
153
|
+
runs: 0,
|
|
154
|
+
errors: 0
|
|
155
|
+
};
|
|
156
|
+
entry.job = new Cron(cron, async () => {
|
|
157
|
+
entry.lastRun = Date.now();
|
|
158
|
+
entry.runs++;
|
|
159
|
+
task.dispatch(void 0).catch((err) => {
|
|
160
|
+
entry.errors++;
|
|
161
|
+
console.error(`[silgi] Cron task failed:`, err instanceof Error ? err.message : err);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
entries.push(entry);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
stop() {
|
|
168
|
+
for (const e of entries) e.job.stop();
|
|
169
|
+
entries.length = 0;
|
|
170
|
+
},
|
|
171
|
+
list() {
|
|
172
|
+
return entries.map((e) => ({
|
|
173
|
+
name: e.name,
|
|
174
|
+
cron: e.cron,
|
|
175
|
+
description: e.description,
|
|
176
|
+
nextRun: e.job.nextRun()?.getTime() ?? null,
|
|
177
|
+
lastRun: e.lastRun,
|
|
178
|
+
runs: e.runs,
|
|
179
|
+
errors: e.errors
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
};
|
|
163
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Process-default cron registry. Shared state — use {@link createCronRegistry}
|
|
186
|
+
* when a silgi instance should own its own jobs.
|
|
187
|
+
*
|
|
188
|
+
* @deprecated Prefer per-instance registries. The module-default is
|
|
189
|
+
* retained so existing imports of `startCronJobs` / `stopCronJobs` /
|
|
190
|
+
* `getScheduledTasks` keep working; a future major will remove these
|
|
191
|
+
* top-level re-exports.
|
|
192
|
+
*/
|
|
193
|
+
const _defaultRegistry = createCronRegistry();
|
|
194
|
+
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|
|
195
|
+
const startCronJobs = _defaultRegistry.start;
|
|
196
|
+
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|
|
197
|
+
const stopCronJobs = _defaultRegistry.stop;
|
|
198
|
+
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|
|
199
|
+
const getScheduledTasks = _defaultRegistry.list;
|
|
164
200
|
//#endregion
|
|
165
|
-
export { collectCronTasks, createTaskFromProcedure, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs };
|
|
201
|
+
export { collectCronTasks, createCronRegistry, createTaskFromProcedure, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs };
|
package/dist/core/url.mjs
CHANGED
|
@@ -10,9 +10,27 @@
|
|
|
10
10
|
* Returns the path portion without query string.
|
|
11
11
|
*
|
|
12
12
|
* Uses manual indexOf — no `new URL()` overhead.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* Handles both absolute URLs (`http://host/path?q`) and bare paths
|
|
16
|
+
* (`/path?q`). The latter shape is produced by adapters that strip the
|
|
17
|
+
* origin before calling the handler, and by test harnesses constructing
|
|
18
|
+
* synthetic requests. Without the bare-path branch, a missing `//`
|
|
19
|
+
* caused `indexOf('//') + 2 = 1` and `indexOf('/', 1)` returned a bogus
|
|
20
|
+
* offset that silently produced the wrong path.
|
|
13
21
|
*/
|
|
14
22
|
function parseUrlPath(url) {
|
|
15
|
-
|
|
23
|
+
if (url.length > 0 && url.charCodeAt(0) === 47) {
|
|
24
|
+
const qMark = url.indexOf("?");
|
|
25
|
+
return qMark === -1 ? url : url.slice(0, qMark);
|
|
26
|
+
}
|
|
27
|
+
const schemeEnd = url.indexOf("//");
|
|
28
|
+
if (schemeEnd === -1) {
|
|
29
|
+
const qMark = url.indexOf("?");
|
|
30
|
+
return qMark === -1 ? url : url.slice(0, qMark);
|
|
31
|
+
}
|
|
32
|
+
const pathStart = url.indexOf("/", schemeEnd + 2);
|
|
33
|
+
if (pathStart === -1) return "/";
|
|
16
34
|
const qMark = url.indexOf("?", pathStart);
|
|
17
35
|
return qMark === -1 ? url.slice(pathStart) : url.slice(pathStart, qMark);
|
|
18
36
|
}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AnySchema, InferSchemaInput, InferSchemaOutput, Schema, ValidationError, type, validateSchema } from "./core/schema.mjs";
|
|
2
|
-
import { ScheduledTaskInfo, TaskDef, TaskEvent, collectCronTasks, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs } from "./core/task.mjs";
|
|
2
|
+
import { CronRegistry, ScheduledTaskInfo, TaskDef, TaskEvent, collectCronTasks, createCronRegistry, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs } from "./core/task.mjs";
|
|
3
3
|
import { ErrorDef, ErrorDefItem, FailFn, GuardDef, GuardFn, InferClient, InferContextFromUse, InferGuardOutput, Meta, MiddlewareDef, ProcedureDef, ProcedureType, ResolveContext, RouterDef, WrapDef, WrapFn } from "./types.mjs";
|
|
4
4
|
import { ConvertOptions, JSONSchema, SchemaConverter, SchemaRegistry, createSchemaRegistry, schemaToJsonSchema } from "./core/schema-converter.mjs";
|
|
5
5
|
import { ScalarOptions, generateOpenAPI, scalarHTML } from "./scalar.mjs";
|
|
@@ -9,8 +9,6 @@ import { ServeOptions, SilgiServer } from "./core/serve.mjs";
|
|
|
9
9
|
import { Driver, Storage, StorageConfig, StorageValue, initStorage, resetStorage, useStorage } from "./core/storage.mjs";
|
|
10
10
|
import { SilgiConfig, SilgiInstance, silgi } from "./silgi.mjs";
|
|
11
11
|
import { SilgiError, SilgiErrorCode, SilgiErrorJSON, SilgiErrorOptions, isDefinedError, isSilgiError, toSilgiError } from "./core/error.mjs";
|
|
12
|
-
import { DomainErrorMapper, mapDomainErrors } from "./error-mapper.mjs";
|
|
13
|
-
import { GuardDeps, GuardMap, RouteKit, RouteKitDeps, defineRouteKit } from "./route-kit.mjs";
|
|
14
12
|
import { BaseContext } from "./core/context.mjs";
|
|
15
13
|
import { AsyncIteratorClass, mapAsyncIterator } from "./core/iterator.mjs";
|
|
16
14
|
import { EventMeta, getEventMeta, withEventMeta } from "./core/sse.mjs";
|
|
@@ -20,4 +18,4 @@ import { mapInput } from "./map-input.mjs";
|
|
|
20
18
|
import { compileProcedure, compileRouter, createContext } from "./compile.mjs";
|
|
21
19
|
import { ProcedureSummary, collectProcedures, getProcedurePaths, isProcedureDef } from "./core/router-utils.mjs";
|
|
22
20
|
import { LazyRouter, isLazy, lazy, resolveLazy } from "./lazy.mjs";
|
|
23
|
-
export { type AnySchema, AsyncIteratorClass, type BaseContext, type CallableOptions, type ContextBridge, type ConvertOptions, type
|
|
21
|
+
export { type AnySchema, AsyncIteratorClass, type BaseContext, type CallableOptions, type ContextBridge, type ConvertOptions, type CronRegistry, type Driver, type ErrorDef, type ErrorDefItem, type EventMeta, type FailFn, type GuardDef, type GuardFn, type InferClient, type InferContextFromUse, type InferGuardOutput, type InferSchemaInput, type InferSchemaOutput, type JSONSchema, type LazyRouter, type LifecycleHooks, type Meta, type MiddlewareDef, type ProcedureBuilder, type ProcedureBuilderWithOutput, type ProcedureDef, type ProcedureSummary, type ProcedureType, type ResolveContext, type RouterDef, type ScalarOptions, type ScheduledTaskInfo, type Schema, type SchemaConverter, type SchemaRegistry, type ServeOptions, type SilgiConfig, SilgiError, type SilgiErrorCode, type SilgiErrorJSON, type SilgiErrorOptions, type SilgiInstance, type SilgiServer, type Storage, type StorageConfig, type StorageValue, type TaskDef, type TaskEvent, ValidationError, type WrapDef, type WrapFn, callable, collectCronTasks, collectProcedures, compileProcedure, compileRouter, createContext, createContextBridge, createCronRegistry, createSchemaRegistry, generateOpenAPI, getEventMeta, getProcedurePaths, getScheduledTasks, initStorage, isDefinedError, isLazy, isProcedureDef, isSilgiError, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, schemaToJsonSchema, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ValidationError, type, validateSchema } from "./core/schema.mjs";
|
|
2
|
-
import { collectCronTasks, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs } from "./core/task.mjs";
|
|
2
|
+
import { collectCronTasks, createCronRegistry, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs } from "./core/task.mjs";
|
|
3
3
|
import { SilgiError, isDefinedError, isSilgiError, toSilgiError } from "./core/error.mjs";
|
|
4
4
|
import { collectProcedures, getProcedurePaths, isProcedureDef } from "./core/router-utils.mjs";
|
|
5
5
|
import { compileProcedure, compileRouter, createContext } from "./compile.mjs";
|
|
@@ -8,12 +8,10 @@ import { AsyncIteratorClass, mapAsyncIterator } from "./core/iterator.mjs";
|
|
|
8
8
|
import { getEventMeta, withEventMeta } from "./core/sse.mjs";
|
|
9
9
|
import { createSchemaRegistry, schemaToJsonSchema } from "./core/schema-converter.mjs";
|
|
10
10
|
import { silgi } from "./silgi.mjs";
|
|
11
|
-
import { mapDomainErrors } from "./error-mapper.mjs";
|
|
12
|
-
import { defineRouteKit } from "./route-kit.mjs";
|
|
13
11
|
import { callable } from "./callable.mjs";
|
|
14
12
|
import { lifecycleWrap } from "./lifecycle.mjs";
|
|
15
13
|
import { mapInput } from "./map-input.mjs";
|
|
16
14
|
import { isLazy, lazy, resolveLazy } from "./lazy.mjs";
|
|
17
15
|
import { initStorage, resetStorage, useStorage } from "./core/storage.mjs";
|
|
18
16
|
import { generateOpenAPI, scalarHTML } from "./scalar.mjs";
|
|
19
|
-
export { AsyncIteratorClass, SilgiError, ValidationError, callable, collectCronTasks, collectProcedures, compileProcedure, compileRouter, createContext, createContextBridge,
|
|
17
|
+
export { AsyncIteratorClass, SilgiError, ValidationError, callable, collectCronTasks, collectProcedures, compileProcedure, compileRouter, createContext, createContextBridge, createCronRegistry, createSchemaRegistry, generateOpenAPI, getEventMeta, getProcedurePaths, getScheduledTasks, initStorage, isDefinedError, isLazy, isProcedureDef, isSilgiError, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, schemaToJsonSchema, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
|
|
@@ -48,6 +48,14 @@ function runWithCtx(ctx, fn) {
|
|
|
48
48
|
* Keyed on `Request` identity. GC-friendly: entry auto-released when the
|
|
49
49
|
* `Request` is collected.
|
|
50
50
|
*
|
|
51
|
+
* @remarks
|
|
52
|
+
* This is a documented exception to ARCHITECTURE §3 ("no WeakMap keyed on
|
|
53
|
+
* Request identity"). Kept only because `setRequestContext` is a public
|
|
54
|
+
* API consumed by user code outside silgi's control — removing it is a
|
|
55
|
+
* breaking change reserved for the next major. New integrations must
|
|
56
|
+
* use `silgi.runInContext(ctx, fn)` or the per-plugin closure pattern
|
|
57
|
+
* (see `requestMetas` inside `tracing()` below).
|
|
58
|
+
*
|
|
51
59
|
* @internal
|
|
52
60
|
*/
|
|
53
61
|
const _requestContextMap = /* @__PURE__ */ new WeakMap();
|
|
@@ -186,7 +194,6 @@ function extractUserData(returned) {
|
|
|
186
194
|
}
|
|
187
195
|
return result;
|
|
188
196
|
}
|
|
189
|
-
const requestMetas = /* @__PURE__ */ new WeakMap();
|
|
190
197
|
/**
|
|
191
198
|
* Creates a Better Auth plugin that auto-traces all auth operations
|
|
192
199
|
* into silgi analytics spans.
|
|
@@ -197,6 +204,8 @@ const requestMetas = /* @__PURE__ */ new WeakMap();
|
|
|
197
204
|
function tracing(config) {
|
|
198
205
|
const captureInput = config?.captureInput ?? true;
|
|
199
206
|
const captureOutput = config?.captureOutput ?? true;
|
|
207
|
+
const wrapMiddleware = config?.createAuthMiddleware ?? ((fn) => fn);
|
|
208
|
+
const requestMetas = /* @__PURE__ */ new WeakMap();
|
|
200
209
|
return {
|
|
201
210
|
id: "silgi-tracing",
|
|
202
211
|
onRequest: async (request, _ctx) => {
|
|
@@ -215,7 +224,7 @@ function tracing(config) {
|
|
|
215
224
|
},
|
|
216
225
|
hooks: { after: [{
|
|
217
226
|
matcher: () => true,
|
|
218
|
-
handler: (
|
|
227
|
+
handler: wrapMiddleware(async (ctx) => {
|
|
219
228
|
try {
|
|
220
229
|
const request = ctx.request;
|
|
221
230
|
if (!request) return;
|
package/dist/lazy.mjs
CHANGED
package/dist/map-input.d.mts
CHANGED
|
@@ -4,13 +4,15 @@ import { WrapDef } from "./types.mjs";
|
|
|
4
4
|
/**
|
|
5
5
|
* Create a wrap that transforms the procedure input before execution.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* The mapper receives the raw input and returns the transformed input.
|
|
9
|
+
* Internally this wrap replaces the value in the pipeline's `RAW_INPUT`
|
|
10
|
+
* symbol slot on `ctx`; the resolver reads the same slot to get the
|
|
11
|
+
* rewritten input. Users never interact with the slot directly — it's
|
|
12
|
+
* framework-internal (see `src/core/ctx-symbols.ts`).
|
|
10
13
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* before calling next().
|
|
14
|
+
* Must run inside the wrap onion (not as a guard) so it can observe and
|
|
15
|
+
* mutate the already-parsed input after schema validation.
|
|
14
16
|
*/
|
|
15
17
|
declare function mapInput<TIn = unknown, TOut = unknown>(mapper: (input: TIn) => TOut | Promise<TOut>): WrapDef;
|
|
16
18
|
//#endregion
|
package/dist/map-input.mjs
CHANGED
|
@@ -25,13 +25,15 @@ import { RAW_INPUT } from "./core/ctx-symbols.mjs";
|
|
|
25
25
|
/**
|
|
26
26
|
* Create a wrap that transforms the procedure input before execution.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
* The
|
|
30
|
-
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* The mapper receives the raw input and returns the transformed input.
|
|
30
|
+
* Internally this wrap replaces the value in the pipeline's `RAW_INPUT`
|
|
31
|
+
* symbol slot on `ctx`; the resolver reads the same slot to get the
|
|
32
|
+
* rewritten input. Users never interact with the slot directly — it's
|
|
33
|
+
* framework-internal (see `src/core/ctx-symbols.ts`).
|
|
31
34
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* before calling next().
|
|
35
|
+
* Must run inside the wrap onion (not as a guard) so it can observe and
|
|
36
|
+
* mutate the already-parsed input after schema validation.
|
|
35
37
|
*/
|
|
36
38
|
function mapInput(mapper) {
|
|
37
39
|
return {
|
|
@@ -107,6 +107,22 @@ function parseAnalyticsDetailPath(pathname, prefix) {
|
|
|
107
107
|
function jsonResponse(data, headers) {
|
|
108
108
|
return new Response(JSON.stringify(data), { headers });
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Parse a request body as JSON without throwing. Returns `null` on any
|
|
112
|
+
* failure (empty body, malformed JSON, non-JSON content). Used by the
|
|
113
|
+
* analytics hidden-paths endpoint so a bad request produces a 400 instead
|
|
114
|
+
* of an uncaught exception that surfaces as a 500 to the client.
|
|
115
|
+
*/
|
|
116
|
+
async function parseJsonBody(request) {
|
|
117
|
+
try {
|
|
118
|
+
const text = await request.text();
|
|
119
|
+
if (!text) return null;
|
|
120
|
+
const parsed = JSON.parse(text);
|
|
121
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
110
126
|
/** Serve analytics dashboard and API routes. */
|
|
111
127
|
async function serveAnalyticsRoute(pathname, request, collector, dashboardHtml) {
|
|
112
128
|
const jsonCacheHeaders = {
|
|
@@ -122,24 +138,20 @@ async function serveAnalyticsRoute(pathname, request, collector, dashboardHtml)
|
|
|
122
138
|
if (pathname === "api/analytics/stats") return jsonResponse(collector.toJSON(), jsonCacheHeaders);
|
|
123
139
|
if (pathname === "api/analytics/hidden") {
|
|
124
140
|
if (request.method === "GET") return jsonResponse(collector.getHiddenPaths(), jsonCacheHeaders);
|
|
125
|
-
if (request.method === "POST") {
|
|
126
|
-
const body = await request
|
|
127
|
-
if (typeof body.path !== "string") return new Response("{\"error\":\"path required\"}", {
|
|
128
|
-
status: 400,
|
|
129
|
-
headers: jsonCacheHeaders
|
|
130
|
-
});
|
|
131
|
-
collector.addHiddenPath(body.path);
|
|
132
|
-
return jsonResponse(collector.getHiddenPaths(), jsonCacheHeaders);
|
|
133
|
-
}
|
|
134
|
-
if (request.method === "DELETE") {
|
|
135
|
-
const body = await request.json();
|
|
136
|
-
if (typeof body.path !== "string") return new Response("{\"error\":\"path required\"}", {
|
|
141
|
+
if (request.method === "POST" || request.method === "DELETE") {
|
|
142
|
+
const body = await parseJsonBody(request);
|
|
143
|
+
if (!body || typeof body.path !== "string") return new Response("{\"error\":\"path required\"}", {
|
|
137
144
|
status: 400,
|
|
138
145
|
headers: jsonCacheHeaders
|
|
139
146
|
});
|
|
140
|
-
collector.
|
|
147
|
+
if (request.method === "POST") collector.addHiddenPath(body.path);
|
|
148
|
+
else collector.removeHiddenPath(body.path);
|
|
141
149
|
return jsonResponse(collector.getHiddenPaths(), jsonCacheHeaders);
|
|
142
150
|
}
|
|
151
|
+
return new Response("{\"error\":\"method not allowed\"}", {
|
|
152
|
+
status: 405,
|
|
153
|
+
headers: jsonCacheHeaders
|
|
154
|
+
});
|
|
143
155
|
}
|
|
144
156
|
if (pathname === "api/analytics/errors") return jsonResponse(queryErrors((await collector.getErrors()).filter((e) => !collector.isHidden(e.procedure)), parseQueryParams(url.searchParams)), jsonCacheHeaders);
|
|
145
157
|
if (pathname === "api/analytics/requests") return jsonResponse(queryRequests((await collector.getRequests()).filter((r) => !collector.isHidden(r.path)), parseQueryParams(url.searchParams)), jsonCacheHeaders);
|
package/dist/plugins/cache.mjs
CHANGED
|
@@ -48,11 +48,8 @@ function ensureStorageConnected() {
|
|
|
48
48
|
setStorage({
|
|
49
49
|
get: (key) => storage.getItem(key),
|
|
50
50
|
set: (key, value, opts) => {
|
|
51
|
-
if (value === null || value === void 0)
|
|
52
|
-
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
storage.setItem(key, value, opts?.ttl ? { ttl: opts.ttl } : void 0);
|
|
51
|
+
if (value === null || value === void 0) return storage.removeItem(key);
|
|
52
|
+
return storage.setItem(key, value, opts?.ttl ? { ttl: opts.ttl } : void 0);
|
|
56
53
|
}
|
|
57
54
|
});
|
|
58
55
|
} catch {}
|
|
@@ -200,11 +197,8 @@ function createUnstorageAdapter(storage) {
|
|
|
200
197
|
return {
|
|
201
198
|
get: (key) => storage.getItem(key),
|
|
202
199
|
set: (key, value, opts) => {
|
|
203
|
-
if (value === null || value === void 0)
|
|
204
|
-
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
storage.setItem(key, value, opts);
|
|
200
|
+
if (value === null || value === void 0) return storage.removeItem(key);
|
|
201
|
+
return storage.setItem(key, value, opts);
|
|
208
202
|
}
|
|
209
203
|
};
|
|
210
204
|
}
|
|
@@ -13,8 +13,9 @@ import { WrapDef } from "../types.mjs";
|
|
|
13
13
|
* - "" → undefined (empty strings become undefined)
|
|
14
14
|
* - Everything else → kept as-is
|
|
15
15
|
*
|
|
16
|
-
* Implemented as a wrap
|
|
17
|
-
*
|
|
16
|
+
* Implemented as a wrap so it runs after the pipeline has populated the
|
|
17
|
+
* `RAW_INPUT` slot on ctx — see the caveat in the top-level docstring
|
|
18
|
+
* about ordering vs. input schema validation.
|
|
18
19
|
*/
|
|
19
20
|
declare const coerceGuard: WrapDef<Record<string, unknown>>;
|
|
20
21
|
declare function coerceValue(value: unknown): unknown;
|
package/dist/plugins/coerce.mjs
CHANGED
|
@@ -7,17 +7,33 @@ import { RAW_INPUT } from "../core/ctx-symbols.mjs";
|
|
|
7
7
|
* query parameters. This wrap coerces common types automatically:
|
|
8
8
|
* "123" → 123, "true" → true, "null" → null, etc.
|
|
9
9
|
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* **Ordering caveat.** The pipeline validates `$input` schemas *before*
|
|
12
|
+
* running the wrap onion. That means a plain `z.number()` rejects "123"
|
|
13
|
+
* before this wrap can see it. You have three options:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Use Zod's own coercion** — `z.coerce.number()`, `z.coerce.boolean()`.
|
|
16
|
+
* Zero extra wrap, works for simple cases.
|
|
17
|
+
* 2. **Skip the input schema and validate inside the resolver** — pairs
|
|
18
|
+
* naturally with `coerceGuard` because the wrap always runs first.
|
|
19
|
+
* 3. **Use a string-accepting schema and coerce with `.transform()`** —
|
|
20
|
+
* e.g. `z.object({ id: z.string().transform(Number) })`. Again no
|
|
21
|
+
* wrap needed.
|
|
22
|
+
*
|
|
23
|
+
* `coerceGuard` itself is useful when you have NO input schema but still
|
|
24
|
+
* want `"42"` / `"true"` / `"null"` normalised before your resolver runs.
|
|
25
|
+
*
|
|
10
26
|
* @example
|
|
11
27
|
* ```ts
|
|
12
|
-
*
|
|
13
|
-
*
|
|
28
|
+
* // Works: no schema → wrap can reshape freely.
|
|
14
29
|
* const getUser = k
|
|
15
30
|
* .$use(coerceGuard)
|
|
16
|
-
* .$
|
|
17
|
-
* .$resolve(({ input }) => db.users.find(input.id))
|
|
31
|
+
* .$resolve(({ input }) => db.users.find((input as any).id))
|
|
18
32
|
*
|
|
19
|
-
* //
|
|
20
|
-
*
|
|
33
|
+
* // Works: schema uses z.coerce.
|
|
34
|
+
* const getUser2 = k
|
|
35
|
+
* .$input(z.object({ id: z.coerce.number() }))
|
|
36
|
+
* .$resolve(({ input }) => db.users.find(input.id))
|
|
21
37
|
* ```
|
|
22
38
|
*/
|
|
23
39
|
/**
|
|
@@ -32,8 +48,9 @@ import { RAW_INPUT } from "../core/ctx-symbols.mjs";
|
|
|
32
48
|
* - "" → undefined (empty strings become undefined)
|
|
33
49
|
* - Everything else → kept as-is
|
|
34
50
|
*
|
|
35
|
-
* Implemented as a wrap
|
|
36
|
-
*
|
|
51
|
+
* Implemented as a wrap so it runs after the pipeline has populated the
|
|
52
|
+
* `RAW_INPUT` slot on ctx — see the caveat in the top-level docstring
|
|
53
|
+
* about ordering vs. input schema validation.
|
|
37
54
|
*/
|
|
38
55
|
const coerceGuard = {
|
|
39
56
|
kind: "wrap",
|
package/dist/silgi.d.mts
CHANGED
|
@@ -85,6 +85,41 @@ interface SilgiConfig<TCtx extends Record<string, unknown>> {
|
|
|
85
85
|
* ```
|
|
86
86
|
*/
|
|
87
87
|
schemaConverters?: SchemaConverter[];
|
|
88
|
+
/**
|
|
89
|
+
* Root-level wrap middleware applied to every procedure in the router.
|
|
90
|
+
*
|
|
91
|
+
* @remarks
|
|
92
|
+
* Each entry must be created via `instance.wrap(fn)`. Root wraps run
|
|
93
|
+
* as the outermost layer of the onion: root wraps → route-level
|
|
94
|
+
* `.$use()` guards/wraps → resolver. Use this for concerns that must
|
|
95
|
+
* apply to every route (tenant scoping, `AsyncLocalStorage` setup,
|
|
96
|
+
* trace propagation), where missing one route would be a bug.
|
|
97
|
+
*
|
|
98
|
+
* Root wraps cannot mutate the context type — use a route-level
|
|
99
|
+
* `$use(guard)` for that. The ambient context passed to `next()` is
|
|
100
|
+
* `TBaseCtx`, identical to the one seen by route-level wraps.
|
|
101
|
+
*
|
|
102
|
+
* Applies to every procedure reachable through `handler()`,
|
|
103
|
+
* `createCaller()`, and HTTP/cron task invocation. Task `dispatch()`
|
|
104
|
+
* (programmatic, bypasses the pipeline) is not wrapped.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* const tenantScopeWrap: WrapDef = {
|
|
109
|
+
* kind: 'wrap',
|
|
110
|
+
* fn: (ctx, next) => tenantScope.run({ orgId: ctx.user.orgId }, next),
|
|
111
|
+
* }
|
|
112
|
+
*
|
|
113
|
+
* const s = silgi({
|
|
114
|
+
* context: (req) => ({ db, user: readUser(req) }),
|
|
115
|
+
* wraps: [tenantScopeWrap],
|
|
116
|
+
* })
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* For convenience you can also create wraps with the standalone
|
|
120
|
+
* helper or from another silgi instance's `wrap()` method.
|
|
121
|
+
*/
|
|
122
|
+
wraps?: WrapDef<TCtx>[];
|
|
88
123
|
/**
|
|
89
124
|
* Storage configuration — mount drivers by path prefix.
|
|
90
125
|
*
|
package/dist/silgi.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createTaskFromProcedure } from "./core/task.mjs";
|
|
2
2
|
import { createProcedureBuilder } from "./builder.mjs";
|
|
3
3
|
import { assignPaths, routerCache } from "./core/router-utils.mjs";
|
|
4
|
+
import { ROOT_WRAPS } from "./core/ctx-symbols.mjs";
|
|
4
5
|
import { compileRouter } from "./compile.mjs";
|
|
5
6
|
import { createCaller } from "./caller.mjs";
|
|
6
7
|
import { createContextBridge } from "./core/context-bridge.mjs";
|
|
@@ -45,6 +46,26 @@ function createProcedure(type, ...args) {
|
|
|
45
46
|
throw new TypeError(`Invalid arguments for ${type}()`);
|
|
46
47
|
}
|
|
47
48
|
/**
|
|
49
|
+
* Stamp root wraps onto a router def as a non-enumerable Symbol-keyed
|
|
50
|
+
* property. Idempotent — if the def is already branded with the same
|
|
51
|
+
* reference, this is a no-op. Multiple silgi instances registering the
|
|
52
|
+
* same def throws, because a single compiled router cannot serve two
|
|
53
|
+
* context shapes.
|
|
54
|
+
*
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
function stampRootWraps(def, wraps) {
|
|
58
|
+
const existing = def[ROOT_WRAPS];
|
|
59
|
+
if (existing === wraps) return;
|
|
60
|
+
if (existing) throw new TypeError("silgi: this router def is already registered with a different silgi instance — build a fresh router object.");
|
|
61
|
+
Object.defineProperty(def, ROOT_WRAPS, {
|
|
62
|
+
value: wraps,
|
|
63
|
+
enumerable: false,
|
|
64
|
+
writable: false,
|
|
65
|
+
configurable: false
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
48
69
|
* Create a Silgi RPC instance with typed context.
|
|
49
70
|
*
|
|
50
71
|
* @remarks
|
|
@@ -88,6 +109,12 @@ function silgi(config) {
|
|
|
88
109
|
m.initStorage(config.storage);
|
|
89
110
|
}) : Promise.resolve();
|
|
90
111
|
const ctxFactory = () => contextFactory(new Request("http://localhost"));
|
|
112
|
+
let rootWraps = null;
|
|
113
|
+
if (config.wraps && config.wraps.length > 0) {
|
|
114
|
+
for (const w of config.wraps) if (!w || w.kind !== "wrap") throw new TypeError("silgi({ wraps }) only accepts wrap middleware — use route-level .$use() for guards.");
|
|
115
|
+
rootWraps = Object.freeze([...config.wraps]);
|
|
116
|
+
}
|
|
117
|
+
const rootWrapsGetter = rootWraps ? () => rootWraps : null;
|
|
91
118
|
return {
|
|
92
119
|
hook: hooks.hook.bind(hooks),
|
|
93
120
|
removeHook: hooks.removeHook.bind(hooks),
|
|
@@ -113,22 +140,26 @@ function silgi(config) {
|
|
|
113
140
|
fn
|
|
114
141
|
}),
|
|
115
142
|
$resolve: ((fn) => createProcedure("query", fn)),
|
|
116
|
-
$input: ((schema) => createProcedureBuilder("query", ctxFactory).$input(schema)),
|
|
143
|
+
$input: ((schema) => createProcedureBuilder("query", ctxFactory, rootWrapsGetter).$input(schema)),
|
|
117
144
|
$use: ((...middleware) => {
|
|
118
|
-
const b = createProcedureBuilder("query", ctxFactory);
|
|
145
|
+
const b = createProcedureBuilder("query", ctxFactory, rootWrapsGetter);
|
|
119
146
|
for (const m of middleware) b.$use(m);
|
|
120
147
|
return b;
|
|
121
148
|
}),
|
|
122
|
-
$output: ((schema) => createProcedureBuilder("query", ctxFactory).$output(schema)),
|
|
123
|
-
$errors: ((errors) => createProcedureBuilder("query", ctxFactory).$errors(errors)),
|
|
124
|
-
$route: ((route) => createProcedureBuilder("query", ctxFactory).$route(route)),
|
|
125
|
-
$meta: ((meta) => createProcedureBuilder("query", ctxFactory).$meta(meta)),
|
|
149
|
+
$output: ((schema) => createProcedureBuilder("query", ctxFactory, rootWrapsGetter).$output(schema)),
|
|
150
|
+
$errors: ((errors) => createProcedureBuilder("query", ctxFactory, rootWrapsGetter).$errors(errors)),
|
|
151
|
+
$route: ((route) => createProcedureBuilder("query", ctxFactory, rootWrapsGetter).$route(route)),
|
|
152
|
+
$meta: ((meta) => createProcedureBuilder("query", ctxFactory, rootWrapsGetter).$meta(meta)),
|
|
126
153
|
subscription: ((...args) => createProcedure("subscription", ...args)),
|
|
127
154
|
$task: ((config) => {
|
|
128
|
-
return createTaskFromProcedure(config, config.resolve, null, null, ctxFactory);
|
|
155
|
+
return createTaskFromProcedure(config, config.resolve, null, null, ctxFactory, rootWrapsGetter);
|
|
129
156
|
}),
|
|
130
157
|
router: (def) => {
|
|
131
158
|
const assigned = assignPaths(def);
|
|
159
|
+
if (rootWraps) {
|
|
160
|
+
stampRootWraps(def, rootWraps);
|
|
161
|
+
stampRootWraps(assigned, rootWraps);
|
|
162
|
+
}
|
|
132
163
|
const flat = compileRouter(assigned);
|
|
133
164
|
routerCache.set(def, flat);
|
|
134
165
|
routerCache.set(assigned, flat);
|
package/dist/ws.mjs
CHANGED
|
@@ -14,8 +14,6 @@ import { decode, encode } from "./codec/msgpack.mjs";
|
|
|
14
14
|
* Server → Client: { id: string, result?: unknown, error?: unknown }
|
|
15
15
|
* Server → Client (stream): { id: string, data: unknown, done?: boolean }
|
|
16
16
|
*/
|
|
17
|
-
const peerAbortControllers = /* @__PURE__ */ new WeakMap();
|
|
18
|
-
const peerKeepaliveTimers = /* @__PURE__ */ new WeakMap();
|
|
19
17
|
/**
|
|
20
18
|
* Internal — build crossws-compatible hooks for Silgi RPC over WebSocket.
|
|
21
19
|
*
|
|
@@ -28,6 +26,8 @@ function _createWSHooks(routerDef, options = {}) {
|
|
|
28
26
|
const useMsgpack = options.protocol === "messagepack" || options.protocol == null && (options.binary ?? false);
|
|
29
27
|
const contextFactory = options.context;
|
|
30
28
|
const keepaliveMs = options.keepalive === false ? 0 : options.keepalive ?? 3e4;
|
|
29
|
+
const peerAbortControllers = /* @__PURE__ */ new WeakMap();
|
|
30
|
+
const peerKeepaliveTimers = /* @__PURE__ */ new WeakMap();
|
|
31
31
|
function send(peer, data) {
|
|
32
32
|
const compress = !!options.compress;
|
|
33
33
|
if (useMsgpack) peer.send(encode(data), { compress });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "silgi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The fastest end-to-end type-safe RPC framework for TypeScript — compiled pipelines, single package, every runtime",
|
|
6
6
|
"keywords": [
|
|
@@ -41,9 +41,7 @@
|
|
|
41
41
|
"lib"
|
|
42
42
|
],
|
|
43
43
|
"type": "module",
|
|
44
|
-
"sideEffects":
|
|
45
|
-
"./dist/integrations/zod/index.mjs"
|
|
46
|
-
],
|
|
44
|
+
"sideEffects": false,
|
|
47
45
|
"exports": {
|
|
48
46
|
".": {
|
|
49
47
|
"import": "./dist/index.mjs",
|
package/dist/error-mapper.d.mts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { SilgiError } from "./core/error.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/error-mapper.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* Map a caught error into a {@link SilgiError}, or return `undefined` to
|
|
6
|
-
* rethrow the original error untouched. `SilgiError` instances always
|
|
7
|
-
* pass through unchanged — the mapper is not invoked for them.
|
|
8
|
-
*/
|
|
9
|
-
type DomainErrorMapper = (error: unknown) => SilgiError | undefined;
|
|
10
|
-
/**
|
|
11
|
-
* Create a resolver wrapper that runs `mapper` on every thrown error.
|
|
12
|
-
*
|
|
13
|
-
* @param mapper - Called with the caught error; return a `SilgiError` to
|
|
14
|
-
* replace it, or `undefined` to rethrow the original.
|
|
15
|
-
* @returns A function that wraps a resolver and applies the mapping.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```ts
|
|
19
|
-
* const handleErrors = mapDomainErrors((e) => {
|
|
20
|
-
* if (e instanceof MyDomainError) {
|
|
21
|
-
* return new SilgiError(e.code, { status: e.status, message: e.message, defined: true })
|
|
22
|
-
* }
|
|
23
|
-
* })
|
|
24
|
-
*
|
|
25
|
-
* k.$resolve(handleErrors(async ({ input, ctx }) => {
|
|
26
|
-
* return await service.run(input, ctx)
|
|
27
|
-
* }))
|
|
28
|
-
* ```
|
|
29
|
-
*/
|
|
30
|
-
declare function mapDomainErrors(mapper: DomainErrorMapper): <TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn | Promise<TReturn>) => (...args: TArgs) => Promise<TReturn>;
|
|
31
|
-
//#endregion
|
|
32
|
-
export { DomainErrorMapper, mapDomainErrors };
|
package/dist/error-mapper.mjs
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { isSilgiError } from "./core/error.mjs";
|
|
2
|
-
//#region src/error-mapper.ts
|
|
3
|
-
/**
|
|
4
|
-
* mapDomainErrors — convert service-layer errors into SilgiError.
|
|
5
|
-
*
|
|
6
|
-
* @remarks
|
|
7
|
-
* Library service layers typically throw a domain error (no HTTP
|
|
8
|
-
* knowledge), and route handlers convert them to {@link SilgiError}
|
|
9
|
-
* before returning. Writing the same try/catch wrapper in every
|
|
10
|
-
* resolver is boilerplate; `mapDomainErrors` replaces it with a single
|
|
11
|
-
* mapper function that runs on every thrown error.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```ts
|
|
15
|
-
* class DomainError extends Error {
|
|
16
|
-
* constructor(public code: string, public status: number, message: string) { super(message) }
|
|
17
|
-
* }
|
|
18
|
-
*
|
|
19
|
-
* const toSilgi = mapDomainErrors((e) => {
|
|
20
|
-
* if (e instanceof DomainError) {
|
|
21
|
-
* return new SilgiError(e.code, { status: e.status, message: e.message, defined: true })
|
|
22
|
-
* }
|
|
23
|
-
* })
|
|
24
|
-
*
|
|
25
|
-
* k.$resolve(toSilgi(async ({ input }) => service.doThing(input)))
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
/**
|
|
29
|
-
* Create a resolver wrapper that runs `mapper` on every thrown error.
|
|
30
|
-
*
|
|
31
|
-
* @param mapper - Called with the caught error; return a `SilgiError` to
|
|
32
|
-
* replace it, or `undefined` to rethrow the original.
|
|
33
|
-
* @returns A function that wraps a resolver and applies the mapping.
|
|
34
|
-
*
|
|
35
|
-
* @example
|
|
36
|
-
* ```ts
|
|
37
|
-
* const handleErrors = mapDomainErrors((e) => {
|
|
38
|
-
* if (e instanceof MyDomainError) {
|
|
39
|
-
* return new SilgiError(e.code, { status: e.status, message: e.message, defined: true })
|
|
40
|
-
* }
|
|
41
|
-
* })
|
|
42
|
-
*
|
|
43
|
-
* k.$resolve(handleErrors(async ({ input, ctx }) => {
|
|
44
|
-
* return await service.run(input, ctx)
|
|
45
|
-
* }))
|
|
46
|
-
* ```
|
|
47
|
-
*/
|
|
48
|
-
function mapDomainErrors(mapper) {
|
|
49
|
-
return function wrap(fn) {
|
|
50
|
-
return async (...args) => {
|
|
51
|
-
try {
|
|
52
|
-
return await fn(...args);
|
|
53
|
-
} catch (e) {
|
|
54
|
-
if (isSilgiError(e)) throw e;
|
|
55
|
-
const mapped = mapper(e);
|
|
56
|
-
if (mapped) throw mapped;
|
|
57
|
-
throw e;
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
//#endregion
|
|
63
|
-
export { mapDomainErrors };
|
package/dist/route-kit.d.mts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { ErrorDef, GuardDef } from "./types.mjs";
|
|
2
|
-
import { SilgiInstance } from "./silgi.mjs";
|
|
3
|
-
|
|
4
|
-
//#region src/route-kit.d.ts
|
|
5
|
-
/**
|
|
6
|
-
* Shape of a `guards` map passed to a kit route. Each entry declares the
|
|
7
|
-
* context additions that specific guard contributes.
|
|
8
|
-
*/
|
|
9
|
-
type GuardMap = Record<string, Record<string, unknown> | void>;
|
|
10
|
-
/** Convert a `GuardMap` into the deps object shape passed to kit builders. */
|
|
11
|
-
type GuardDeps<TGuards extends GuardMap> = { [K in keyof TGuards]: GuardDef<any, TGuards[K], ErrorDef> };
|
|
12
|
-
/** Deps injected into a kit route builder — the instance plus the typed guards. */
|
|
13
|
-
type RouteKitDeps<TCtx extends Record<string, unknown>, TGuards extends GuardMap> = {
|
|
14
|
-
s: SilgiInstance<TCtx>;
|
|
15
|
-
} & GuardDeps<TGuards>;
|
|
16
|
-
/**
|
|
17
|
-
* Return value of `defineRouteKit<Ctx>()`.
|
|
18
|
-
*
|
|
19
|
-
* @remarks
|
|
20
|
-
* Use {@link RouteKit.route} to declare a single route that depends on a
|
|
21
|
-
* named set of guards. The kit carries no runtime state — it only
|
|
22
|
-
* binds the ctx shape for inference.
|
|
23
|
-
*/
|
|
24
|
-
interface RouteKit<TCtx extends Record<string, unknown>> {
|
|
25
|
-
/**
|
|
26
|
-
* Declare a route factory.
|
|
27
|
-
*
|
|
28
|
-
* @typeParam TGuards - Map of guard name → context additions. Empty by
|
|
29
|
-
* default; pass an explicit shape when the route depends on guards.
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* ```ts
|
|
33
|
-
* kit.route<{ auth: { user: User } }>()(({ s, auth }) =>
|
|
34
|
-
* s.$use(auth).$resolve(({ ctx }) => ctx.user)
|
|
35
|
-
* )
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
|
-
route: <TGuards extends GuardMap = {}>() => <TReturn>(builder: (deps: RouteKitDeps<TCtx, TGuards>) => TReturn) => (deps: RouteKitDeps<TCtx, TGuards>) => TReturn;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Create a context-bound route kit for isolated packages.
|
|
42
|
-
*
|
|
43
|
-
* @typeParam TCtx - Base context shape the server will provide. Flows
|
|
44
|
-
* into every route's resolver through the injected `s` instance.
|
|
45
|
-
*/
|
|
46
|
-
declare function defineRouteKit<TCtx extends Record<string, unknown>>(): RouteKit<TCtx>;
|
|
47
|
-
//#endregion
|
|
48
|
-
export { GuardDeps, GuardMap, RouteKit, RouteKitDeps, defineRouteKit };
|
package/dist/route-kit.mjs
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
//#region src/route-kit.ts
|
|
2
|
-
/**
|
|
3
|
-
* Create a context-bound route kit for isolated packages.
|
|
4
|
-
*
|
|
5
|
-
* @typeParam TCtx - Base context shape the server will provide. Flows
|
|
6
|
-
* into every route's resolver through the injected `s` instance.
|
|
7
|
-
*/
|
|
8
|
-
function defineRouteKit() {
|
|
9
|
-
return { route: () => (builder) => builder };
|
|
10
|
-
}
|
|
11
|
-
//#endregion
|
|
12
|
-
export { defineRouteKit };
|