@zudojs/api 1.1.1 → 1.2.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/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,33 @@
|
|
|
1
|
+
import type { APIOperation } from "../../operation/operation.type.js";
|
|
2
|
+
import type { APIOperationRoute } from "./apiRoute.type.js";
|
|
3
|
+
/**
|
|
4
|
+
* Splits and validates a path template, returning its segments.
|
|
5
|
+
*
|
|
6
|
+
* @throws {TypeError} if `path` is not a string.
|
|
7
|
+
* @throws {RangeError} if `path` is empty, over-long, does not start with
|
|
8
|
+
* `/`, has an empty or illegal segment, or repeats a parameter name.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseRoutePath(path: unknown, label: string): readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* Validates `metadata.http` on an operation definition.
|
|
13
|
+
*
|
|
14
|
+
* @throws {TypeError | RangeError} for an unknown method or invalid path.
|
|
15
|
+
*/
|
|
16
|
+
export declare function assertValidHttpOptions(http: unknown, operationName: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* Resolves the HTTP route of one operation.
|
|
19
|
+
*
|
|
20
|
+
* Without `metadata.http` the route is `POST /<operation name>`; a name
|
|
21
|
+
* whose default path would contain an empty or `:`-prefixed segment must
|
|
22
|
+
* declare `metadata.http.path` explicitly.
|
|
23
|
+
*
|
|
24
|
+
* @param basePath Prefix such as `"/api"`; `"/"` or `undefined` for none.
|
|
25
|
+
* @throws {TypeError | RangeError} if the route or base path is invalid.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveApiRoute(operation: APIOperation, basePath?: string): APIOperationRoute;
|
|
28
|
+
/**
|
|
29
|
+
* Returns the conflict key of a route: two routes with the same method
|
|
30
|
+
* and the same path shape (parameter names ignored) can never both match.
|
|
31
|
+
*/
|
|
32
|
+
export declare function routeConflictKey(route: APIOperationRoute): string;
|
|
33
|
+
//# sourceMappingURL=apiRoute.resolver.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
2
|
+
const MAX_PATH_LENGTH = 512;
|
|
3
|
+
const LITERAL_SEGMENT = /^[A-Za-z0-9._~!$&'()*+,;=@-][A-Za-z0-9._~!$&'()*+,;=@:-]*$/;
|
|
4
|
+
const PARAM_SEGMENT = /^:([A-Za-z_][A-Za-z0-9_]*)$/;
|
|
5
|
+
const UNSAFE_PARAM_NAMES = new Set(["__proto__", "constructor", "prototype"]);
|
|
6
|
+
/**
|
|
7
|
+
* Splits and validates a path template, returning its segments.
|
|
8
|
+
*
|
|
9
|
+
* @throws {TypeError} if `path` is not a string.
|
|
10
|
+
* @throws {RangeError} if `path` is empty, over-long, does not start with
|
|
11
|
+
* `/`, has an empty or illegal segment, or repeats a parameter name.
|
|
12
|
+
*/
|
|
13
|
+
export function parseRoutePath(path, label) {
|
|
14
|
+
if (typeof path !== "string") {
|
|
15
|
+
throw new TypeError(`${label} must be a string, received ${typeof path}.`);
|
|
16
|
+
}
|
|
17
|
+
if (path.length === 0 || path.length > MAX_PATH_LENGTH || !path.startsWith("/")) {
|
|
18
|
+
throw new RangeError(`${label} must start with "/" and be at most ${MAX_PATH_LENGTH} characters.`);
|
|
19
|
+
}
|
|
20
|
+
const trimmed = path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
21
|
+
const segments = trimmed === "/" ? [] : trimmed.slice(1).split("/");
|
|
22
|
+
const params = new Set();
|
|
23
|
+
for (const segment of segments) {
|
|
24
|
+
const param = PARAM_SEGMENT.exec(segment)?.[1];
|
|
25
|
+
if (param !== undefined) {
|
|
26
|
+
if (UNSAFE_PARAM_NAMES.has(param) || params.has(param)) {
|
|
27
|
+
throw new RangeError(`${label} "${path}" repeats or misuses parameter ":${param}".`);
|
|
28
|
+
}
|
|
29
|
+
params.add(param);
|
|
30
|
+
}
|
|
31
|
+
else if (!LITERAL_SEGMENT.test(segment)) {
|
|
32
|
+
throw new RangeError(`${label} "${path}" has an empty or invalid segment "${segment}".`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return segments;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Validates `metadata.http` on an operation definition.
|
|
39
|
+
*
|
|
40
|
+
* @throws {TypeError | RangeError} for an unknown method or invalid path.
|
|
41
|
+
*/
|
|
42
|
+
export function assertValidHttpOptions(http, operationName) {
|
|
43
|
+
if (http === undefined) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (typeof http !== "object" || http === null) {
|
|
47
|
+
throw new TypeError(`Operation "${operationName}" metadata.http must be an object.`);
|
|
48
|
+
}
|
|
49
|
+
const { method, path } = http;
|
|
50
|
+
if (method !== undefined && (typeof method !== "string" || !METHODS.has(method))) {
|
|
51
|
+
throw new RangeError(`Operation "${operationName}" metadata.http.method must be one of ${[...METHODS].join(", ")}.`);
|
|
52
|
+
}
|
|
53
|
+
if (path !== undefined) {
|
|
54
|
+
parseRoutePath(path, `Operation "${operationName}" metadata.http.path`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolves the HTTP route of one operation.
|
|
59
|
+
*
|
|
60
|
+
* Without `metadata.http` the route is `POST /<operation name>`; a name
|
|
61
|
+
* whose default path would contain an empty or `:`-prefixed segment must
|
|
62
|
+
* declare `metadata.http.path` explicitly.
|
|
63
|
+
*
|
|
64
|
+
* @param basePath Prefix such as `"/api"`; `"/"` or `undefined` for none.
|
|
65
|
+
* @throws {TypeError | RangeError} if the route or base path is invalid.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveApiRoute(operation, basePath) {
|
|
68
|
+
const http = operation.metadata?.http;
|
|
69
|
+
assertValidHttpOptions(http, operation.name);
|
|
70
|
+
const method = http?.method ?? "POST";
|
|
71
|
+
const own = http?.path ?? `/${operation.name}`;
|
|
72
|
+
const ownSegments = parseRoutePath(own, `Operation "${operation.name}" route path`);
|
|
73
|
+
if (http?.path === undefined && ownSegments.some((s) => s.startsWith(":"))) {
|
|
74
|
+
throw new RangeError(`Operation "${operation.name}" cannot use its name as a path; declare metadata.http.path.`);
|
|
75
|
+
}
|
|
76
|
+
const baseSegments = basePath === undefined ? [] : parseRoutePath(basePath, "basePath");
|
|
77
|
+
const segments = [...baseSegments, ...ownSegments];
|
|
78
|
+
const path = `/${segments.join("/")}`;
|
|
79
|
+
parseRoutePath(path, `Operation "${operation.name}" route path`);
|
|
80
|
+
const pathParams = segments.filter((s) => s.startsWith(":")).map((s) => s.slice(1));
|
|
81
|
+
const inputSource = method === "GET" || method === "DELETE" ? "query" : "body";
|
|
82
|
+
const metadata = operation.metadata;
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
operationId: operation.name,
|
|
85
|
+
method,
|
|
86
|
+
path,
|
|
87
|
+
pathParams: Object.freeze(pathParams),
|
|
88
|
+
inputSource,
|
|
89
|
+
...(operation.input !== undefined ? { input: operation.input } : {}),
|
|
90
|
+
...(operation.output !== undefined ? { output: operation.output } : {}),
|
|
91
|
+
...(metadata?.description !== undefined ? { description: metadata.description } : {}),
|
|
92
|
+
...(metadata?.tags !== undefined ? { tags: metadata.tags } : {}),
|
|
93
|
+
...(metadata?.deprecated !== undefined ? { deprecated: metadata.deprecated } : {}),
|
|
94
|
+
...(metadata?.version !== undefined ? { version: metadata.version } : {}),
|
|
95
|
+
successStatus: 200,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Returns the conflict key of a route: two routes with the same method
|
|
100
|
+
* and the same path shape (parameter names ignored) can never both match.
|
|
101
|
+
*/
|
|
102
|
+
export function routeConflictKey(route) {
|
|
103
|
+
const shape = route.path
|
|
104
|
+
.split("/")
|
|
105
|
+
.map((segment) => (segment.startsWith(":") ? ":" : segment))
|
|
106
|
+
.join("/");
|
|
107
|
+
return `${route.method} ${shape}`;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=apiRoute.resolver.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { APIOperation } from "../../operation/operation.type.js";
|
|
2
|
+
import type { APIOperationSource } from "../shared/apiBinding.type.js";
|
|
3
|
+
import type { APIOperationRoute } from "./apiRoute.type.js";
|
|
4
|
+
/**
|
|
5
|
+
* Options for {@link describeApiRoutes}.
|
|
6
|
+
*/
|
|
7
|
+
export interface DescribeApiRoutesOptions {
|
|
8
|
+
/** Prefix for every route, e.g. `"/api"`. */
|
|
9
|
+
readonly basePath?: string;
|
|
10
|
+
}
|
|
11
|
+
/** A route paired with the operation it runs. */
|
|
12
|
+
export interface APIRouteEntry {
|
|
13
|
+
readonly route: APIOperationRoute;
|
|
14
|
+
readonly operation: APIOperation;
|
|
15
|
+
readonly segments: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Result of matching a request against a route table.
|
|
19
|
+
*/
|
|
20
|
+
export type APIRouteMatch = {
|
|
21
|
+
readonly kind: "found";
|
|
22
|
+
readonly entry: APIRouteEntry;
|
|
23
|
+
readonly params: Readonly<Record<string, string>>;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: "method-not-allowed";
|
|
26
|
+
readonly allow: readonly string[];
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: "not-found";
|
|
29
|
+
} | {
|
|
30
|
+
readonly kind: "bad-path";
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Describes the HTTP route of every operation — the structural contract
|
|
34
|
+
* `@zudojs/http` mounts and `@zudojs/openapi` documents.
|
|
35
|
+
*
|
|
36
|
+
* @throws {TypeError | RangeError} if a route is invalid, or if two
|
|
37
|
+
* operations bind the same method and path shape.
|
|
38
|
+
*/
|
|
39
|
+
export declare function describeApiRoutes(operations: APIOperationSource, options?: DescribeApiRoutesOptions): readonly APIOperationRoute[];
|
|
40
|
+
/**
|
|
41
|
+
* A compiled, immutable route table.
|
|
42
|
+
*/
|
|
43
|
+
export interface APIRouteTable {
|
|
44
|
+
readonly entries: readonly APIRouteEntry[];
|
|
45
|
+
match(method: string, pathname: string): APIRouteMatch;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Compiles operations into a route table for request matching.
|
|
49
|
+
*
|
|
50
|
+
* @throws {TypeError | RangeError} as {@link describeApiRoutes}.
|
|
51
|
+
*/
|
|
52
|
+
export declare function compileRouteTable(operations: APIOperationSource, basePath?: string): APIRouteTable;
|
|
53
|
+
//# sourceMappingURL=apiRoute.table.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { listOperations } from "../shared/apiBinding.helper.js";
|
|
2
|
+
import { resolveApiRoute, routeConflictKey } from "./apiRoute.resolver.js";
|
|
3
|
+
/**
|
|
4
|
+
* Describes the HTTP route of every operation — the structural contract
|
|
5
|
+
* `@zudojs/http` mounts and `@zudojs/openapi` documents.
|
|
6
|
+
*
|
|
7
|
+
* @throws {TypeError | RangeError} if a route is invalid, or if two
|
|
8
|
+
* operations bind the same method and path shape.
|
|
9
|
+
*/
|
|
10
|
+
export function describeApiRoutes(operations, options = {}) {
|
|
11
|
+
return Object.freeze(compileEntries(operations, options.basePath).map((e) => e.route));
|
|
12
|
+
}
|
|
13
|
+
function compileEntries(operations, basePath) {
|
|
14
|
+
const owners = new Map();
|
|
15
|
+
const entries = listOperations(operations).map((operation) => {
|
|
16
|
+
const route = resolveApiRoute(operation, basePath);
|
|
17
|
+
const key = routeConflictKey(route);
|
|
18
|
+
const owner = owners.get(key);
|
|
19
|
+
if (owner !== undefined) {
|
|
20
|
+
throw new RangeError(`Operations "${owner}" and "${operation.name}" both bind ${route.method} ${route.path}.`);
|
|
21
|
+
}
|
|
22
|
+
owners.set(key, operation.name);
|
|
23
|
+
const segments = route.path === "/" ? [] : route.path.slice(1).split("/");
|
|
24
|
+
return { route, operation, segments };
|
|
25
|
+
});
|
|
26
|
+
return entries.sort((a, b) => specificity(a.segments, b.segments));
|
|
27
|
+
}
|
|
28
|
+
/** Orders literal segments before parameters, so `/users/me` beats `/users/:id`. */
|
|
29
|
+
function specificity(a, b) {
|
|
30
|
+
for (let i = 0; i < Math.min(a.length, b.length); i += 1) {
|
|
31
|
+
const diff = Number(a[i].startsWith(":")) - Number(b[i].startsWith(":"));
|
|
32
|
+
if (diff !== 0) {
|
|
33
|
+
return diff;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Compiles operations into a route table for request matching.
|
|
40
|
+
*
|
|
41
|
+
* @throws {TypeError | RangeError} as {@link describeApiRoutes}.
|
|
42
|
+
*/
|
|
43
|
+
export function compileRouteTable(operations, basePath) {
|
|
44
|
+
const entries = Object.freeze(compileEntries(operations, basePath));
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
entries,
|
|
47
|
+
match(method, pathname) {
|
|
48
|
+
const trimmed = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
|
49
|
+
const parts = trimmed === "/" || trimmed === "" ? [] : trimmed.slice(1).split("/");
|
|
50
|
+
const allow = new Set();
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
const params = matchSegments(entry.segments, parts);
|
|
53
|
+
if (params === "bad") {
|
|
54
|
+
return { kind: "bad-path" };
|
|
55
|
+
}
|
|
56
|
+
if (params === undefined) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (entry.route.method === method) {
|
|
60
|
+
return { kind: "found", entry, params };
|
|
61
|
+
}
|
|
62
|
+
allow.add(entry.route.method);
|
|
63
|
+
}
|
|
64
|
+
return allow.size > 0
|
|
65
|
+
? { kind: "method-not-allowed", allow: [...allow] }
|
|
66
|
+
: { kind: "not-found" };
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function matchSegments(template, parts) {
|
|
71
|
+
if (template.length !== parts.length) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const params = {};
|
|
75
|
+
for (let i = 0; i < template.length; i += 1) {
|
|
76
|
+
const expected = template[i];
|
|
77
|
+
const actual = parts[i];
|
|
78
|
+
if (expected.startsWith(":")) {
|
|
79
|
+
if (actual.length === 0) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
params[expected.slice(1)] = decodeURIComponent(actual);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return "bad";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else if (expected !== actual) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return params;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=apiRoute.table.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP route description for an operation.
|
|
3
|
+
*
|
|
4
|
+
* `@zudojs/api` does not import `@zudojs/http`; these types are the
|
|
5
|
+
* structural contract an HTTP layer (and OpenAPI generation) consumes.
|
|
6
|
+
*/
|
|
7
|
+
/** HTTP methods an operation can be bound to. */
|
|
8
|
+
export type APIHttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
9
|
+
/**
|
|
10
|
+
* Where a route reads operation input from: the query string (`GET`,
|
|
11
|
+
* `DELETE`) or a JSON request body (`POST`, `PUT`, `PATCH`). Path
|
|
12
|
+
* parameters are merged into the input either way.
|
|
13
|
+
*/
|
|
14
|
+
export type APIRouteInputSource = "query" | "body";
|
|
15
|
+
/**
|
|
16
|
+
* HTTP binding declared on an operation as `metadata.http`.
|
|
17
|
+
*
|
|
18
|
+
* Both fields are optional: an operation without them is served at
|
|
19
|
+
* `POST /<operation name>`.
|
|
20
|
+
*/
|
|
21
|
+
export interface APIOperationHttpOptions {
|
|
22
|
+
/** HTTP method. Defaults to `"POST"`. */
|
|
23
|
+
readonly method?: APIHttpMethod;
|
|
24
|
+
/**
|
|
25
|
+
* Path template, e.g. `"/users/:id"`. A segment starting with `:` is a
|
|
26
|
+
* path parameter merged into the input under that name. Defaults to
|
|
27
|
+
* `"/" + operation.name`.
|
|
28
|
+
*/
|
|
29
|
+
readonly path?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Stable, structural description of one operation's HTTP route.
|
|
33
|
+
*
|
|
34
|
+
* Produced by {@link describeApiRoutes}; consumed by `@zudojs/http` to
|
|
35
|
+
* mount routes and by `@zudojs/openapi` to generate documents. Contains
|
|
36
|
+
* only plain data plus the operation's own schema objects, so it can be
|
|
37
|
+
* read without importing this package's classes.
|
|
38
|
+
*/
|
|
39
|
+
export interface APIOperationRoute {
|
|
40
|
+
/** The operation's name — unique, and usable as an OpenAPI `operationId`. */
|
|
41
|
+
readonly operationId: string;
|
|
42
|
+
/** HTTP method. */
|
|
43
|
+
readonly method: APIHttpMethod;
|
|
44
|
+
/** Full path template including any base path, e.g. `"/api/users/:id"`. */
|
|
45
|
+
readonly path: string;
|
|
46
|
+
/** Names of the `:param` segments in `path`, in order. */
|
|
47
|
+
readonly pathParams: readonly string[];
|
|
48
|
+
/** Where non-path input is read from. */
|
|
49
|
+
readonly inputSource: APIRouteInputSource;
|
|
50
|
+
/** Input schema declared on the operation, if any. */
|
|
51
|
+
readonly input?: unknown;
|
|
52
|
+
/** Output schema declared on the operation, if any. */
|
|
53
|
+
readonly output?: unknown;
|
|
54
|
+
/** `metadata.description`. */
|
|
55
|
+
readonly description?: string;
|
|
56
|
+
/** `metadata.tags`. */
|
|
57
|
+
readonly tags?: readonly string[];
|
|
58
|
+
/** `metadata.deprecated`. */
|
|
59
|
+
readonly deprecated?: boolean;
|
|
60
|
+
/** `metadata.version`. */
|
|
61
|
+
readonly version?: string;
|
|
62
|
+
/** Status sent for a successful call. Always `200`. */
|
|
63
|
+
readonly successStatus: 200;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=apiRoute.type.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP routes for operations: the structural {@link APIOperationRoute}
|
|
3
|
+
* contract (method, path template, operation id, schemas) that an HTTP
|
|
4
|
+
* layer mounts and OpenAPI generation documents, plus route resolution,
|
|
5
|
+
* validation and request matching.
|
|
6
|
+
*/
|
|
7
|
+
export type { APIHttpMethod, APIRouteInputSource, APIOperationHttpOptions, APIOperationRoute, } from "./apiRoute.type.js";
|
|
8
|
+
export { assertValidHttpOptions, parseRoutePath, resolveApiRoute, routeConflictKey, } from "./apiRoute.resolver.js";
|
|
9
|
+
export type { DescribeApiRoutesOptions, APIRouteEntry, APIRouteMatch, APIRouteTable, } from "./apiRoute.table.js";
|
|
10
|
+
export { compileRouteTable, describeApiRoutes } from "./apiRoute.table.js";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP routes for operations: the structural {@link APIOperationRoute}
|
|
3
|
+
* contract (method, path template, operation id, schemas) that an HTTP
|
|
4
|
+
* layer mounts and OpenAPI generation documents, plus route resolution,
|
|
5
|
+
* validation and request matching.
|
|
6
|
+
*/
|
|
7
|
+
export { assertValidHttpOptions, parseRoutePath, resolveApiRoute, routeConflictKey, } from "./apiRoute.resolver.js";
|
|
8
|
+
export { compileRouteTable, describeApiRoutes } from "./apiRoute.table.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { RPCContext, RPCProcedure } from "@zudojs/rpc";
|
|
2
|
+
import type { AnyAPIOperation, APIOperation } from "../../operation/operation.type.js";
|
|
3
|
+
import type { APIBindingOptions, APIOperationSource } from "../shared/apiBinding.type.js";
|
|
4
|
+
/**
|
|
5
|
+
* Extra time the RPC dispatcher allows beyond an operation's own
|
|
6
|
+
* deadline, so the executor's `APITimeoutError` (mapped to
|
|
7
|
+
* `RPC_TIMEOUT`) is what a slow call reports.
|
|
8
|
+
*/
|
|
9
|
+
export declare const API_RPC_TIMEOUT_MARGIN_MS = 1000;
|
|
10
|
+
/**
|
|
11
|
+
* Options for the RPC binding. `state` receives the `RPCContext` — read
|
|
12
|
+
* the transport-verified `context.auth` there, never frame `metadata`.
|
|
13
|
+
*/
|
|
14
|
+
export interface APIRpcBindingOptions extends APIBindingOptions<RPCContext> {
|
|
15
|
+
/**
|
|
16
|
+
* Maps an operation to its procedure name. Defaults to the operation
|
|
17
|
+
* name, which must then satisfy the RPC name rule (`"users.get"`).
|
|
18
|
+
*/
|
|
19
|
+
readonly procedureName?: (operation: APIOperation) => string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Anything procedures can be registered on: an `RPCServer`, an
|
|
23
|
+
* `RPCProcedureRegistry`, or an `RPCProcedureRouter`.
|
|
24
|
+
*/
|
|
25
|
+
export interface APIRpcProcedureTarget {
|
|
26
|
+
register(procedure: RPCProcedure): unknown;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Wraps one operation as an RPC procedure.
|
|
30
|
+
*
|
|
31
|
+
* The handler runs the operation through the executor — schema
|
|
32
|
+
* validation, interceptors and the operation timeout all apply — under
|
|
33
|
+
* the RPC call's abort signal, with the frame's request id and
|
|
34
|
+
* correlation id. A failed result is thrown as the matching RPC error
|
|
35
|
+
* (see `apiErrorToRPCError`), so the RPC server maps it to a wire code.
|
|
36
|
+
*
|
|
37
|
+
* @throws {RPCInvalidRequestError} if the procedure name is not a valid
|
|
38
|
+
* RPC name.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createApiRpcProcedure(operation: AnyAPIOperation, options?: APIRpcBindingOptions): RPCProcedure;
|
|
41
|
+
/**
|
|
42
|
+
* Registers every operation as an RPC procedure and returns the
|
|
43
|
+
* procedure names, in registration order.
|
|
44
|
+
*/
|
|
45
|
+
export declare function registerApiRpcProcedures(target: APIRpcProcedureTarget, operations: APIOperationSource, options?: APIRpcBindingOptions): readonly string[];
|
|
46
|
+
//# sourceMappingURL=apiRpc.binding.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createRPCProcedure } from "@zudojs/rpc";
|
|
2
|
+
import { createOperationRunner, listOperations } from "../shared/apiBinding.helper.js";
|
|
3
|
+
import { resolveOperationTimeout } from "../../operation/operation.type.js";
|
|
4
|
+
import { apiErrorToRPCError } from "./apiRpc.errors.js";
|
|
5
|
+
/**
|
|
6
|
+
* Extra time the RPC dispatcher allows beyond an operation's own
|
|
7
|
+
* deadline, so the executor's `APITimeoutError` (mapped to
|
|
8
|
+
* `RPC_TIMEOUT`) is what a slow call reports.
|
|
9
|
+
*/
|
|
10
|
+
export const API_RPC_TIMEOUT_MARGIN_MS = 1_000;
|
|
11
|
+
/**
|
|
12
|
+
* Wraps one operation as an RPC procedure.
|
|
13
|
+
*
|
|
14
|
+
* The handler runs the operation through the executor — schema
|
|
15
|
+
* validation, interceptors and the operation timeout all apply — under
|
|
16
|
+
* the RPC call's abort signal, with the frame's request id and
|
|
17
|
+
* correlation id. A failed result is thrown as the matching RPC error
|
|
18
|
+
* (see `apiErrorToRPCError`), so the RPC server maps it to a wire code.
|
|
19
|
+
*
|
|
20
|
+
* @throws {RPCInvalidRequestError} if the procedure name is not a valid
|
|
21
|
+
* RPC name.
|
|
22
|
+
*/
|
|
23
|
+
export function createApiRpcProcedure(operation, options = {}) {
|
|
24
|
+
return buildProcedure(operation, options, createOperationRunner(options));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Registers every operation as an RPC procedure and returns the
|
|
28
|
+
* procedure names, in registration order.
|
|
29
|
+
*/
|
|
30
|
+
export function registerApiRpcProcedures(target, operations, options = {}) {
|
|
31
|
+
const run = createOperationRunner(options);
|
|
32
|
+
const names = [];
|
|
33
|
+
for (const operation of listOperations(operations)) {
|
|
34
|
+
const procedure = buildProcedure(operation, options, run);
|
|
35
|
+
target.register(procedure);
|
|
36
|
+
names.push(procedure.name);
|
|
37
|
+
}
|
|
38
|
+
return Object.freeze(names);
|
|
39
|
+
}
|
|
40
|
+
function buildProcedure(operation, options, run) {
|
|
41
|
+
const name = options.procedureName?.(operation) ?? operation.name;
|
|
42
|
+
const metadata = operation.metadata;
|
|
43
|
+
return createRPCProcedure(name, async (input, context) => {
|
|
44
|
+
const { result } = await run({
|
|
45
|
+
operation,
|
|
46
|
+
input,
|
|
47
|
+
source: context,
|
|
48
|
+
transport: "rpc",
|
|
49
|
+
requestId: context.metadata.requestId ?? context.request.id,
|
|
50
|
+
correlationId: context.metadata.correlationId,
|
|
51
|
+
signal: context.signal,
|
|
52
|
+
});
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
throw apiErrorToRPCError(result.error, name);
|
|
55
|
+
}
|
|
56
|
+
return result.data;
|
|
57
|
+
}, {
|
|
58
|
+
timeout: resolveOperationTimeout(operation) + API_RPC_TIMEOUT_MARGIN_MS,
|
|
59
|
+
...(metadata?.description !== undefined ? { description: metadata.description } : {}),
|
|
60
|
+
...(metadata?.idempotent !== undefined ? { idempotent: metadata.idempotent } : {}),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=apiRpc.binding.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { RPCError } from "@zudojs/errors";
|
|
2
|
+
import type { APIError } from "../../errors/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Converts an operation's `APIError` into the RPC error the RPC server
|
|
5
|
+
* maps onto the wire.
|
|
6
|
+
*
|
|
7
|
+
* Errors with an RPC equivalent become it, so a remote caller sees the
|
|
8
|
+
* same wire codes (`RPC_VALIDATION_ERROR` with issue `details`,
|
|
9
|
+
* `RPC_UNAUTHENTICATED`, `RPC_FORBIDDEN`, `RPC_RATE_LIMITED`,
|
|
10
|
+
* `RPC_TIMEOUT`, `RPC_CANCELLED`, `RPC_UNAVAILABLE`) as for a native
|
|
11
|
+
* procedure. Anything else — `APIConflictError`, `APINotFoundError`, a
|
|
12
|
+
* custom `createAPIError` — becomes an `RPCError` that keeps the API
|
|
13
|
+
* error's `code`, `statusCode` and `expose` flag, so a domain code such as
|
|
14
|
+
* `ERR_API_CONFLICT` survives the trip while a non-exposed message is
|
|
15
|
+
* still withheld. A typed error built with `expose: false` keeps its code
|
|
16
|
+
* but travels with the generic internal message, exactly as over HTTP.
|
|
17
|
+
* The original error is kept as `cause` for `onInternalError`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function apiErrorToRPCError(error: APIError, procedure: string): RPCError;
|
|
20
|
+
//# sourceMappingURL=apiRpc.errors.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ErrorCode, RPCAuthenticationError, RPCCancelledError, RPCError, RPCForbiddenError, RPCInternalError, RPCRateLimitedError, RPCTimeoutError, RPCUnavailableError, RPCValidationError, } from "@zudojs/errors";
|
|
2
|
+
import { API_INTERNAL_ERROR_MESSAGE } from "../shared/apiWireResult.helper.js";
|
|
3
|
+
import { APIRateLimitError, APITimeoutError, APIValidationError, } from "../../errors/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Converts an operation's `APIError` into the RPC error the RPC server
|
|
6
|
+
* maps onto the wire.
|
|
7
|
+
*
|
|
8
|
+
* Errors with an RPC equivalent become it, so a remote caller sees the
|
|
9
|
+
* same wire codes (`RPC_VALIDATION_ERROR` with issue `details`,
|
|
10
|
+
* `RPC_UNAUTHENTICATED`, `RPC_FORBIDDEN`, `RPC_RATE_LIMITED`,
|
|
11
|
+
* `RPC_TIMEOUT`, `RPC_CANCELLED`, `RPC_UNAVAILABLE`) as for a native
|
|
12
|
+
* procedure. Anything else — `APIConflictError`, `APINotFoundError`, a
|
|
13
|
+
* custom `createAPIError` — becomes an `RPCError` that keeps the API
|
|
14
|
+
* error's `code`, `statusCode` and `expose` flag, so a domain code such as
|
|
15
|
+
* `ERR_API_CONFLICT` survives the trip while a non-exposed message is
|
|
16
|
+
* still withheld. A typed error built with `expose: false` keeps its code
|
|
17
|
+
* but travels with the generic internal message, exactly as over HTTP.
|
|
18
|
+
* The original error is kept as `cause` for `onInternalError`.
|
|
19
|
+
*/
|
|
20
|
+
export function apiErrorToRPCError(error, procedure) {
|
|
21
|
+
return withCause(translate(error, procedure), error);
|
|
22
|
+
}
|
|
23
|
+
function translate(error, procedure) {
|
|
24
|
+
// The RPC server sends the message of every typed error below, so an
|
|
25
|
+
// error built with `expose: false` must not hand its message over: the
|
|
26
|
+
// HTTP, CLI and queue bindings all replace it with the generic text.
|
|
27
|
+
const exposed = error.expose === true;
|
|
28
|
+
const message = exposed ? error.message : API_INTERNAL_ERROR_MESSAGE;
|
|
29
|
+
switch (error.code) {
|
|
30
|
+
case ErrorCode.API_VALIDATION:
|
|
31
|
+
return new RPCValidationError(message, exposed && error instanceof APIValidationError ? error.issues : [], procedure);
|
|
32
|
+
case ErrorCode.API_AUTHENTICATION:
|
|
33
|
+
return new RPCAuthenticationError(message, procedure);
|
|
34
|
+
case ErrorCode.API_AUTHORIZATION:
|
|
35
|
+
return new RPCForbiddenError(message, procedure);
|
|
36
|
+
case ErrorCode.API_RATE_LIMIT:
|
|
37
|
+
return new RPCRateLimitedError(message, error instanceof APIRateLimitError ? error.retryAfterSeconds : undefined, procedure);
|
|
38
|
+
case ErrorCode.API_TIMEOUT:
|
|
39
|
+
return new RPCTimeoutError(error instanceof APITimeoutError ? error.timeoutMs : 0, procedure);
|
|
40
|
+
case ErrorCode.OPERATION_CANCELLED:
|
|
41
|
+
return new RPCCancelledError(message, procedure);
|
|
42
|
+
case ErrorCode.API_UNAVAILABLE:
|
|
43
|
+
return new RPCUnavailableError(message, procedure);
|
|
44
|
+
case ErrorCode.API_INTERNAL:
|
|
45
|
+
return new RPCInternalError(message, procedure);
|
|
46
|
+
default:
|
|
47
|
+
return new RPCError(message, {
|
|
48
|
+
code: error.code,
|
|
49
|
+
statusCode: error.statusCode,
|
|
50
|
+
expose: error.expose,
|
|
51
|
+
procedureName: procedure,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function withCause(error, cause) {
|
|
56
|
+
Object.defineProperty(error, "cause", {
|
|
57
|
+
value: cause,
|
|
58
|
+
enumerable: false,
|
|
59
|
+
configurable: true,
|
|
60
|
+
writable: true,
|
|
61
|
+
});
|
|
62
|
+
return error;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=apiRpc.errors.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC binding: registers operations as `@zudojs/rpc` procedures, so any
|
|
3
|
+
* RPC transport (in-memory, HTTP) can call them, with API errors mapped
|
|
4
|
+
* onto RPC wire codes.
|
|
5
|
+
*/
|
|
6
|
+
export type { APIRpcBindingOptions, APIRpcProcedureTarget } from "./apiRpc.binding.js";
|
|
7
|
+
export { API_RPC_TIMEOUT_MARGIN_MS, createApiRpcProcedure, registerApiRpcProcedures, } from "./apiRpc.binding.js";
|
|
8
|
+
export { apiErrorToRPCError } from "./apiRpc.errors.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC binding: registers operations as `@zudojs/rpc` procedures, so any
|
|
3
|
+
* RPC transport (in-memory, HTTP) can call them, with API errors mapped
|
|
4
|
+
* onto RPC wire codes.
|
|
5
|
+
*/
|
|
6
|
+
export { API_RPC_TIMEOUT_MARGIN_MS, createApiRpcProcedure, registerApiRpcProcedures, } from "./apiRpc.binding.js";
|
|
7
|
+
export { apiErrorToRPCError } from "./apiRpc.errors.js";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { APIOperation } from "../../operation/operation.type.js";
|
|
2
|
+
import type { APIResult } from "../../result/apiResult.type.js";
|
|
3
|
+
import type { APITransportKind } from "../../context/contextKey.type.js";
|
|
4
|
+
import type { APIBindingOptions, APIOperationSource } from "./apiBinding.type.js";
|
|
5
|
+
/**
|
|
6
|
+
* Lists the operations of a source, validating a plain list the way the
|
|
7
|
+
* registry validates on `register`.
|
|
8
|
+
*
|
|
9
|
+
* @throws {TypeError | RangeError} for a malformed operation or a
|
|
10
|
+
* duplicated name in a list.
|
|
11
|
+
*/
|
|
12
|
+
export declare function listOperations(source: APIOperationSource): readonly APIOperation[];
|
|
13
|
+
/**
|
|
14
|
+
* One call as a binding describes it to the runner.
|
|
15
|
+
*/
|
|
16
|
+
export interface APIBoundCall<TSource> {
|
|
17
|
+
readonly operation: APIOperation;
|
|
18
|
+
readonly input: unknown;
|
|
19
|
+
readonly source: TSource;
|
|
20
|
+
readonly transport: APITransportKind;
|
|
21
|
+
/** Untrusted request id from the transport; normalized before use. */
|
|
22
|
+
readonly requestId?: unknown;
|
|
23
|
+
/** Untrusted correlation id; recorded only when it is a safe id. */
|
|
24
|
+
readonly correlationId?: unknown;
|
|
25
|
+
readonly signal?: AbortSignal;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Outcome of {@link createOperationRunner}'s `run`: the executor's result
|
|
29
|
+
* and the request id the call ran under.
|
|
30
|
+
*/
|
|
31
|
+
export interface APIBoundOutcome {
|
|
32
|
+
readonly result: APIResult<unknown>;
|
|
33
|
+
readonly requestId: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Creates the function every binding runs calls through, so the context,
|
|
37
|
+
* interceptors, error normalization and internal-error reporting are
|
|
38
|
+
* identical over HTTP, RPC, queues and the CLI. Input carrying a
|
|
39
|
+
* `__proto__`, `constructor` or `prototype` key anywhere is refused with
|
|
40
|
+
* an `APIValidationError` after `state` runs and before the operation
|
|
41
|
+
* does. `run` never throws.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createOperationRunner<TSource>(options: APIBindingOptions<TSource>): (call: APIBoundCall<TSource>) => Promise<APIBoundOutcome>;
|
|
44
|
+
//# sourceMappingURL=apiBinding.helper.d.ts.map
|