silgi 0.52.2 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/aws-lambda.mjs +1 -1
- package/dist/broker/redis.mjs +1 -2
- package/dist/builder.mjs +35 -7
- package/dist/caller.mjs +67 -50
- package/dist/client/plugins/retry.mjs +9 -4
- package/dist/compile.d.mts +17 -10
- package/dist/compile.mjs +161 -144
- package/dist/core/ctx-symbols.mjs +18 -1
- package/dist/core/handler.d.mts +3 -3
- package/dist/core/handler.mjs +94 -83
- package/dist/core/input.mjs +116 -37
- package/dist/core/schema-converter.d.mts +68 -63
- package/dist/core/schema-converter.mjs +85 -56
- package/dist/core/serve.d.mts +18 -17
- package/dist/core/serve.mjs +154 -64
- package/dist/core/sse.d.mts +5 -6
- package/dist/core/sse.mjs +86 -46
- package/dist/core/task.d.mts +36 -8
- package/dist/core/task.mjs +210 -90
- package/dist/core/url.mjs +19 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- 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.d.mts +62 -126
- package/dist/plugins/cache.mjs +146 -134
- package/dist/plugins/coerce.d.mts +3 -2
- package/dist/plugins/coerce.mjs +25 -8
- package/dist/scalar.d.mts +24 -13
- package/dist/scalar.mjs +292 -201
- package/dist/silgi.d.mts +35 -0
- package/dist/silgi.mjs +177 -103
- package/dist/ws.d.mts +26 -27
- package/dist/ws.mjs +128 -89
- package/package.json +2 -4
package/dist/core/handler.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { routerCache } from "./router-utils.mjs";
|
|
2
|
-
import { compileRouter
|
|
2
|
+
import { compileRouter } from "../compile.mjs";
|
|
3
3
|
import { applyContext } from "./dispatch.mjs";
|
|
4
4
|
import { detectResponseFormat, encodeResponse, makeErrorResponse } from "./codec.mjs";
|
|
5
5
|
import { parseInput } from "./input.mjs";
|
|
@@ -7,58 +7,45 @@ import { iteratorToEventStream } from "./sse.mjs";
|
|
|
7
7
|
import { parseUrlPath } from "./url.mjs";
|
|
8
8
|
//#region src/core/handler.ts
|
|
9
9
|
/**
|
|
10
|
-
* Fetch API handler
|
|
10
|
+
* Fetch API handler
|
|
11
|
+
* -------------------
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* Every adapter that speaks the Fetch API (Next.js App Router, SvelteKit,
|
|
14
|
+
* srvx, Bun, Cloudflare Workers, Deno) ends up calling the handler built
|
|
15
|
+
* here. It is the single place that turns a `Request` into a `Response`
|
|
16
|
+
* by running the compiled pipeline produced by `compileRouter`.
|
|
14
17
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
18
|
+
* Responsibilities, in order:
|
|
19
|
+
*
|
|
20
|
+
* 1. URL parsing and `basePath` stripping.
|
|
21
|
+
* 2. Route lookup + HTTP method enforcement.
|
|
22
|
+
* 3. Context construction (factory + optional `AsyncLocalStorage` bridge).
|
|
23
|
+
* 4. `request:prepare` hook so plugins (analytics, etc.) can seed `ctx`.
|
|
24
|
+
* 5. Input parsing (body / query / URL params).
|
|
25
|
+
* 6. Pipeline execution — the compiled handler from `compileProcedure`.
|
|
26
|
+
* 7. Response encoding (JSON, msgpack, stream, SSE, raw `Response`).
|
|
27
|
+
*
|
|
28
|
+
* Analytics and the Scalar UI are layered on top via `wrapHandler` — they
|
|
29
|
+
* do not live inside the hot path.
|
|
17
30
|
*/
|
|
18
|
-
/** Wrap a stream to release pooled context on completion or cancellation. */
|
|
19
|
-
function wrapStreamWithRelease(source, ctx) {
|
|
20
|
-
let released = false;
|
|
21
|
-
const release = () => {
|
|
22
|
-
if (!released) {
|
|
23
|
-
released = true;
|
|
24
|
-
releaseContext(ctx);
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
const reader = source.getReader();
|
|
28
|
-
return new ReadableStream({
|
|
29
|
-
async pull(controller) {
|
|
30
|
-
try {
|
|
31
|
-
const { done, value } = await reader.read();
|
|
32
|
-
if (done) {
|
|
33
|
-
release();
|
|
34
|
-
controller.close();
|
|
35
|
-
} else controller.enqueue(value);
|
|
36
|
-
} catch (err) {
|
|
37
|
-
release();
|
|
38
|
-
controller.error(err);
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
cancel() {
|
|
42
|
-
release();
|
|
43
|
-
reader.cancel();
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
31
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
32
|
+
* Convert a pipeline output into an HTTP `Response`.
|
|
33
|
+
*
|
|
34
|
+
* We handle four shapes:
|
|
35
|
+
* - `Response` — user returned one directly; pass through.
|
|
36
|
+
* - `ReadableStream` — wrap in a binary response.
|
|
37
|
+
* - Async iterator — render as Server-Sent Events.
|
|
38
|
+
* - Plain value — encode as JSON (or msgpack when the client asked for it).
|
|
39
|
+
*
|
|
40
|
+
* The function is async so callers have a single `await` point and do not
|
|
41
|
+
* have to branch on sync-vs-async encoders underneath.
|
|
51
42
|
*/
|
|
52
|
-
function makeResponse(output, route, format
|
|
43
|
+
async function makeResponse(output, route, format) {
|
|
53
44
|
if (output instanceof Response) return output;
|
|
54
|
-
if (output instanceof ReadableStream) {
|
|
55
|
-
detachContext(ctx);
|
|
56
|
-
return new Response(wrapStreamWithRelease(output, ctx), { headers: { "content-type": "application/octet-stream" } });
|
|
57
|
-
}
|
|
45
|
+
if (output instanceof ReadableStream) return new Response(output, { headers: { "content-type": "application/octet-stream" } });
|
|
58
46
|
if (output && typeof output === "object" && Symbol.asyncIterator in output) {
|
|
59
|
-
detachContext(ctx);
|
|
60
47
|
const stream = iteratorToEventStream(output);
|
|
61
|
-
return new Response(
|
|
48
|
+
return new Response(stream, { headers: {
|
|
62
49
|
"content-type": "text/event-stream",
|
|
63
50
|
"cache-control": "no-cache"
|
|
64
51
|
} });
|
|
@@ -71,29 +58,44 @@ function makeResponse(output, route, format, ctx) {
|
|
|
71
58
|
} : { "content-type": "application/json" } });
|
|
72
59
|
}
|
|
73
60
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
61
|
+
* Wrap a `FetchHandler` with Scalar UI and/or analytics, if configured.
|
|
62
|
+
*
|
|
63
|
+
* Both wrappers have non-trivial imports (Scalar pulls in the API
|
|
64
|
+
* reference, analytics pulls in the dashboard). We defer those imports
|
|
65
|
+
* until the first request so that a handler you never hit does not pay
|
|
66
|
+
* the cost.
|
|
67
|
+
*
|
|
68
|
+
* If the lazy init fails (network blip, broken import) we fall back to
|
|
69
|
+
* the raw handler and log once — one failed init must not wedge every
|
|
70
|
+
* subsequent request.
|
|
77
71
|
*/
|
|
78
72
|
function wrapHandler(handler, router, options, prefix) {
|
|
79
73
|
if (!options?.scalar && !options?.analytics) return handler;
|
|
80
|
-
let wrapped;
|
|
74
|
+
let wrapped = handler;
|
|
75
|
+
let initDone = false;
|
|
81
76
|
let initPromise;
|
|
82
|
-
async
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
77
|
+
const init = async () => {
|
|
78
|
+
try {
|
|
79
|
+
let next = handler;
|
|
80
|
+
if (options.scalar) {
|
|
81
|
+
const { wrapWithScalar } = await import("../scalar.mjs");
|
|
82
|
+
const scalarOpts = typeof options.scalar === "object" ? options.scalar : {};
|
|
83
|
+
next = wrapWithScalar(next, router, scalarOpts, prefix, options.schemaRegistry);
|
|
84
|
+
}
|
|
85
|
+
if (options.analytics) {
|
|
86
|
+
const { wrapWithAnalytics } = await import("../plugins/analytics.mjs");
|
|
87
|
+
next = wrapWithAnalytics(next, router, options.analytics, options.schemaRegistry, options.hooks);
|
|
88
|
+
}
|
|
89
|
+
wrapped = next;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error("[silgi] Failed to initialise scalar/analytics wrapper:", err);
|
|
92
|
+
wrapped = handler;
|
|
93
|
+
} finally {
|
|
94
|
+
initDone = true;
|
|
92
95
|
}
|
|
93
|
-
|
|
94
|
-
}
|
|
96
|
+
};
|
|
95
97
|
return (request) => {
|
|
96
|
-
if (
|
|
98
|
+
if (initDone) return wrapped(request);
|
|
97
99
|
initPromise ??= init();
|
|
98
100
|
return initPromise.then(() => wrapped(request));
|
|
99
101
|
};
|
|
@@ -111,25 +113,39 @@ function createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge) {
|
|
|
111
113
|
status: 404,
|
|
112
114
|
message: "Procedure not found"
|
|
113
115
|
});
|
|
114
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Hook dispatch helpers.
|
|
118
|
+
*
|
|
119
|
+
* Hook errors never fail the request. But we do log them: silently
|
|
120
|
+
* swallowing a hook throw hides genuine user bugs (a typo'd field, a
|
|
121
|
+
* thrown assertion) and the dashboard / trace / logging pipeline just
|
|
122
|
+
* stops working with no visible signal.
|
|
123
|
+
*/
|
|
124
|
+
const reportHookError = (name, err) => {
|
|
125
|
+
console.error(`[silgi] hook "${name}" threw:`, err);
|
|
126
|
+
};
|
|
127
|
+
const callHook = (name, event) => {
|
|
115
128
|
if (!hooks) return;
|
|
116
129
|
try {
|
|
117
130
|
const result = hooks.callHook(name, event);
|
|
118
|
-
if (result instanceof Promise) result.catch(() =>
|
|
119
|
-
} catch {
|
|
120
|
-
|
|
121
|
-
|
|
131
|
+
if (result instanceof Promise) result.catch((err) => reportHookError(name, err));
|
|
132
|
+
} catch (err) {
|
|
133
|
+
reportHookError(name, err);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const awaitHook = async (name, event) => {
|
|
122
137
|
if (!hooks) return;
|
|
123
138
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
139
|
+
await hooks.callHook(name, event);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
reportHookError(name, err);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
128
144
|
return async function handleRequest(request) {
|
|
129
145
|
const url = request.url;
|
|
130
146
|
let fullPath = parseUrlPath(url);
|
|
131
147
|
if (prefix) {
|
|
132
|
-
if (!fullPath.startsWith(prefix)) return new Response(notFoundBody, {
|
|
148
|
+
if (fullPath !== prefix && !fullPath.startsWith(prefix + "/")) return new Response(notFoundBody, {
|
|
133
149
|
status: 404,
|
|
134
150
|
headers: jsonHeaders
|
|
135
151
|
});
|
|
@@ -158,17 +174,15 @@ function createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge) {
|
|
|
158
174
|
});
|
|
159
175
|
}
|
|
160
176
|
const format = detectResponseFormat(request);
|
|
161
|
-
|
|
177
|
+
const ctx = Object.create(null);
|
|
162
178
|
let rawInput;
|
|
163
179
|
try {
|
|
164
|
-
|
|
165
|
-
applyContext(ctx, baseCtxResult instanceof Promise ? await baseCtxResult : baseCtxResult);
|
|
180
|
+
applyContext(ctx, await contextFactory(request));
|
|
166
181
|
if (match.params) ctx.params = match.params;
|
|
167
|
-
|
|
182
|
+
await awaitHook("request:prepare", {
|
|
168
183
|
request,
|
|
169
184
|
ctx
|
|
170
185
|
});
|
|
171
|
-
if (prepareResult) await prepareResult;
|
|
172
186
|
if (!route.passthrough) rawInput = await parseInput(request, url, qMark);
|
|
173
187
|
if (match.params) rawInput = rawInput != null && typeof rawInput === "object" ? {
|
|
174
188
|
...match.params,
|
|
@@ -178,8 +192,7 @@ function createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge) {
|
|
|
178
192
|
path: pathname,
|
|
179
193
|
input: rawInput
|
|
180
194
|
});
|
|
181
|
-
const
|
|
182
|
-
const output = pipelineResult instanceof Promise ? await pipelineResult : pipelineResult;
|
|
195
|
+
const output = await (bridge ? bridge.run(ctx, () => route.handler(ctx, rawInput, request.signal)) : route.handler(ctx, rawInput, request.signal));
|
|
183
196
|
callHook("response", {
|
|
184
197
|
path: pathname,
|
|
185
198
|
output,
|
|
@@ -190,15 +203,13 @@ function createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge) {
|
|
|
190
203
|
ctx,
|
|
191
204
|
output
|
|
192
205
|
});
|
|
193
|
-
|
|
194
|
-
return response instanceof Promise ? await response : response;
|
|
206
|
+
return await makeResponse(output, route, format);
|
|
195
207
|
} catch (error) {
|
|
196
208
|
callHook("error", {
|
|
197
209
|
path: pathname,
|
|
198
210
|
error
|
|
199
211
|
});
|
|
200
|
-
|
|
201
|
-
return errorResponse instanceof Promise ? await errorResponse : errorResponse;
|
|
212
|
+
return await makeErrorResponse(error, format);
|
|
202
213
|
}
|
|
203
214
|
};
|
|
204
215
|
}
|
package/dist/core/input.mjs
CHANGED
|
@@ -1,49 +1,128 @@
|
|
|
1
1
|
import { SilgiError } from "./error.mjs";
|
|
2
2
|
//#region src/core/input.ts
|
|
3
3
|
/**
|
|
4
|
-
* Request input parsing
|
|
4
|
+
* Request input parsing
|
|
5
|
+
* -----------------------
|
|
6
|
+
*
|
|
7
|
+
* Pulls the RPC input payload out of an incoming `Request` and returns
|
|
8
|
+
* it in its decoded form. The shape of the input depends on how the
|
|
9
|
+
* client chose to send it:
|
|
10
|
+
*
|
|
11
|
+
* - `GET` / no-body — JSON-encoded, URL-escaped, on `?data=` query.
|
|
12
|
+
* - `content-type: application/msgpack` — binary, via the msgpack codec.
|
|
13
|
+
* - `content-type: application/x-devalue` — text, via devalue.
|
|
14
|
+
* - anything else (typically JSON) — JSON body.
|
|
15
|
+
*
|
|
16
|
+
* Empty bodies always resolve to `undefined` so that a procedure with
|
|
17
|
+
* no input, or one whose schema allows `undefined`, works without the
|
|
18
|
+
* client having to send a payload at all. Malformed non-empty bodies
|
|
19
|
+
* throw `BAD_REQUEST` with a message the client can show to a user.
|
|
5
20
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The msgpack and devalue codecs are pulled in on first use so that a
|
|
23
|
+
* handler that only ever sees JSON never pays the cost of loading
|
|
24
|
+
* them. `Promise`-cached at module scope: once the first request
|
|
25
|
+
* triggers the import, subsequent calls share the resolved module.
|
|
26
|
+
*
|
|
27
|
+
* Module-global is intentional and safe here — the cached value is a
|
|
28
|
+
* reference to an immutable ES module, not user data. Two silgi
|
|
29
|
+
* instances in the same process legitimately share it.
|
|
30
|
+
*/
|
|
31
|
+
let msgpackModule;
|
|
32
|
+
let devalueModule;
|
|
33
|
+
/** Max bytes permitted in the `?data=` query param. Shields against JSON-bomb payloads in a URL. */
|
|
10
34
|
const MAX_QUERY_DATA_LENGTH = 8192;
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const ct = request.headers.get("content-type");
|
|
28
|
-
if (ct) {
|
|
29
|
-
if (ct.includes("msgpack")) {
|
|
30
|
-
_msgpack ??= await import("../codec/msgpack.mjs");
|
|
31
|
-
const buf = new Uint8Array(await request.arrayBuffer());
|
|
32
|
-
return buf.length > 0 ? _msgpack.decode(buf) : void 0;
|
|
33
|
-
}
|
|
34
|
-
if (ct.includes("x-devalue")) {
|
|
35
|
-
_devalue ??= await import("../codec/devalue.mjs");
|
|
36
|
-
const text = await request.text();
|
|
37
|
-
return text ? _devalue.decode(text) : void 0;
|
|
35
|
+
/**
|
|
36
|
+
* Find the value of the `data=` query parameter by key, not by substring.
|
|
37
|
+
*
|
|
38
|
+
* A naive `searchStr.indexOf('data=')` matches `userdata=`, `mydata=`,
|
|
39
|
+
* or any other key that merely ends in `data`, and silently returns
|
|
40
|
+
* the wrong value. This scans for `data=` only at a parameter
|
|
41
|
+
* boundary — i.e. at the start of the search string, or right after
|
|
42
|
+
* an `&`.
|
|
43
|
+
*/
|
|
44
|
+
function findDataParam(searchStr) {
|
|
45
|
+
let i = 0;
|
|
46
|
+
while (i < searchStr.length) {
|
|
47
|
+
if (searchStr.startsWith("data=", i)) {
|
|
48
|
+
const valueStart = i + 5;
|
|
49
|
+
const valueEnd = searchStr.indexOf("&", valueStart);
|
|
50
|
+
return valueEnd === -1 ? searchStr.slice(valueStart) : searchStr.slice(valueStart, valueEnd);
|
|
38
51
|
}
|
|
52
|
+
const nextAmp = searchStr.indexOf("&", i);
|
|
53
|
+
if (nextAmp === -1) return null;
|
|
54
|
+
i = nextAmp + 1;
|
|
39
55
|
}
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Decode the `?data=` query param as JSON. Returns `undefined` when
|
|
60
|
+
* the query is missing or has no `data=` field. Throws `BAD_REQUEST`
|
|
61
|
+
* when the payload is oversized or unparsable.
|
|
62
|
+
*/
|
|
63
|
+
function decodeQueryInput(url, qMark) {
|
|
64
|
+
if (qMark === -1) return void 0;
|
|
65
|
+
const encoded = findDataParam(url.slice(qMark + 1));
|
|
66
|
+
if (encoded === null) return void 0;
|
|
67
|
+
if (encoded.length > MAX_QUERY_DATA_LENGTH) throw new SilgiError("BAD_REQUEST", { message: "Query data parameter too large" });
|
|
68
|
+
return JSON.parse(decodeURIComponent(encoded));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Decode a MessagePack-encoded request body. Empty bodies resolve to
|
|
72
|
+
* `undefined` (procedures with no input work without a payload).
|
|
73
|
+
*/
|
|
74
|
+
async function decodeMsgpackBody(request) {
|
|
75
|
+
msgpackModule ??= await import("../codec/msgpack.mjs");
|
|
76
|
+
const buf = new Uint8Array(await request.arrayBuffer());
|
|
77
|
+
return buf.length > 0 ? msgpackModule.decode(buf) : void 0;
|
|
78
|
+
}
|
|
79
|
+
/** Decode a devalue-encoded request body. Empty bodies resolve to `undefined`. */
|
|
80
|
+
async function decodeDevalueBody(request) {
|
|
81
|
+
devalueModule ??= await import("../codec/devalue.mjs");
|
|
82
|
+
const text = await request.text();
|
|
83
|
+
return text ? devalueModule.decode(text) : void 0;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Decode a JSON-encoded request body.
|
|
87
|
+
*
|
|
88
|
+
* Empty bodies resolve to `undefined` so the input schema sees the
|
|
89
|
+
* same value whether or not the client sent a body at all. Malformed
|
|
90
|
+
* non-empty bodies throw `BAD_REQUEST`.
|
|
91
|
+
*
|
|
92
|
+
* Why we `text()` first and then `JSON.parse` — instead of
|
|
93
|
+
* `request.json()`: Bun's `request.json()` is a fast path, but it
|
|
94
|
+
* throws a generic `SyntaxError` for **both** empty and malformed
|
|
95
|
+
* bodies, so we cannot tell them apart. Reading text first keeps the
|
|
96
|
+
* two cases distinct (and Bun's `text()` is also fast).
|
|
97
|
+
*/
|
|
98
|
+
async function decodeJsonBody(request) {
|
|
99
|
+
const text = await request.text();
|
|
100
|
+
if (!text) return void 0;
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(text);
|
|
42
103
|
} catch {
|
|
43
|
-
|
|
104
|
+
throw new SilgiError("BAD_REQUEST", { message: "Malformed JSON body" });
|
|
44
105
|
}
|
|
45
|
-
|
|
46
|
-
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Decode the input payload off a Fetch `Request`.
|
|
109
|
+
*
|
|
110
|
+
* @param request The incoming request.
|
|
111
|
+
* @param url The full request URL (reused by the caller — we avoid
|
|
112
|
+
* re-parsing it here).
|
|
113
|
+
* @param qMark Byte offset of the `?` in `url`, or `-1` when absent.
|
|
114
|
+
*
|
|
115
|
+
* @returns The decoded input value, or `undefined` when the request
|
|
116
|
+
* carries no payload.
|
|
117
|
+
*/
|
|
118
|
+
async function parseInput(request, url, qMark) {
|
|
119
|
+
if (request.method === "GET" || !request.body) return decodeQueryInput(url, qMark);
|
|
120
|
+
const contentType = request.headers.get("content-type");
|
|
121
|
+
if (contentType) {
|
|
122
|
+
if (contentType.includes("msgpack")) return decodeMsgpackBody(request);
|
|
123
|
+
if (contentType.includes("x-devalue")) return decodeDevalueBody(request);
|
|
124
|
+
}
|
|
125
|
+
return decodeJsonBody(request);
|
|
47
126
|
}
|
|
48
127
|
//#endregion
|
|
49
128
|
export { parseInput };
|
|
@@ -2,7 +2,9 @@ import { AnySchema } from "./schema.mjs";
|
|
|
2
2
|
|
|
3
3
|
//#region src/core/schema-converter.d.ts
|
|
4
4
|
/**
|
|
5
|
-
* JSON Schema subset used
|
|
5
|
+
* JSON Schema subset used across silgi's OpenAPI and analytics output.
|
|
6
|
+
* Intentionally broad (`[key: string]: unknown`) so library-specific
|
|
7
|
+
* fields (e.g. Zod's `x-native-type`) pass through untouched.
|
|
6
8
|
*
|
|
7
9
|
* @category Schema
|
|
8
10
|
*/
|
|
@@ -22,54 +24,68 @@ interface JSONSchema {
|
|
|
22
24
|
default?: unknown;
|
|
23
25
|
[key: string]: unknown;
|
|
24
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* JSON Schema dialect passed through to the schema library. Matches the
|
|
29
|
+
* `target` field of the Standard JSON Schema spec. Unknown strings are
|
|
30
|
+
* allowed so new dialects can be threaded through without a silgi
|
|
31
|
+
* release; libraries that do not recognise the value should throw and
|
|
32
|
+
* the conversion falls back to an empty schema.
|
|
33
|
+
*
|
|
34
|
+
* @category Schema
|
|
35
|
+
*/
|
|
36
|
+
type JSONSchemaTarget = 'draft-2020-12' | 'draft-07' | 'openapi-3.0' | (string & {});
|
|
25
37
|
/**
|
|
26
38
|
* Options passed to a converter's `toJsonSchema` method.
|
|
27
39
|
*
|
|
28
40
|
* @category Schema
|
|
29
41
|
*/
|
|
30
42
|
interface ConvertOptions {
|
|
43
|
+
/** `'input'` for pre-transform types, `'output'` for post-transform. */
|
|
31
44
|
strategy: 'input' | 'output';
|
|
45
|
+
/** JSON Schema dialect to target. Defaults to `'draft-2020-12'`. */
|
|
46
|
+
target?: JSONSchemaTarget;
|
|
47
|
+
/** Opaque options the converter may forward to its underlying library. */
|
|
48
|
+
libraryOptions?: Record<string, unknown>;
|
|
32
49
|
}
|
|
33
50
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
51
|
+
* Fallback converter for a schema library that has not adopted the
|
|
52
|
+
* Standard JSON Schema extension yet. Pass instances via
|
|
53
|
+
* `silgi({ schemaConverters: [...] })`.
|
|
36
54
|
*
|
|
37
55
|
* @remarks
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
56
|
+
* Libraries that *do* implement the extension (Zod v4.2+, ArkType
|
|
57
|
+
* v2.1.28+, Valibot v1.2+) are handled without a converter — silgi
|
|
58
|
+
* calls their native `~standard.jsonSchema` directly. Write a converter
|
|
59
|
+
* only when you need to support a vendor that has not yet adopted the
|
|
60
|
+
* spec.
|
|
41
61
|
*
|
|
42
62
|
* @example
|
|
43
|
-
*
|
|
44
|
-
* import type { SchemaConverter } from 'silgi'
|
|
63
|
+
* import type { SchemaConverter } from 'silgi'
|
|
45
64
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* ```
|
|
65
|
+
* const myConverter: SchemaConverter = {
|
|
66
|
+
* vendor: 'my-lib',
|
|
67
|
+
* toJsonSchema(schema, opts) {
|
|
68
|
+
* return { type: 'string' }
|
|
69
|
+
* },
|
|
70
|
+
* }
|
|
53
71
|
*
|
|
54
72
|
* @category Schema
|
|
55
73
|
*/
|
|
56
74
|
interface SchemaConverter {
|
|
57
|
-
/**
|
|
75
|
+
/** Matches the `~standard.vendor` reported by the schema library. */
|
|
58
76
|
vendor: string;
|
|
59
77
|
/**
|
|
60
|
-
* Convert a schema to a JSON Schema object.
|
|
61
|
-
*
|
|
62
|
-
* @param schema - The schema to convert.
|
|
63
|
-
* @param opts - Conversion options including `strategy` (`'input'` | `'output'`).
|
|
64
|
-
* @returns A JSON Schema object. Return `{}` for unsupported/unknown schemas.
|
|
78
|
+
* Convert a schema to a JSON Schema object. Return `{}` for schemas
|
|
79
|
+
* the converter does not understand.
|
|
65
80
|
*/
|
|
66
81
|
toJsonSchema(schema: AnySchema, opts: ConvertOptions): JSONSchema;
|
|
67
82
|
}
|
|
68
83
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
84
|
+
* Per-instance mapping of vendor string → fallback converter. Built by
|
|
85
|
+
* {@link createSchemaRegistry} and threaded through the handler pipeline
|
|
86
|
+
* to the scalar and analytics wrappers. Using `Map` gives O(1) lookup
|
|
87
|
+
* and keyed-by-vendor semantics that match the spec's own extension
|
|
88
|
+
* contract.
|
|
73
89
|
*
|
|
74
90
|
* @category Schema
|
|
75
91
|
*/
|
|
@@ -77,55 +93,44 @@ type SchemaRegistry = Map<string, SchemaConverter>;
|
|
|
77
93
|
/**
|
|
78
94
|
* Build a {@link SchemaRegistry} from an array of converters.
|
|
79
95
|
*
|
|
80
|
-
* @param converters - Array of {@link SchemaConverter} objects, each
|
|
81
|
-
* declaring their own `vendor`.
|
|
82
|
-
* @returns A `Map<string, SchemaConverter>` keyed by `converter.vendor`.
|
|
83
|
-
*
|
|
84
96
|
* @example
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* import { createSchemaRegistry } from 'silgi'
|
|
88
|
-
*
|
|
89
|
-
* const registry = createSchemaRegistry([zodConverter])
|
|
90
|
-
* ```
|
|
97
|
+
* import { zodConverter } from 'silgi/zod'
|
|
98
|
+
* const registry = createSchemaRegistry([zodConverter])
|
|
91
99
|
*
|
|
92
100
|
* @category Schema
|
|
93
101
|
*/
|
|
94
102
|
declare function createSchemaRegistry(converters?: SchemaConverter[]): SchemaRegistry;
|
|
95
103
|
/**
|
|
96
|
-
* Convert any Standard Schema to JSON Schema.
|
|
97
|
-
*
|
|
98
|
-
* @
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* @
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* {@link createSchemaRegistry}. When omitted the function still handles
|
|
114
|
-
* schemas that expose the native `jsonSchema.input()` fast path.
|
|
115
|
-
* @returns A JSON Schema object. Returns `{}` when conversion is not possible.
|
|
104
|
+
* Convert any Standard Schema to a JSON Schema object.
|
|
105
|
+
*
|
|
106
|
+
* @param schema The schema to convert.
|
|
107
|
+
* @param strategy `'input'` (default) for pre-transform types, `'output'`
|
|
108
|
+
* for post-transform. Matters for schemas that coerce
|
|
109
|
+
* (e.g. `z.coerce.number()` takes a string and yields a
|
|
110
|
+
* number — input and output schemas differ).
|
|
111
|
+
* @param registry Optional fallback registry built by
|
|
112
|
+
* {@link createSchemaRegistry}. Omit to rely solely on
|
|
113
|
+
* the native Standard JSON Schema extension.
|
|
114
|
+
* @param options Extra knobs: `target` dialect (default
|
|
115
|
+
* `'draft-2020-12'`), opaque `libraryOptions`
|
|
116
|
+
* forwarded to the underlying library.
|
|
117
|
+
*
|
|
118
|
+
* @returns A JSON Schema object. `{}` when the schema cannot be
|
|
119
|
+
* converted (silent fallback — analytics / OpenAPI output
|
|
120
|
+
* still renders, just without schema detail for that field).
|
|
116
121
|
*
|
|
117
122
|
* @example
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* import { createSchemaRegistry, schemaToJsonSchema } from 'silgi'
|
|
121
|
-
* import { z } from 'zod'
|
|
123
|
+
* import { zodConverter } from 'silgi/zod'
|
|
124
|
+
* import { createSchemaRegistry, schemaToJsonSchema } from 'silgi'
|
|
122
125
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* ```
|
|
126
|
+
* const registry = createSchemaRegistry([zodConverter])
|
|
127
|
+
* const json = schemaToJsonSchema(MySchema, 'input', registry)
|
|
126
128
|
*
|
|
127
129
|
* @category Schema
|
|
128
130
|
*/
|
|
129
|
-
declare function schemaToJsonSchema(schema: AnySchema, strategy?: 'input' | 'output', registry?: SchemaRegistry
|
|
131
|
+
declare function schemaToJsonSchema(schema: AnySchema, strategy?: 'input' | 'output', registry?: SchemaRegistry, options?: {
|
|
132
|
+
target?: JSONSchemaTarget;
|
|
133
|
+
libraryOptions?: Record<string, unknown>;
|
|
134
|
+
}): JSONSchema;
|
|
130
135
|
//#endregion
|
|
131
136
|
export { ConvertOptions, JSONSchema, SchemaConverter, SchemaRegistry, createSchemaRegistry, schemaToJsonSchema };
|