@zudojs/api 1.1.1 → 1.2.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/README.md +162 -6
- package/dist/api/bindings/cli/apiCli.argv.d.ts +36 -0
- package/dist/api/bindings/cli/apiCli.argv.js +78 -0
- package/dist/api/bindings/cli/apiCli.binding.d.ts +45 -0
- package/dist/api/bindings/cli/apiCli.binding.js +77 -0
- package/dist/api/bindings/cli/apiCli.exitCode.d.ts +31 -0
- package/dist/api/bindings/cli/apiCli.exitCode.js +43 -0
- package/dist/api/bindings/cli/apiCli.values.d.ts +24 -0
- package/dist/api/bindings/cli/apiCli.values.js +71 -0
- package/dist/api/bindings/cli/index.d.ts +12 -0
- package/dist/api/bindings/cli/index.js +9 -0
- package/dist/api/bindings/fetch/apiFetch.handler.d.ts +35 -0
- package/dist/api/bindings/fetch/apiFetch.handler.js +99 -0
- package/dist/api/bindings/fetch/apiFetch.input.d.ts +29 -0
- package/dist/api/bindings/fetch/apiFetch.input.js +74 -0
- package/dist/api/bindings/fetch/index.d.ts +11 -0
- package/dist/api/bindings/fetch/index.js +9 -0
- package/dist/api/bindings/index.d.ts +25 -0
- package/dist/api/bindings/index.js +18 -0
- package/dist/api/bindings/openapi/apiOpenAPI.descriptor.d.ts +37 -0
- package/dist/api/bindings/openapi/apiOpenAPI.descriptor.js +67 -0
- package/dist/api/bindings/openapi/apiOpenAPI.schemas.d.ts +32 -0
- package/dist/api/bindings/openapi/apiOpenAPI.schemas.js +62 -0
- package/dist/api/bindings/openapi/index.d.ts +9 -0
- package/dist/api/bindings/openapi/index.js +8 -0
- package/dist/api/bindings/queue/apiQueue.binding.d.ts +39 -0
- package/dist/api/bindings/queue/apiQueue.binding.js +65 -0
- package/dist/api/bindings/queue/index.d.ts +7 -0
- package/dist/api/bindings/queue/index.js +6 -0
- package/dist/api/bindings/route/apiRoute.resolver.d.ts +33 -0
- package/dist/api/bindings/route/apiRoute.resolver.js +109 -0
- package/dist/api/bindings/route/apiRoute.table.d.ts +53 -0
- package/dist/api/bindings/route/apiRoute.table.js +95 -0
- package/dist/api/bindings/route/apiRoute.type.d.ts +65 -0
- package/dist/api/bindings/route/apiRoute.type.js +8 -0
- package/dist/api/bindings/route/index.d.ts +11 -0
- package/dist/api/bindings/route/index.js +9 -0
- package/dist/api/bindings/rpc/apiRpc.binding.d.ts +46 -0
- package/dist/api/bindings/rpc/apiRpc.binding.js +63 -0
- package/dist/api/bindings/rpc/apiRpc.errors.d.ts +20 -0
- package/dist/api/bindings/rpc/apiRpc.errors.js +64 -0
- package/dist/api/bindings/rpc/index.d.ts +9 -0
- package/dist/api/bindings/rpc/index.js +8 -0
- package/dist/api/bindings/shared/apiBinding.helper.d.ts +44 -0
- package/dist/api/bindings/shared/apiBinding.helper.js +66 -0
- package/dist/api/bindings/shared/apiBinding.type.d.ts +37 -0
- package/dist/api/bindings/shared/apiBinding.type.js +2 -0
- package/dist/api/bindings/shared/apiWireResult.helper.d.ts +47 -0
- package/dist/api/bindings/shared/apiWireResult.helper.js +38 -0
- package/dist/api/bindings/shared/index.d.ts +11 -0
- package/dist/api/bindings/shared/index.js +8 -0
- package/dist/api/context/context.type.d.ts +4 -4
- package/dist/api/context/context.type.js +2 -2
- package/dist/api/context/contextKey.type.d.ts +8 -0
- package/dist/api/context/contextKey.type.js +2 -0
- package/dist/api/executor/executor.core.d.ts +20 -63
- package/dist/api/executor/executor.core.js +36 -193
- package/dist/api/executor/executor.deadline.d.ts +40 -0
- package/dist/api/executor/executor.deadline.js +93 -0
- package/dist/api/executor/executor.issues.d.ts +19 -0
- package/dist/api/executor/executor.issues.js +46 -0
- package/dist/api/executor/executor.normalize.d.ts +16 -0
- package/dist/api/executor/executor.normalize.js +45 -0
- package/dist/api/executor/executor.type.d.ts +33 -0
- package/dist/api/executor/executor.type.js +2 -0
- package/dist/api/executor/executor.validation.d.ts +31 -0
- package/dist/api/executor/executor.validation.js +58 -0
- package/dist/api/executor/index.d.ts +9 -2
- package/dist/api/executor/index.js +8 -1
- package/dist/api/handler/handler.type.d.ts +7 -4
- package/dist/api/interceptors/interceptor.type.d.ts +6 -5
- package/dist/api/operation/operation.type.d.ts +18 -1
- package/dist/api/operation/operation.type.js +5 -11
- package/dist/api/operation/operationSchema.type.d.ts +56 -0
- package/dist/api/operation/operationSchema.type.js +2 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +7 -2
- package/package.json +16 -6
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Serializer } from "@zudojs/serialization";
|
|
2
|
+
import type { APIBindingOptions, APIOperationSource } from "../shared/apiBinding.type.js";
|
|
3
|
+
/** Default limit on a request body read by the fetch binding (1 MiB). */
|
|
4
|
+
export declare const DEFAULT_API_MAX_BODY_BYTES: number;
|
|
5
|
+
/**
|
|
6
|
+
* Options for {@link createApiFetchHandler}. `state` receives the
|
|
7
|
+
* `Request` — derive the authenticated principal there.
|
|
8
|
+
*/
|
|
9
|
+
export interface APIFetchHandlerOptions extends APIBindingOptions<Request> {
|
|
10
|
+
/** Prefix for every route, e.g. `"/api"`. */
|
|
11
|
+
readonly basePath?: string;
|
|
12
|
+
/** Largest JSON request body accepted, in bytes. Defaults to 1 MiB. */
|
|
13
|
+
readonly maxBodyBytes?: number;
|
|
14
|
+
/** Response/request serializer. Defaults to `@zudojs/serialization` JSON. */
|
|
15
|
+
readonly serializer?: Serializer<unknown, string>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Exposes operations as a web-standard handler,
|
|
19
|
+
* `(request: Request) => Promise<Response>`, that any Fetch API server
|
|
20
|
+
* (`@zudojs/http`, Bun, Deno, edge runtimes, Node adapters) can mount.
|
|
21
|
+
*
|
|
22
|
+
* Routes come from each operation's `metadata.http` (default
|
|
23
|
+
* `POST /<name>`), input from the query string or JSON body plus path
|
|
24
|
+
* parameters, and every response body is an {@link APIWireResult}:
|
|
25
|
+
* `{ ok: true, data }` with 200, or `{ ok: false, error }` with the error's
|
|
26
|
+
* status (422 validation, 404 unknown route, 405 wrong method, 413/415 bad
|
|
27
|
+
* body, 500 internal). Internal messages and stack traces never reach the
|
|
28
|
+
* response. The call runs under `request.signal`, so a client that
|
|
29
|
+
* disconnects cancels it; the request id comes from `x-request-id` when
|
|
30
|
+
* safe and is echoed back in the same header.
|
|
31
|
+
*
|
|
32
|
+
* @throws {TypeError | RangeError} at creation for invalid or conflicting routes.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createApiFetchHandler(operations: APIOperationSource, options?: APIFetchHandlerOptions): (request: Request) => Promise<Response>;
|
|
35
|
+
//# sourceMappingURL=apiFetch.handler.d.ts.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createSerializer } from "@zudojs/serialization";
|
|
2
|
+
import { createOperationRunner } from "../shared/apiBinding.helper.js";
|
|
3
|
+
import { toApiWireResult } from "../shared/apiWireResult.helper.js";
|
|
4
|
+
import { compileRouteTable } from "../route/apiRoute.table.js";
|
|
5
|
+
import { APINotFoundError, createAPIError, ErrorCode } from "../../errors/index.js";
|
|
6
|
+
import { normalizeRequestId } from "../../context/context.type.js";
|
|
7
|
+
import { normalizeAPIError } from "../../executor/executor.core.js";
|
|
8
|
+
import { readFetchInput } from "./apiFetch.input.js";
|
|
9
|
+
/** Default limit on a request body read by the fetch binding (1 MiB). */
|
|
10
|
+
export const DEFAULT_API_MAX_BODY_BYTES = 1024 * 1024;
|
|
11
|
+
const MAX_REFLECTED_PATH = 256;
|
|
12
|
+
const FALLBACK_WRITER = createSerializer("json");
|
|
13
|
+
/**
|
|
14
|
+
* Exposes operations as a web-standard handler,
|
|
15
|
+
* `(request: Request) => Promise<Response>`, that any Fetch API server
|
|
16
|
+
* (`@zudojs/http`, Bun, Deno, edge runtimes, Node adapters) can mount.
|
|
17
|
+
*
|
|
18
|
+
* Routes come from each operation's `metadata.http` (default
|
|
19
|
+
* `POST /<name>`), input from the query string or JSON body plus path
|
|
20
|
+
* parameters, and every response body is an {@link APIWireResult}:
|
|
21
|
+
* `{ ok: true, data }` with 200, or `{ ok: false, error }` with the error's
|
|
22
|
+
* status (422 validation, 404 unknown route, 405 wrong method, 413/415 bad
|
|
23
|
+
* body, 500 internal). Internal messages and stack traces never reach the
|
|
24
|
+
* response. The call runs under `request.signal`, so a client that
|
|
25
|
+
* disconnects cancels it; the request id comes from `x-request-id` when
|
|
26
|
+
* safe and is echoed back in the same header.
|
|
27
|
+
*
|
|
28
|
+
* @throws {TypeError | RangeError} at creation for invalid or conflicting routes.
|
|
29
|
+
*/
|
|
30
|
+
export function createApiFetchHandler(operations, options = {}) {
|
|
31
|
+
const table = compileRouteTable(operations, options.basePath);
|
|
32
|
+
const run = createOperationRunner(options);
|
|
33
|
+
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_API_MAX_BODY_BYTES;
|
|
34
|
+
const reader = options.serializer ?? createSerializer("json", { maxSize: maxBodyBytes, maxDepth: 64 });
|
|
35
|
+
const writer = options.serializer ?? createSerializer("json");
|
|
36
|
+
const respond = (body, code, requestId, extra) => {
|
|
37
|
+
let text;
|
|
38
|
+
let status = code;
|
|
39
|
+
try {
|
|
40
|
+
text = writer.serialize(body);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const failure = normalizeAPIError(error, "response serialization");
|
|
44
|
+
options.onInternalError?.(failure, requestId);
|
|
45
|
+
text = FALLBACK_WRITER.serialize(toApiWireResult({ ok: false, error: failure }, requestId));
|
|
46
|
+
status = 500;
|
|
47
|
+
}
|
|
48
|
+
return new Response(text, {
|
|
49
|
+
status,
|
|
50
|
+
headers: {
|
|
51
|
+
"content-type": "application/json; charset=utf-8",
|
|
52
|
+
"cache-control": "no-store",
|
|
53
|
+
"x-content-type-options": "nosniff",
|
|
54
|
+
"x-request-id": requestId,
|
|
55
|
+
...extra,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const refuse = (error, requestId, extra) => {
|
|
60
|
+
const body = toApiWireResult({ ok: false, error }, requestId);
|
|
61
|
+
return respond(body, body.ok ? 200 : body.error.statusCode, requestId, extra);
|
|
62
|
+
};
|
|
63
|
+
return async (request) => {
|
|
64
|
+
const requestId = normalizeRequestId(request.headers.get("x-request-id") ?? undefined);
|
|
65
|
+
const url = new URL(request.url);
|
|
66
|
+
const match = table.match(request.method, url.pathname);
|
|
67
|
+
if (match.kind === "not-found") {
|
|
68
|
+
const path = url.pathname.slice(0, MAX_REFLECTED_PATH);
|
|
69
|
+
return refuse(new APINotFoundError(path, request.method.slice(0, 16)), requestId);
|
|
70
|
+
}
|
|
71
|
+
if (match.kind === "bad-path") {
|
|
72
|
+
return refuse(clientError(400, "Request path is not correctly encoded."), requestId);
|
|
73
|
+
}
|
|
74
|
+
if (match.kind === "method-not-allowed") {
|
|
75
|
+
const allow = match.allow.join(", ");
|
|
76
|
+
return refuse(clientError(405, `Method not allowed; use ${allow}.`), requestId, { allow });
|
|
77
|
+
}
|
|
78
|
+
const { route, operation } = match.entry;
|
|
79
|
+
const read = await readFetchInput(request, url, route, match.params, reader, maxBodyBytes);
|
|
80
|
+
if (!read.ok) {
|
|
81
|
+
return refuse(read.error, requestId);
|
|
82
|
+
}
|
|
83
|
+
const outcome = await run({
|
|
84
|
+
operation,
|
|
85
|
+
input: read.input,
|
|
86
|
+
source: request,
|
|
87
|
+
transport: "http",
|
|
88
|
+
requestId,
|
|
89
|
+
correlationId: request.headers.get("x-correlation-id") ?? undefined,
|
|
90
|
+
signal: request.signal,
|
|
91
|
+
});
|
|
92
|
+
const body = toApiWireResult(outcome.result, outcome.requestId);
|
|
93
|
+
return respond(body, body.ok ? route.successStatus : body.error.statusCode, outcome.requestId);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function clientError(statusCode, message) {
|
|
97
|
+
return createAPIError(message, { statusCode, code: ErrorCode.API_ERROR, expose: true });
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=apiFetch.handler.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Serializer } from "@zudojs/serialization";
|
|
2
|
+
import type { APIOperationRoute } from "../route/apiRoute.type.js";
|
|
3
|
+
import type { APIError } from "../../errors/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Operation input read from a request, or the client error explaining why
|
|
6
|
+
* it could not be read.
|
|
7
|
+
*/
|
|
8
|
+
export type APIFetchInput = {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
readonly input: unknown;
|
|
11
|
+
} | {
|
|
12
|
+
readonly ok: false;
|
|
13
|
+
readonly error: APIError;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Reads operation input from a Fetch API request.
|
|
17
|
+
*
|
|
18
|
+
* `GET` / `DELETE` routes read the query string: each key becomes a
|
|
19
|
+
* string, a repeated key an array of strings (schemas coerce as needed).
|
|
20
|
+
* Body routes read a JSON body, bounded by `maxBodyBytes`; an empty body
|
|
21
|
+
* is `undefined`, and any declared content type other than JSON is
|
|
22
|
+
* refused (415) even when the body is empty. Path parameters are merged
|
|
23
|
+
* over the result and win, so the URL — not the body — names the
|
|
24
|
+
* resource. Keys that could pollute a prototype (`__proto__`,
|
|
25
|
+
* `constructor`, `prototype`) are refused at any depth, in the query and
|
|
26
|
+
* the body alike.
|
|
27
|
+
*/
|
|
28
|
+
export declare function readFetchInput(request: Request, url: URL, route: APIOperationRoute, params: Readonly<Record<string, string>>, serializer: Serializer<unknown, string>, maxBodyBytes: number): Promise<APIFetchInput>;
|
|
29
|
+
//# sourceMappingURL=apiFetch.input.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readBoundedBody } from "@zudojs/rpc";
|
|
2
|
+
import { findUnsafeKey } from "@zudojs/security";
|
|
3
|
+
import { isPlainObject } from "@zudojs/types";
|
|
4
|
+
import { createAPIError, ErrorCode } from "../../errors/index.js";
|
|
5
|
+
const JSON_TYPE = /^application\/(?:[\w.+-]+\+)?json(?:\s*;|$)/i;
|
|
6
|
+
const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
7
|
+
/**
|
|
8
|
+
* Reads operation input from a Fetch API request.
|
|
9
|
+
*
|
|
10
|
+
* `GET` / `DELETE` routes read the query string: each key becomes a
|
|
11
|
+
* string, a repeated key an array of strings (schemas coerce as needed).
|
|
12
|
+
* Body routes read a JSON body, bounded by `maxBodyBytes`; an empty body
|
|
13
|
+
* is `undefined`, and any declared content type other than JSON is
|
|
14
|
+
* refused (415) even when the body is empty. Path parameters are merged
|
|
15
|
+
* over the result and win, so the URL — not the body — names the
|
|
16
|
+
* resource. Keys that could pollute a prototype (`__proto__`,
|
|
17
|
+
* `constructor`, `prototype`) are refused at any depth, in the query and
|
|
18
|
+
* the body alike.
|
|
19
|
+
*/
|
|
20
|
+
export async function readFetchInput(request, url, route, params, serializer, maxBodyBytes) {
|
|
21
|
+
const hasParams = route.pathParams.length > 0;
|
|
22
|
+
if (route.inputSource === "query") {
|
|
23
|
+
const query = {};
|
|
24
|
+
for (const key of new Set(url.searchParams.keys())) {
|
|
25
|
+
if (UNSAFE_KEYS.has(key)) {
|
|
26
|
+
return fail(400, ErrorCode.API_VALIDATION, `Query parameter "${key}" is not allowed.`);
|
|
27
|
+
}
|
|
28
|
+
const values = url.searchParams.getAll(key);
|
|
29
|
+
query[key] = values.length === 1 ? values[0] : values;
|
|
30
|
+
}
|
|
31
|
+
return { ok: true, input: { ...query, ...params } };
|
|
32
|
+
}
|
|
33
|
+
const body = await readBoundedBody(request, maxBodyBytes);
|
|
34
|
+
if (!body.ok) {
|
|
35
|
+
return body.reason === "too-large"
|
|
36
|
+
? fail(413, ErrorCode.PAYLOAD_TOO_LARGE, `Request body exceeds ${maxBodyBytes} bytes.`)
|
|
37
|
+
: fail(400, ErrorCode.HTTP_BODY, "Request body could not be read.");
|
|
38
|
+
}
|
|
39
|
+
// A declared non-JSON type is refused even with an empty body: an HTML
|
|
40
|
+
// form can post `text/plain`, `multipart/form-data` or urlencoded bodies
|
|
41
|
+
// cross-site without a CORS preflight, which would otherwise run an
|
|
42
|
+
// input-less operation on the victim's cookies.
|
|
43
|
+
const contentType = request.headers.get("content-type");
|
|
44
|
+
if (contentType !== null && !JSON_TYPE.test(contentType)) {
|
|
45
|
+
return fail(415, ErrorCode.HTTP_UNSUPPORTED_BODY_TYPE, "Request body must be application/json.");
|
|
46
|
+
}
|
|
47
|
+
let value;
|
|
48
|
+
if (body.text.trim().length > 0) {
|
|
49
|
+
if (contentType === null) {
|
|
50
|
+
return fail(415, ErrorCode.HTTP_UNSUPPORTED_BODY_TYPE, "Request body must be application/json.");
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
value = serializer.deserialize(body.text);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return fail(400, ErrorCode.HTTP_BODY_PARSE, "Request body is not valid JSON.");
|
|
57
|
+
}
|
|
58
|
+
const unsafe = findUnsafeKey(value);
|
|
59
|
+
if (unsafe !== undefined) {
|
|
60
|
+
return fail(400, ErrorCode.API_VALIDATION, `Request body key "${unsafe}" is not allowed.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!hasParams) {
|
|
64
|
+
return { ok: true, input: value };
|
|
65
|
+
}
|
|
66
|
+
if (value !== undefined && !isPlainObject(value)) {
|
|
67
|
+
return fail(400, ErrorCode.API_VALIDATION, "Request body must be a JSON object.");
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, input: { ...(value ?? {}), ...params } };
|
|
70
|
+
}
|
|
71
|
+
function fail(statusCode, code, message) {
|
|
72
|
+
return { ok: false, error: createAPIError(message, { statusCode, code, expose: true }) };
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=apiFetch.input.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web-standard HTTP binding: exposes operations as a
|
|
3
|
+
* `(request: Request) => Promise<Response>` handler that any Fetch API
|
|
4
|
+
* server can mount, with routes taken from each operation's
|
|
5
|
+
* `metadata.http`.
|
|
6
|
+
*/
|
|
7
|
+
export type { APIFetchHandlerOptions } from "./apiFetch.handler.js";
|
|
8
|
+
export { createApiFetchHandler, DEFAULT_API_MAX_BODY_BYTES } from "./apiFetch.handler.js";
|
|
9
|
+
export type { APIFetchInput } from "./apiFetch.input.js";
|
|
10
|
+
export { readFetchInput } from "./apiFetch.input.js";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web-standard HTTP binding: exposes operations as a
|
|
3
|
+
* `(request: Request) => Promise<Response>` handler that any Fetch API
|
|
4
|
+
* server can mount, with routes taken from each operation's
|
|
5
|
+
* `metadata.http`.
|
|
6
|
+
*/
|
|
7
|
+
export { createApiFetchHandler, DEFAULT_API_MAX_BODY_BYTES } from "./apiFetch.handler.js";
|
|
8
|
+
export { readFetchInput } from "./apiFetch.input.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bindings that expose one set of operations over many transports — HTTP
|
|
3
|
+
* (a web-standard fetch handler), RPC, queues and the command line, plus
|
|
4
|
+
* OpenAPI route descriptors — with
|
|
5
|
+
* the same executor, interceptors, validation and client-safe error shape
|
|
6
|
+
* on every one.
|
|
7
|
+
*
|
|
8
|
+
* This barrel is the package's public binding surface; runner and
|
|
9
|
+
* route-matching internals stay in the subfolder barrels.
|
|
10
|
+
*/
|
|
11
|
+
export type { APIBindingOptions, APIOperationSource, APIWireError, APIWireResult, } from "./shared/index.js";
|
|
12
|
+
export { API_INTERNAL_ERROR_MESSAGE, toApiWireError, toApiWireResult, } from "./shared/index.js";
|
|
13
|
+
export type { APIHttpMethod, APIRouteInputSource, APIOperationHttpOptions, APIOperationRoute, DescribeApiRoutesOptions, } from "./route/index.js";
|
|
14
|
+
export { describeApiRoutes, resolveApiRoute } from "./route/index.js";
|
|
15
|
+
export type { APIFetchHandlerOptions } from "./fetch/index.js";
|
|
16
|
+
export { createApiFetchHandler, DEFAULT_API_MAX_BODY_BYTES } from "./fetch/index.js";
|
|
17
|
+
export type { APIRpcBindingOptions, APIRpcProcedureTarget } from "./rpc/index.js";
|
|
18
|
+
export { API_RPC_TIMEOUT_MARGIN_MS, apiErrorToRPCError, createApiRpcProcedure, registerApiRpcProcedures, } from "./rpc/index.js";
|
|
19
|
+
export type { APIQueueBindingOptions, APIQueueTarget } from "./queue/index.js";
|
|
20
|
+
export { bindApiQueue, createApiQueueProcessor } from "./queue/index.js";
|
|
21
|
+
export type { APICliInvocation, APICliIO, APICliOptions, APICliParseResult, APICliExitCodeValue, } from "./cli/index.js";
|
|
22
|
+
export { APICliExitCode, parseApiCliArgs, runApiCli } from "./cli/index.js";
|
|
23
|
+
export type { ToOpenAPIRouteDescriptorsOptions } from "./openapi/index.js";
|
|
24
|
+
export { apiSuccessBodySchema, apiWireErrorBodySchema, toOpenAPIRouteDescriptor, toOpenAPIRouteDescriptors, } from "./openapi/index.js";
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bindings that expose one set of operations over many transports — HTTP
|
|
3
|
+
* (a web-standard fetch handler), RPC, queues and the command line, plus
|
|
4
|
+
* OpenAPI route descriptors — with
|
|
5
|
+
* the same executor, interceptors, validation and client-safe error shape
|
|
6
|
+
* on every one.
|
|
7
|
+
*
|
|
8
|
+
* This barrel is the package's public binding surface; runner and
|
|
9
|
+
* route-matching internals stay in the subfolder barrels.
|
|
10
|
+
*/
|
|
11
|
+
export { API_INTERNAL_ERROR_MESSAGE, toApiWireError, toApiWireResult, } from "./shared/index.js";
|
|
12
|
+
export { describeApiRoutes, resolveApiRoute } from "./route/index.js";
|
|
13
|
+
export { createApiFetchHandler, DEFAULT_API_MAX_BODY_BYTES } from "./fetch/index.js";
|
|
14
|
+
export { API_RPC_TIMEOUT_MARGIN_MS, apiErrorToRPCError, createApiRpcProcedure, registerApiRpcProcedures, } from "./rpc/index.js";
|
|
15
|
+
export { bindApiQueue, createApiQueueProcessor } from "./queue/index.js";
|
|
16
|
+
export { APICliExitCode, parseApiCliArgs, runApiCli } from "./cli/index.js";
|
|
17
|
+
export { apiSuccessBodySchema, apiWireErrorBodySchema, toOpenAPIRouteDescriptor, toOpenAPIRouteDescriptors, } from "./openapi/index.js";
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { OpenAPIRouteDescriptor } from "@zudojs/openapi";
|
|
2
|
+
import type { APIOperationSource } from "../shared/apiBinding.type.js";
|
|
3
|
+
import type { APIOperationRoute } from "../route/apiRoute.type.js";
|
|
4
|
+
import type { DescribeApiRoutesOptions } from "../route/apiRoute.table.js";
|
|
5
|
+
/**
|
|
6
|
+
* Options for {@link toOpenAPIRouteDescriptors}.
|
|
7
|
+
*/
|
|
8
|
+
export type ToOpenAPIRouteDescriptorsOptions = DescribeApiRoutesOptions;
|
|
9
|
+
/**
|
|
10
|
+
* Converts an operation set into `@zudojs/openapi` route descriptors, so
|
|
11
|
+
* the operations served by `createApiFetchHandler` can be documented in
|
|
12
|
+
* one call:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* const document = createOpenAPIDocumentFromRoutes(
|
|
16
|
+
* toOpenAPIRouteDescriptors(registry, { basePath: "/api" }),
|
|
17
|
+
* { info: { title: "Users", version: "1.0.0" } },
|
|
18
|
+
* );
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Each descriptor carries the route's method, path, `operationId`,
|
|
22
|
+
* description, tags and deprecation. The input schema becomes `query`
|
|
23
|
+
* (`GET` / `DELETE`) or `body`, with path-bound fields moved to `params`.
|
|
24
|
+
* `responses` documents 200 with the `{ ok: true, data }` envelope around
|
|
25
|
+
* the output schema, and 422, 404, 409, 500 and 504 with the
|
|
26
|
+
* `{ ok: false, error }` body. Only `@zudojs/schema` schemas can be
|
|
27
|
+
* converted; input or output declared with another library is left
|
|
28
|
+
* undocumented (`data` is then `unknown`).
|
|
29
|
+
*
|
|
30
|
+
* @throws {TypeError | RangeError} as `describeApiRoutes`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function toOpenAPIRouteDescriptors(operations: APIOperationSource, options?: ToOpenAPIRouteDescriptorsOptions): readonly OpenAPIRouteDescriptor[];
|
|
33
|
+
/**
|
|
34
|
+
* Converts one {@link APIOperationRoute} into an OpenAPI route descriptor.
|
|
35
|
+
*/
|
|
36
|
+
export declare function toOpenAPIRouteDescriptor(route: APIOperationRoute): OpenAPIRouteDescriptor;
|
|
37
|
+
//# sourceMappingURL=apiOpenAPI.descriptor.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describeApiRoutes } from "../route/apiRoute.table.js";
|
|
2
|
+
import { apiSuccessBodySchema, apiWireErrorBodySchema, splitInputSchema, } from "./apiOpenAPI.schemas.js";
|
|
3
|
+
/**
|
|
4
|
+
* Error responses every operation can produce over the fetch binding,
|
|
5
|
+
* each documented with the `{ ok: false, error }` body.
|
|
6
|
+
*/
|
|
7
|
+
const ERROR_RESPONSES = [
|
|
8
|
+
["422", "Input failed validation."],
|
|
9
|
+
["404", "No operation is bound to this method and path."],
|
|
10
|
+
["409", "The operation reported a conflict."],
|
|
11
|
+
["500", "Internal error. The message is generic; detail is logged server-side."],
|
|
12
|
+
["504", "The operation timed out."],
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Converts an operation set into `@zudojs/openapi` route descriptors, so
|
|
16
|
+
* the operations served by `createApiFetchHandler` can be documented in
|
|
17
|
+
* one call:
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* const document = createOpenAPIDocumentFromRoutes(
|
|
21
|
+
* toOpenAPIRouteDescriptors(registry, { basePath: "/api" }),
|
|
22
|
+
* { info: { title: "Users", version: "1.0.0" } },
|
|
23
|
+
* );
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Each descriptor carries the route's method, path, `operationId`,
|
|
27
|
+
* description, tags and deprecation. The input schema becomes `query`
|
|
28
|
+
* (`GET` / `DELETE`) or `body`, with path-bound fields moved to `params`.
|
|
29
|
+
* `responses` documents 200 with the `{ ok: true, data }` envelope around
|
|
30
|
+
* the output schema, and 422, 404, 409, 500 and 504 with the
|
|
31
|
+
* `{ ok: false, error }` body. Only `@zudojs/schema` schemas can be
|
|
32
|
+
* converted; input or output declared with another library is left
|
|
33
|
+
* undocumented (`data` is then `unknown`).
|
|
34
|
+
*
|
|
35
|
+
* @throws {TypeError | RangeError} as `describeApiRoutes`.
|
|
36
|
+
*/
|
|
37
|
+
export function toOpenAPIRouteDescriptors(operations, options = {}) {
|
|
38
|
+
return Object.freeze(describeApiRoutes(operations, options).map(toOpenAPIRouteDescriptor));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Converts one {@link APIOperationRoute} into an OpenAPI route descriptor.
|
|
42
|
+
*/
|
|
43
|
+
export function toOpenAPIRouteDescriptor(route) {
|
|
44
|
+
const { params, rest } = splitInputSchema(route.input, route.pathParams);
|
|
45
|
+
const responses = {
|
|
46
|
+
"200": { schema: apiSuccessBodySchema(route.output), description: "Success." },
|
|
47
|
+
};
|
|
48
|
+
for (const [status, description] of ERROR_RESPONSES) {
|
|
49
|
+
responses[status] = { schema: apiWireErrorBodySchema, description };
|
|
50
|
+
}
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
method: route.method,
|
|
53
|
+
path: route.path,
|
|
54
|
+
operationId: route.operationId,
|
|
55
|
+
...(route.description !== undefined ? { description: route.description } : {}),
|
|
56
|
+
...(route.tags !== undefined ? { tags: route.tags } : {}),
|
|
57
|
+
...(route.deprecated !== undefined ? { deprecated: route.deprecated } : {}),
|
|
58
|
+
...(params !== undefined ? { params: params } : {}),
|
|
59
|
+
...(rest !== undefined
|
|
60
|
+
? route.inputSource === "query"
|
|
61
|
+
? { query: rest }
|
|
62
|
+
: { body: rest }
|
|
63
|
+
: {}),
|
|
64
|
+
responses: Object.freeze(responses),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=apiOpenAPI.descriptor.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `{ ok: false, error }` body every binding sends on failure, as a
|
|
3
|
+
* `@zudojs/schema` schema for OpenAPI documents.
|
|
4
|
+
*/
|
|
5
|
+
export declare const apiWireErrorBodySchema: import("@zudojs/schema").ObjectSchema<{
|
|
6
|
+
ok: false;
|
|
7
|
+
error: {
|
|
8
|
+
code: string;
|
|
9
|
+
message: string;
|
|
10
|
+
statusCode: number;
|
|
11
|
+
requestId: string;
|
|
12
|
+
issues?: string[] | undefined;
|
|
13
|
+
};
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Builds the `{ ok: true, data }` success envelope around an operation's
|
|
17
|
+
* output schema. An output that OpenAPI generation cannot convert (a
|
|
18
|
+
* Standard Schema from another library), or no output at all, is
|
|
19
|
+
* documented as `data: unknown`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function apiSuccessBodySchema(output: unknown): unknown;
|
|
22
|
+
/**
|
|
23
|
+
* Splits an input schema into the path-parameter part and the rest, so
|
|
24
|
+
* a field bound from the path is not also documented as a query parameter
|
|
25
|
+
* or body property. Returns `undefined` parts for input OpenAPI cannot
|
|
26
|
+
* convert.
|
|
27
|
+
*/
|
|
28
|
+
export declare function splitInputSchema(input: unknown, pathParams: readonly string[]): {
|
|
29
|
+
readonly params?: unknown;
|
|
30
|
+
readonly rest?: unknown;
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=apiOpenAPI.schemas.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isSchemaDefinition } from "@zudojs/openapi";
|
|
2
|
+
import { schema } from "@zudojs/schema";
|
|
3
|
+
/**
|
|
4
|
+
* The `{ ok: false, error }` body every binding sends on failure, as a
|
|
5
|
+
* `@zudojs/schema` schema for OpenAPI documents.
|
|
6
|
+
*/
|
|
7
|
+
export const apiWireErrorBodySchema = schema.object({
|
|
8
|
+
ok: schema.literal(false),
|
|
9
|
+
error: schema.object({
|
|
10
|
+
code: schema.string(),
|
|
11
|
+
message: schema.string(),
|
|
12
|
+
statusCode: schema.number(),
|
|
13
|
+
requestId: schema.string(),
|
|
14
|
+
issues: schema.optional(schema.array(schema.string())),
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Builds the `{ ok: true, data }` success envelope around an operation's
|
|
19
|
+
* output schema. An output that OpenAPI generation cannot convert (a
|
|
20
|
+
* Standard Schema from another library), or no output at all, is
|
|
21
|
+
* documented as `data: unknown`.
|
|
22
|
+
*/
|
|
23
|
+
export function apiSuccessBodySchema(output) {
|
|
24
|
+
const data = isSchemaDefinition(output) ? output : schema.unknown();
|
|
25
|
+
return schema.object({
|
|
26
|
+
ok: schema.literal(true),
|
|
27
|
+
data: data,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function isSplittable(value) {
|
|
31
|
+
const candidate = value;
|
|
32
|
+
return (isSchemaDefinition(value) &&
|
|
33
|
+
candidate?._type === "object" &&
|
|
34
|
+
typeof candidate.shape === "object" &&
|
|
35
|
+
typeof candidate.pick === "function" &&
|
|
36
|
+
typeof candidate.omit === "function");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Splits an input schema into the path-parameter part and the rest, so
|
|
40
|
+
* a field bound from the path is not also documented as a query parameter
|
|
41
|
+
* or body property. Returns `undefined` parts for input OpenAPI cannot
|
|
42
|
+
* convert.
|
|
43
|
+
*/
|
|
44
|
+
export function splitInputSchema(input, pathParams) {
|
|
45
|
+
if (!isSchemaDefinition(input)) {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
if (!isSplittable(input)) {
|
|
49
|
+
return { rest: input };
|
|
50
|
+
}
|
|
51
|
+
const bound = pathParams.filter((name) => Object.hasOwn(input.shape, name));
|
|
52
|
+
if (bound.length === 0) {
|
|
53
|
+
return { rest: input };
|
|
54
|
+
}
|
|
55
|
+
const rest = input.omit(bound);
|
|
56
|
+
const restEmpty = Object.keys(rest.shape ?? {}).length === 0;
|
|
57
|
+
return {
|
|
58
|
+
params: input.pick(bound),
|
|
59
|
+
...(restEmpty ? {} : { rest }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=apiOpenAPI.schemas.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI bridge: converts an operation set into `@zudojs/openapi` route
|
|
3
|
+
* descriptors, so `createOpenAPIDocumentFromRoutes` documents exactly
|
|
4
|
+
* what `createApiFetchHandler` serves, envelopes included.
|
|
5
|
+
*/
|
|
6
|
+
export type { ToOpenAPIRouteDescriptorsOptions } from "./apiOpenAPI.descriptor.js";
|
|
7
|
+
export { toOpenAPIRouteDescriptor, toOpenAPIRouteDescriptors, } from "./apiOpenAPI.descriptor.js";
|
|
8
|
+
export { apiSuccessBodySchema, apiWireErrorBodySchema, splitInputSchema, } from "./apiOpenAPI.schemas.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI bridge: converts an operation set into `@zudojs/openapi` route
|
|
3
|
+
* descriptors, so `createOpenAPIDocumentFromRoutes` documents exactly
|
|
4
|
+
* what `createApiFetchHandler` serves, envelopes included.
|
|
5
|
+
*/
|
|
6
|
+
export { toOpenAPIRouteDescriptor, toOpenAPIRouteDescriptors, } from "./apiOpenAPI.descriptor.js";
|
|
7
|
+
export { apiSuccessBodySchema, apiWireErrorBodySchema, splitInputSchema, } from "./apiOpenAPI.schemas.js";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Job, Processor } from "@zudojs/queue";
|
|
2
|
+
import type { AnyAPIOperation } from "../../operation/operation.type.js";
|
|
3
|
+
import type { APIBindingOptions, APIOperationSource } from "../shared/apiBinding.type.js";
|
|
4
|
+
/**
|
|
5
|
+
* Options for the queue binding. `state` receives the `Job` being run.
|
|
6
|
+
*/
|
|
7
|
+
export type APIQueueBindingOptions = APIBindingOptions<Job<unknown>>;
|
|
8
|
+
/**
|
|
9
|
+
* Anything a processor can be registered on — a `Queue` from
|
|
10
|
+
* `@zudojs/queue`, such as `createInMemoryQueue(...)`.
|
|
11
|
+
*/
|
|
12
|
+
export interface APIQueueTarget<TData> {
|
|
13
|
+
process(name: string, processor: Processor<TData>): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Wraps one operation as a queue processor.
|
|
17
|
+
*
|
|
18
|
+
* `job.data` is the operation input. The operation runs through the
|
|
19
|
+
* executor — validation, interceptors, timeout — under the job's abort
|
|
20
|
+
* signal, with the job id as request id and `job.metadata.correlationId`
|
|
21
|
+
* as correlation id. Success completes the job with the operation's
|
|
22
|
+
* output as its result. A failure throws an `APIError` carrying the same
|
|
23
|
+
* client-safe message and code the other bindings expose (the original
|
|
24
|
+
* is its `cause`), so the queue retries and dead-letters it as usual and
|
|
25
|
+
* `job.error` never holds internal detail; the original error also goes
|
|
26
|
+
* to `onInternalError`.
|
|
27
|
+
*
|
|
28
|
+
* Every failure is retried up to the job's `attempts`, validation
|
|
29
|
+
* failures included — enqueue with `attempts: 1` when input is not
|
|
30
|
+
* already known to be valid.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createApiQueueProcessor<TData = unknown>(operation: AnyAPIOperation, options?: APIQueueBindingOptions): Processor<TData>;
|
|
33
|
+
/**
|
|
34
|
+
* Registers a processor for every operation, under the operation's name
|
|
35
|
+
* as job name, and returns the job names. Enqueue work with
|
|
36
|
+
* `queue.add(operation.name, input)`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function bindApiQueue<TData = unknown>(queue: APIQueueTarget<TData>, operations: APIOperationSource, options?: APIQueueBindingOptions): readonly string[];
|
|
39
|
+
//# sourceMappingURL=apiQueue.binding.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createJobResult } from "@zudojs/queue";
|
|
2
|
+
import { createOperationRunner, listOperations } from "../shared/apiBinding.helper.js";
|
|
3
|
+
import { toApiWireError } from "../shared/apiWireResult.helper.js";
|
|
4
|
+
import { createAPIError } from "../../errors/index.js";
|
|
5
|
+
/**
|
|
6
|
+
* Wraps one operation as a queue processor.
|
|
7
|
+
*
|
|
8
|
+
* `job.data` is the operation input. The operation runs through the
|
|
9
|
+
* executor — validation, interceptors, timeout — under the job's abort
|
|
10
|
+
* signal, with the job id as request id and `job.metadata.correlationId`
|
|
11
|
+
* as correlation id. Success completes the job with the operation's
|
|
12
|
+
* output as its result. A failure throws an `APIError` carrying the same
|
|
13
|
+
* client-safe message and code the other bindings expose (the original
|
|
14
|
+
* is its `cause`), so the queue retries and dead-letters it as usual and
|
|
15
|
+
* `job.error` never holds internal detail; the original error also goes
|
|
16
|
+
* to `onInternalError`.
|
|
17
|
+
*
|
|
18
|
+
* Every failure is retried up to the job's `attempts`, validation
|
|
19
|
+
* failures included — enqueue with `attempts: 1` when input is not
|
|
20
|
+
* already known to be valid.
|
|
21
|
+
*/
|
|
22
|
+
export function createApiQueueProcessor(operation, options = {}) {
|
|
23
|
+
return buildProcessor(operation, createOperationRunner(options));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Registers a processor for every operation, under the operation's name
|
|
27
|
+
* as job name, and returns the job names. Enqueue work with
|
|
28
|
+
* `queue.add(operation.name, input)`.
|
|
29
|
+
*/
|
|
30
|
+
export function bindApiQueue(queue, operations, options = {}) {
|
|
31
|
+
const run = createOperationRunner(options);
|
|
32
|
+
const names = [];
|
|
33
|
+
for (const operation of listOperations(operations)) {
|
|
34
|
+
queue.process(operation.name, buildProcessor(operation, run));
|
|
35
|
+
names.push(operation.name);
|
|
36
|
+
}
|
|
37
|
+
return Object.freeze(names);
|
|
38
|
+
}
|
|
39
|
+
function buildProcessor(operation, run) {
|
|
40
|
+
return async (job, context) => {
|
|
41
|
+
const started = Date.now();
|
|
42
|
+
const { result, requestId } = await run({
|
|
43
|
+
operation,
|
|
44
|
+
input: job.data,
|
|
45
|
+
source: job,
|
|
46
|
+
transport: "queue",
|
|
47
|
+
requestId: job.id,
|
|
48
|
+
correlationId: job.metadata?.["correlationId"],
|
|
49
|
+
signal: context.signal,
|
|
50
|
+
});
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
const wire = toApiWireError(result.error, requestId);
|
|
53
|
+
throw createAPIError(wire.message, {
|
|
54
|
+
code: wire.code,
|
|
55
|
+
statusCode: wire.statusCode,
|
|
56
|
+
expose: result.error.expose,
|
|
57
|
+
cause: result.error,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// Wrapped in a JobResult so an output that happens to carry a
|
|
61
|
+
// `success` field is not mistaken for a JobResult by the queue.
|
|
62
|
+
return createJobResult(result.data, Date.now() - started);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=apiQueue.binding.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue binding: consumes `@zudojs/queue` jobs by running the operation
|
|
3
|
+
* named by the job, with the job payload as input.
|
|
4
|
+
*/
|
|
5
|
+
export type { APIQueueBindingOptions, APIQueueTarget } from "./apiQueue.binding.js";
|
|
6
|
+
export { bindApiQueue, createApiQueueProcessor } from "./apiQueue.binding.js";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|