@zudojs/api 1.1.0 → 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 +166 -7
- 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 -191
- 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,40 @@
|
|
|
1
|
+
import type { APIContext } from "../context/context.type.js";
|
|
2
|
+
import type { APIError } from "../errors/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Error for an execution cancelled by the caller's `AbortSignal`.
|
|
5
|
+
*
|
|
6
|
+
* Carries `ErrorCode.OPERATION_CANCELLED` — branch on that, not on the
|
|
7
|
+
* status code. `statusCode` is 499, an nginx convention ("Client Closed
|
|
8
|
+
* Request") rather than an IANA status; this package is
|
|
9
|
+
* transport-agnostic, so each adapter should map the *code* onto whatever
|
|
10
|
+
* its protocol calls "cancelled" (gRPC `CANCELLED`, a dropped queue
|
|
11
|
+
* message, a non-zero CLI exit) rather than passing 499 to the wire.
|
|
12
|
+
*/
|
|
13
|
+
export declare function abortedError(operationName: string): APIError;
|
|
14
|
+
/**
|
|
15
|
+
* Returns a view of `context` whose `signal` is `signal`.
|
|
16
|
+
*
|
|
17
|
+
* Every other member resolves through the original context — `get`,
|
|
18
|
+
* `set` and `metadata` are delegated to it, so a value an interceptor set
|
|
19
|
+
* is visible to the handler and vice versa, and any extra members of a
|
|
20
|
+
* hand-rolled context are reachable through the prototype chain.
|
|
21
|
+
*/
|
|
22
|
+
export declare function withContextSignal(context: APIContext, signal: AbortSignal): APIContext;
|
|
23
|
+
/**
|
|
24
|
+
* Runs `run` under a deadline and the caller's abort signal.
|
|
25
|
+
*
|
|
26
|
+
* `run` receives an `AbortSignal` that aborts when the deadline elapses
|
|
27
|
+
* (reason: the `APITimeoutError` the call fails with) or the caller's
|
|
28
|
+
* signal aborts (reason: the `OPERATION_CANCELLED` error the call fails
|
|
29
|
+
* with). A cooperative handler that passes it on (to `fetch`, a driver
|
|
30
|
+
* query, a loop check) therefore stops working instead of running on
|
|
31
|
+
* with its result discarded — the source of duplicate side effects when
|
|
32
|
+
* a client retries a timed-out call.
|
|
33
|
+
*
|
|
34
|
+
* `timeoutMs` is always a positive integer here — `defineOperation`
|
|
35
|
+
* rejects anything else and `resolveOperationTimeout` substitutes the
|
|
36
|
+
* default for hand-rolled operations — so the deadline can never be
|
|
37
|
+
* silently disabled.
|
|
38
|
+
*/
|
|
39
|
+
export declare function withDeadline<T>(run: (signal: AbortSignal) => Promise<T> | T, timeoutMs: number, operationName: string, callerSignal?: AbortSignal): Promise<T>;
|
|
40
|
+
//# sourceMappingURL=executor.deadline.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { APITimeoutError, createAPIError, ErrorCode } from "../errors/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Error for an execution cancelled by the caller's `AbortSignal`.
|
|
4
|
+
*
|
|
5
|
+
* Carries `ErrorCode.OPERATION_CANCELLED` — branch on that, not on the
|
|
6
|
+
* status code. `statusCode` is 499, an nginx convention ("Client Closed
|
|
7
|
+
* Request") rather than an IANA status; this package is
|
|
8
|
+
* transport-agnostic, so each adapter should map the *code* onto whatever
|
|
9
|
+
* its protocol calls "cancelled" (gRPC `CANCELLED`, a dropped queue
|
|
10
|
+
* message, a non-zero CLI exit) rather than passing 499 to the wire.
|
|
11
|
+
*/
|
|
12
|
+
export function abortedError(operationName) {
|
|
13
|
+
return createAPIError(`Operation "${operationName}" was aborted.`, {
|
|
14
|
+
code: ErrorCode.OPERATION_CANCELLED,
|
|
15
|
+
statusCode: 499,
|
|
16
|
+
expose: true,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Returns a view of `context` whose `signal` is `signal`.
|
|
21
|
+
*
|
|
22
|
+
* Every other member resolves through the original context — `get`,
|
|
23
|
+
* `set` and `metadata` are delegated to it, so a value an interceptor set
|
|
24
|
+
* is visible to the handler and vice versa, and any extra members of a
|
|
25
|
+
* hand-rolled context are reachable through the prototype chain.
|
|
26
|
+
*/
|
|
27
|
+
export function withContextSignal(context, signal) {
|
|
28
|
+
const derived = Object.create(context, {
|
|
29
|
+
signal: { value: signal, enumerable: true },
|
|
30
|
+
requestId: { value: context.requestId, enumerable: true },
|
|
31
|
+
state: { value: context.state, enumerable: true },
|
|
32
|
+
get: { value: context.get.bind(context), enumerable: true },
|
|
33
|
+
set: { value: context.set.bind(context), enumerable: true },
|
|
34
|
+
metadata: { get: () => context.metadata, enumerable: true },
|
|
35
|
+
});
|
|
36
|
+
return Object.freeze(derived);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Runs `run` under a deadline and the caller's abort signal.
|
|
40
|
+
*
|
|
41
|
+
* `run` receives an `AbortSignal` that aborts when the deadline elapses
|
|
42
|
+
* (reason: the `APITimeoutError` the call fails with) or the caller's
|
|
43
|
+
* signal aborts (reason: the `OPERATION_CANCELLED` error the call fails
|
|
44
|
+
* with). A cooperative handler that passes it on (to `fetch`, a driver
|
|
45
|
+
* query, a loop check) therefore stops working instead of running on
|
|
46
|
+
* with its result discarded — the source of duplicate side effects when
|
|
47
|
+
* a client retries a timed-out call.
|
|
48
|
+
*
|
|
49
|
+
* `timeoutMs` is always a positive integer here — `defineOperation`
|
|
50
|
+
* rejects anything else and `resolveOperationTimeout` substitutes the
|
|
51
|
+
* default for hand-rolled operations — so the deadline can never be
|
|
52
|
+
* silently disabled.
|
|
53
|
+
*/
|
|
54
|
+
export function withDeadline(run, timeoutMs, operationName, callerSignal) {
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const cleanup = () => {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
callerSignal?.removeEventListener("abort", onAbort);
|
|
60
|
+
};
|
|
61
|
+
const fail = (error) => {
|
|
62
|
+
cleanup();
|
|
63
|
+
if (!controller.signal.aborted) {
|
|
64
|
+
controller.abort(error);
|
|
65
|
+
}
|
|
66
|
+
reject(error);
|
|
67
|
+
};
|
|
68
|
+
const onAbort = () => fail(abortedError(operationName));
|
|
69
|
+
const timer = setTimeout(() => fail(new APITimeoutError(timeoutMs)), timeoutMs);
|
|
70
|
+
callerSignal?.addEventListener("abort", onAbort, { once: true });
|
|
71
|
+
if (callerSignal?.aborted) {
|
|
72
|
+
onAbort();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
let pending;
|
|
76
|
+
try {
|
|
77
|
+
// `Promise.resolve`: a hand-rolled operation whose handler returns
|
|
78
|
+
// synchronously is a legal JavaScript caller.
|
|
79
|
+
pending = Promise.resolve(run(controller.signal));
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
pending = Promise.reject(error);
|
|
83
|
+
}
|
|
84
|
+
pending.then((value) => {
|
|
85
|
+
cleanup();
|
|
86
|
+
resolve(value);
|
|
87
|
+
}, (error) => {
|
|
88
|
+
cleanup();
|
|
89
|
+
reject(error);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=executor.deadline.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { APISchemaIssue } from "../schema/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Truncates `value` to at most `max` characters, marking the cut with `…`.
|
|
4
|
+
*/
|
|
5
|
+
export declare function truncate(value: string, max: number): string;
|
|
6
|
+
/**
|
|
7
|
+
* Renders a Standard Schema issue path as a dotted string. Path segments
|
|
8
|
+
* are field names, never submitted values, so they are safe to expose.
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatIssuePath(issue: APISchemaIssue): string;
|
|
11
|
+
/**
|
|
12
|
+
* Converts schema issues into the capped, redacted list carried on the
|
|
13
|
+
* client-facing `APIValidationError`.
|
|
14
|
+
*
|
|
15
|
+
* With `exposeMessages` off (the default), each entry names only the
|
|
16
|
+
* failing path (`"user.email: invalid"`), never the submitted value.
|
|
17
|
+
*/
|
|
18
|
+
export declare function toClientIssues(issues: ReadonlyArray<APISchemaIssue>, limit: number, exposeMessages: boolean): readonly string[];
|
|
19
|
+
//# sourceMappingURL=executor.issues.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { MAX_VALIDATION_ISSUE_LENGTH } from "../constants.js";
|
|
2
|
+
/**
|
|
3
|
+
* Truncates `value` to at most `max` characters, marking the cut with `…`.
|
|
4
|
+
*/
|
|
5
|
+
export function truncate(value, max) {
|
|
6
|
+
const text = typeof value === "string" ? value : String(value);
|
|
7
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Renders a Standard Schema issue path as a dotted string. Path segments
|
|
11
|
+
* are field names, never submitted values, so they are safe to expose.
|
|
12
|
+
*/
|
|
13
|
+
export function formatIssuePath(issue) {
|
|
14
|
+
const path = issue.path;
|
|
15
|
+
if (path === undefined || path.length === 0) {
|
|
16
|
+
return "(root)";
|
|
17
|
+
}
|
|
18
|
+
return path
|
|
19
|
+
.map((segment) => {
|
|
20
|
+
const key = typeof segment === "object" && segment !== null && "key" in segment
|
|
21
|
+
? segment.key
|
|
22
|
+
: segment;
|
|
23
|
+
return truncate(String(key), 64);
|
|
24
|
+
})
|
|
25
|
+
.join(".");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Converts schema issues into the capped, redacted list carried on the
|
|
29
|
+
* client-facing `APIValidationError`.
|
|
30
|
+
*
|
|
31
|
+
* With `exposeMessages` off (the default), each entry names only the
|
|
32
|
+
* failing path (`"user.email: invalid"`), never the submitted value.
|
|
33
|
+
*/
|
|
34
|
+
export function toClientIssues(issues, limit, exposeMessages) {
|
|
35
|
+
const shown = issues
|
|
36
|
+
.slice(0, limit)
|
|
37
|
+
.map((issue) => exposeMessages
|
|
38
|
+
? truncate(issue.message, MAX_VALIDATION_ISSUE_LENGTH)
|
|
39
|
+
: `${formatIssuePath(issue)}: invalid`);
|
|
40
|
+
const omitted = issues.length - shown.length;
|
|
41
|
+
if (omitted > 0) {
|
|
42
|
+
shown.push(`… and ${omitted} more issue(s) omitted.`);
|
|
43
|
+
}
|
|
44
|
+
return shown;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=executor.issues.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { APIError } from "../errors/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Error normalizer for converting unknown errors into APIError instances.
|
|
4
|
+
*
|
|
5
|
+
* APIErrors pass through untouched. Everything else is wrapped in an
|
|
6
|
+
* `APIInternalError` (`expose: false`) carrying a generic message; the
|
|
7
|
+
* original error is preserved on `cause` for logging.
|
|
8
|
+
*
|
|
9
|
+
* The wrapper's own message is deliberately *not* a copy of the original.
|
|
10
|
+
* `BaseError.toJSON()` in `@zudojs/errors` 0.1.0 emits `message`, `stack`
|
|
11
|
+
* and the serialized `cause` regardless of `expose`, so a transport doing
|
|
12
|
+
* `res.json(result.error)` would otherwise ship the raw driver message
|
|
13
|
+
* (connection strings, constraint names, file paths, tokens) to a client.
|
|
14
|
+
*/
|
|
15
|
+
export declare function normalizeAPIError(error: unknown, operationName?: string): APIError;
|
|
16
|
+
//# sourceMappingURL=executor.normalize.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { APIInternalError, isAPIError } from "../errors/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Error normalizer for converting unknown errors into APIError instances.
|
|
4
|
+
*
|
|
5
|
+
* APIErrors pass through untouched. Everything else is wrapped in an
|
|
6
|
+
* `APIInternalError` (`expose: false`) carrying a generic message; the
|
|
7
|
+
* original error is preserved on `cause` for logging.
|
|
8
|
+
*
|
|
9
|
+
* The wrapper's own message is deliberately *not* a copy of the original.
|
|
10
|
+
* `BaseError.toJSON()` in `@zudojs/errors` 0.1.0 emits `message`, `stack`
|
|
11
|
+
* and the serialized `cause` regardless of `expose`, so a transport doing
|
|
12
|
+
* `res.json(result.error)` would otherwise ship the raw driver message
|
|
13
|
+
* (connection strings, constraint names, file paths, tokens) to a client.
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeAPIError(error, operationName) {
|
|
16
|
+
if (isAPIError(error)) {
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
const where = operationName !== undefined && operationName !== ""
|
|
20
|
+
? `operation "${operationName}"`
|
|
21
|
+
: "an API operation";
|
|
22
|
+
const wrapped = error instanceof Error
|
|
23
|
+
? new APIInternalError(`An unexpected internal error occurred in ${where}.`)
|
|
24
|
+
: new APIInternalError(`A non-error value (${describeValueType(error)}) was thrown in ${where}.`);
|
|
25
|
+
// `APIInternalError`'s subclass constructor forwards only
|
|
26
|
+
// `{ endpoint, method }`, so `cause` cannot be passed through it.
|
|
27
|
+
// `APIError` / `createAPIError` *do* accept `cause`, but constructing
|
|
28
|
+
// through them would lose the `APIInternalError` class identity that
|
|
29
|
+
// consumers match on. `cause` is a declared writable class field on
|
|
30
|
+
// `BaseError`, so assigning it after construction is equivalent for
|
|
31
|
+
// `toJSON()` and for `error.cause` reads; only the native `[[cause]]`
|
|
32
|
+
// slot differs.
|
|
33
|
+
wrapped.cause = error;
|
|
34
|
+
return wrapped;
|
|
35
|
+
}
|
|
36
|
+
function describeValueType(value) {
|
|
37
|
+
if (value === null) {
|
|
38
|
+
return "null";
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return "array";
|
|
42
|
+
}
|
|
43
|
+
return typeof value;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=executor.normalize.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { APIInterceptor } from "../interceptors/interceptor.type.js";
|
|
2
|
+
/**
|
|
3
|
+
* Options for {@link APIExecutor}.
|
|
4
|
+
*/
|
|
5
|
+
export interface APIExecutorOptions {
|
|
6
|
+
/** Interceptor pipeline, outermost first. */
|
|
7
|
+
readonly interceptors?: readonly APIInterceptor[];
|
|
8
|
+
/**
|
|
9
|
+
* Whether raw schema issue messages are copied into the client-facing
|
|
10
|
+
* `APIValidationError` (which is `expose: true`).
|
|
11
|
+
*
|
|
12
|
+
* Default `false`. Schema messages routinely interpolate the received
|
|
13
|
+
* value — Zod's built-in messages do for several checks, and most
|
|
14
|
+
* hand-written `message:` strings do — which would put submitted
|
|
15
|
+
* secrets straight into a 422 body. With the default, clients receive
|
|
16
|
+
* one entry per failing path (`"user.email: invalid"`) naming *where*
|
|
17
|
+
* validation failed but never echoing *what* was submitted.
|
|
18
|
+
*
|
|
19
|
+
* Set to `true` only when every schema in the process is known to
|
|
20
|
+
* produce value-free messages.
|
|
21
|
+
*/
|
|
22
|
+
readonly exposeValidationMessages?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Maximum number of validation issues carried on a single
|
|
25
|
+
* `APIValidationError`. Defaults to `MAX_VALIDATION_ISSUES`.
|
|
26
|
+
*
|
|
27
|
+
* A schema over a large array emits one issue per failing element, so
|
|
28
|
+
* an uncapped list is an amplification vector: the executor is the
|
|
29
|
+
* layer on the untrusted-input boundary and caps it here.
|
|
30
|
+
*/
|
|
31
|
+
readonly maxValidationIssues?: number;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=executor.type.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { APIResult } from "../result/apiResult.type.js";
|
|
2
|
+
import type { APIOperation } from "../operation/operation.type.js";
|
|
3
|
+
/**
|
|
4
|
+
* How client-facing validation issues are reported.
|
|
5
|
+
*/
|
|
6
|
+
export interface APIInputIssueOptions {
|
|
7
|
+
/** Maximum number of issues carried on the error. */
|
|
8
|
+
readonly maxIssues: number;
|
|
9
|
+
/** Whether raw schema messages are copied (otherwise paths only). */
|
|
10
|
+
readonly exposeMessages: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Validates input against `operation.input`, returning the validated
|
|
14
|
+
* (possibly transformed) value, or a client-facing `APIValidationError`
|
|
15
|
+
* (422) whose issues name failing paths, never submitted values.
|
|
16
|
+
*
|
|
17
|
+
* A schema that throws, or is unrecognised, fails closed with an
|
|
18
|
+
* internal error rather than letting the input through.
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateOperationInput<TInput, TOutput>(operation: APIOperation<TInput, TOutput>, input: TInput, options: APIInputIssueOptions): Promise<APIResult<TInput>>;
|
|
21
|
+
/**
|
|
22
|
+
* Validates handler output against `operation.output`.
|
|
23
|
+
*
|
|
24
|
+
* A response that does not match its declared schema is a server bug,
|
|
25
|
+
* not a client mistake, so failures surface as an `APIInternalError`
|
|
26
|
+
* (500, `expose: false`) naming only the failing paths — never the
|
|
27
|
+
* offending values, which are exactly the fields (password hashes,
|
|
28
|
+
* internal audit columns) that should not reach a client.
|
|
29
|
+
*/
|
|
30
|
+
export declare function validateOperationOutput<TInput, TOutput>(operation: APIOperation<TInput, TOutput>, output: TOutput, maxIssues: number): Promise<APIResult<TOutput>>;
|
|
31
|
+
//# sourceMappingURL=executor.validation.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { apiFailure, apiSuccess } from "../result/apiResult.type.js";
|
|
2
|
+
import { APIInternalError, APIValidationError } from "../errors/index.js";
|
|
3
|
+
import { validateWithSchema } from "../schema/index.js";
|
|
4
|
+
import { normalizeAPIError } from "./executor.normalize.js";
|
|
5
|
+
import { formatIssuePath, toClientIssues } from "./executor.issues.js";
|
|
6
|
+
/**
|
|
7
|
+
* Validates input against `operation.input`, returning the validated
|
|
8
|
+
* (possibly transformed) value, or a client-facing `APIValidationError`
|
|
9
|
+
* (422) whose issues name failing paths, never submitted values.
|
|
10
|
+
*
|
|
11
|
+
* A schema that throws, or is unrecognised, fails closed with an
|
|
12
|
+
* internal error rather than letting the input through.
|
|
13
|
+
*/
|
|
14
|
+
export async function validateOperationInput(operation, input, options) {
|
|
15
|
+
if (operation.input === undefined) {
|
|
16
|
+
return apiSuccess(input);
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const validation = await validateWithSchema(operation.input, input);
|
|
20
|
+
if (!validation.ok) {
|
|
21
|
+
return apiFailure(new APIValidationError(`Invalid input for operation "${operation.name}".`, toClientIssues(validation.issues, options.maxIssues, options.exposeMessages)));
|
|
22
|
+
}
|
|
23
|
+
return apiSuccess(validation.value);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return apiFailure(normalizeAPIError(error, operation.name));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Validates handler output against `operation.output`.
|
|
31
|
+
*
|
|
32
|
+
* A response that does not match its declared schema is a server bug,
|
|
33
|
+
* not a client mistake, so failures surface as an `APIInternalError`
|
|
34
|
+
* (500, `expose: false`) naming only the failing paths — never the
|
|
35
|
+
* offending values, which are exactly the fields (password hashes,
|
|
36
|
+
* internal audit columns) that should not reach a client.
|
|
37
|
+
*/
|
|
38
|
+
export async function validateOperationOutput(operation, output, maxIssues) {
|
|
39
|
+
if (operation.output === undefined) {
|
|
40
|
+
return apiSuccess(output);
|
|
41
|
+
}
|
|
42
|
+
let validation;
|
|
43
|
+
try {
|
|
44
|
+
validation = await validateWithSchema(operation.output, output);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return apiFailure(normalizeAPIError(error, operation.name));
|
|
48
|
+
}
|
|
49
|
+
if (!validation.ok) {
|
|
50
|
+
const paths = validation.issues
|
|
51
|
+
.slice(0, maxIssues)
|
|
52
|
+
.map(formatIssuePath)
|
|
53
|
+
.join(", ");
|
|
54
|
+
return apiFailure(new APIInternalError(`Invalid output for operation "${operation.name}" at: ${paths}.`));
|
|
55
|
+
}
|
|
56
|
+
return apiSuccess(validation.value);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=executor.validation.js.map
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/api/executor
|
|
3
|
+
*
|
|
4
|
+
* Runs an operation through its interceptor pipeline: interceptors first,
|
|
5
|
+
* then input validation, the handler under its deadline and abort signal,
|
|
6
|
+
* and output validation.
|
|
7
|
+
*/
|
|
1
8
|
export { APIExecutor } from "./executor.core.js";
|
|
2
|
-
export { normalizeAPIError } from "./executor.
|
|
3
|
-
export type { APIExecutorOptions } from "./executor.
|
|
9
|
+
export { normalizeAPIError } from "./executor.normalize.js";
|
|
10
|
+
export type { APIExecutorOptions } from "./executor.type.js";
|
|
4
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/api/executor
|
|
3
|
+
*
|
|
4
|
+
* Runs an operation through its interceptor pipeline: interceptors first,
|
|
5
|
+
* then input validation, the handler under its deadline and abort signal,
|
|
6
|
+
* and output validation.
|
|
7
|
+
*/
|
|
1
8
|
export { APIExecutor } from "./executor.core.js";
|
|
2
|
-
export { normalizeAPIError } from "./executor.
|
|
9
|
+
export { normalizeAPIError } from "./executor.normalize.js";
|
|
3
10
|
//# sourceMappingURL=index.js.map
|
|
@@ -3,10 +3,13 @@ import type { APIContext } from "../context/context.type.js";
|
|
|
3
3
|
* Handler for an API operation.
|
|
4
4
|
*
|
|
5
5
|
* Receives the operation input and the execution context, and returns
|
|
6
|
-
* the operation output.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* the operation output. When the operation declares an `input` schema (a
|
|
7
|
+
* Standard Schema or a `safeParse` schema), the handler receives the
|
|
8
|
+
* validated value; without one, input is passed through as-is and the
|
|
9
|
+
* handler must validate it itself.
|
|
10
|
+
*
|
|
11
|
+
* `context.signal` aborts when the operation's deadline elapses or the
|
|
12
|
+
* caller aborts; pass it to anything cancellable.
|
|
10
13
|
*/
|
|
11
14
|
export type APIHandler<TInput = unknown, TOutput = unknown> = (input: TInput, context: APIContext) => Promise<TOutput>;
|
|
12
15
|
//# sourceMappingURL=handler.type.d.ts.map
|
|
@@ -7,13 +7,14 @@ import type { APIOperation } from "../operation/operation.type.js";
|
|
|
7
7
|
export interface APIExecutionContext<TInput = unknown, TOutput = unknown> {
|
|
8
8
|
readonly operation: APIOperation<TInput, TOutput>;
|
|
9
9
|
/**
|
|
10
|
-
* The input the
|
|
10
|
+
* The input as the caller sent it, not yet validated.
|
|
11
11
|
*
|
|
12
12
|
* Writable on purpose: an interceptor may replace it before calling
|
|
13
|
-
* `next()` (to sanitize, scope to a tenant, or apply a default)
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* the
|
|
13
|
+
* `next()` (to sanitize, scope to a tenant, or apply a default). The
|
|
14
|
+
* executor reads this field after the last interceptor calls `next()`,
|
|
15
|
+
* validates it against the operation's `input` schema, and hands the
|
|
16
|
+
* validated value to the handler, so a replacement made by any
|
|
17
|
+
* interceptor in the chain takes effect and cannot bypass the schema.
|
|
17
18
|
*/
|
|
18
19
|
input: TInput;
|
|
19
20
|
readonly context: APIContext;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { APIHandler } from "../handler/handler.type.js";
|
|
2
|
+
import type { APIOperationHttpOptions } from "../bindings/route/apiRoute.type.js";
|
|
3
|
+
import type { APIInputSchema, DefineOperationWithSchemaOptions, InferAPISchemaOutput } from "./operationSchema.type.js";
|
|
2
4
|
/**
|
|
3
5
|
* Metadata for an API operation.
|
|
4
6
|
*/
|
|
@@ -13,6 +15,12 @@ export interface APIOperationMetadata {
|
|
|
13
15
|
*/
|
|
14
16
|
readonly timeout?: number;
|
|
15
17
|
readonly idempotent?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* HTTP binding used by `createApiFetchHandler` and `describeApiRoutes`:
|
|
20
|
+
* method and path template (`"/users/:id"`). Defaults to
|
|
21
|
+
* `POST /<name>`. Validated by {@link defineOperation}.
|
|
22
|
+
*/
|
|
23
|
+
readonly http?: APIOperationHttpOptions;
|
|
16
24
|
}
|
|
17
25
|
/**
|
|
18
26
|
* Core API operation contract.
|
|
@@ -130,9 +138,18 @@ export declare function freezeOperationMetadata(metadata: APIOperationMetadata |
|
|
|
130
138
|
* unusable timeout fails here, at startup, rather than on the first
|
|
131
139
|
* request that reaches the operation.
|
|
132
140
|
*
|
|
141
|
+
* Without type arguments, the handler's `input` is inferred from the
|
|
142
|
+
* `input` schema (a Standard Schema's output type, or a `safeParse`
|
|
143
|
+
* schema's success `data`), also inline in `registry.register(...)` or an
|
|
144
|
+
* operation list. With explicit type arguments
|
|
145
|
+
* (`defineOperation<TInput, TOutput>(...)`) they are used as given. The
|
|
146
|
+
* return type never takes part in inference, so a contextual type such as
|
|
147
|
+
* `AnyAPIOperation` cannot turn the handler's `input` into `never`.
|
|
148
|
+
*
|
|
133
149
|
* @throws {TypeError} if `name` or `handler` has the wrong type, or if
|
|
134
150
|
* `input` / `output` is set but is not a recognised schema.
|
|
135
151
|
* @throws {RangeError} if `name` or a supplied `timeout` is out of range.
|
|
136
152
|
*/
|
|
137
|
-
export declare function defineOperation<
|
|
153
|
+
export declare function defineOperation<TSchema extends APIInputSchema, TOutput = unknown>(options: DefineOperationWithSchemaOptions<TSchema, TOutput>): APIOperation<NoInfer<InferAPISchemaOutput<TSchema>>, NoInfer<TOutput>>;
|
|
154
|
+
export declare function defineOperation<TInput = unknown, TOutput = unknown>(options: DefineOperationOptions<TInput, TOutput>): APIOperation<NoInfer<TInput>, NoInfer<TOutput>>;
|
|
138
155
|
//# sourceMappingURL=operation.type.d.ts.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assertAPISchema } from "../schema/index.js";
|
|
2
|
+
import { assertValidHttpOptions } from "../bindings/route/apiRoute.resolver.js";
|
|
2
3
|
import { DEFAULT_OPERATION_TIMEOUT, MAX_OPERATION_NAME_LENGTH, MAX_OPERATION_TIMEOUT, } from "../constants.js";
|
|
3
4
|
const OPERATION_NAME_PATTERN = /^[A-Za-z0-9._:/-]+$/;
|
|
4
5
|
/**
|
|
@@ -93,19 +94,11 @@ export function freezeOperationMetadata(metadata) {
|
|
|
93
94
|
if (Array.isArray(metadata.tags)) {
|
|
94
95
|
Object.freeze(metadata.tags);
|
|
95
96
|
}
|
|
97
|
+
if (typeof metadata.http === "object" && metadata.http !== null) {
|
|
98
|
+
Object.freeze(metadata.http);
|
|
99
|
+
}
|
|
96
100
|
return Object.freeze(metadata);
|
|
97
101
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Creates a new API operation definition.
|
|
100
|
-
*
|
|
101
|
-
* Validates the definition eagerly — a bad name, a missing handler, or an
|
|
102
|
-
* unusable timeout fails here, at startup, rather than on the first
|
|
103
|
-
* request that reaches the operation.
|
|
104
|
-
*
|
|
105
|
-
* @throws {TypeError} if `name` or `handler` has the wrong type, or if
|
|
106
|
-
* `input` / `output` is set but is not a recognised schema.
|
|
107
|
-
* @throws {RangeError} if `name` or a supplied `timeout` is out of range.
|
|
108
|
-
*/
|
|
109
102
|
export function defineOperation(options) {
|
|
110
103
|
assertValidOperationShape(options);
|
|
111
104
|
if (options.timeout !== undefined) {
|
|
@@ -114,6 +107,7 @@ export function defineOperation(options) {
|
|
|
114
107
|
if (options.metadata?.timeout !== undefined) {
|
|
115
108
|
assertValidTimeout(options.metadata.timeout, "Operation metadata.timeout");
|
|
116
109
|
}
|
|
110
|
+
assertValidHttpOptions(options.metadata?.http, options.name);
|
|
117
111
|
// Explicit field list rather than `...options`: an operation carries
|
|
118
112
|
// exactly the contract fields, never arbitrary extra properties.
|
|
119
113
|
const operation = {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { APIHandler } from "../handler/handler.type.js";
|
|
2
|
+
import type { APIOperationMetadata } from "./operation.type.js";
|
|
3
|
+
/**
|
|
4
|
+
* A schema the executor can validate against, as a type: a Standard
|
|
5
|
+
* Schema (a `"~standard"` object with `validate`, implemented by Zod,
|
|
6
|
+
* Valibot, ArkType, …) or a `safeParse` schema such as `@zudojs/schema`.
|
|
7
|
+
*/
|
|
8
|
+
export type APIInputSchema = {
|
|
9
|
+
readonly "~standard": {
|
|
10
|
+
validate(value: unknown): unknown;
|
|
11
|
+
};
|
|
12
|
+
} | {
|
|
13
|
+
safeParse(value: unknown): unknown;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The value a schema produces on success — what the executor hands the
|
|
17
|
+
* handler after validating the input.
|
|
18
|
+
*
|
|
19
|
+
* Read from a Standard Schema's `"~standard".types.output`, or from the
|
|
20
|
+
* `data` of a `safeParse` success result (`@zudojs/schema`, Zod). A schema
|
|
21
|
+
* that declares neither yields `unknown`.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const TodoInput = schema.object({ title: schema.string() });
|
|
25
|
+
* type T = InferAPISchemaOutput<typeof TodoInput>; // { title: string }
|
|
26
|
+
*/
|
|
27
|
+
export type InferAPISchemaOutput<TSchema> = TSchema extends {
|
|
28
|
+
readonly "~standard": {
|
|
29
|
+
readonly types?: infer TTypes;
|
|
30
|
+
};
|
|
31
|
+
} ? NonNullable<TTypes> extends {
|
|
32
|
+
readonly output: infer TOut;
|
|
33
|
+
} ? TOut : unknown : TSchema extends {
|
|
34
|
+
safeParse(...args: never[]): infer TResult;
|
|
35
|
+
} ? Extract<Awaited<TResult>, {
|
|
36
|
+
readonly success: true;
|
|
37
|
+
}> extends {
|
|
38
|
+
readonly data: infer TData;
|
|
39
|
+
} ? TData : unknown : unknown;
|
|
40
|
+
/**
|
|
41
|
+
* Options for a `defineOperation` call whose input type is inferred from
|
|
42
|
+
* its `input` schema: `handler`'s `input` parameter is the schema's
|
|
43
|
+
* output type, with no type arguments or annotations needed.
|
|
44
|
+
*/
|
|
45
|
+
export interface DefineOperationWithSchemaOptions<TSchema extends APIInputSchema, TOutput = unknown> {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
/** The input schema; the handler's input type is inferred from it. */
|
|
48
|
+
readonly input: TSchema;
|
|
49
|
+
/** Output schema; see `APIOperation.output`. */
|
|
50
|
+
readonly output?: unknown;
|
|
51
|
+
readonly handler: APIHandler<InferAPISchemaOutput<TSchema>, TOutput>;
|
|
52
|
+
readonly metadata?: APIOperationMetadata;
|
|
53
|
+
/** Timeout in milliseconds; see `APIOperation.timeout`. */
|
|
54
|
+
readonly timeout?: number;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=operationSchema.type.d.ts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* Application-facing API layer for the Zudojs framework.
|
|
5
5
|
*
|
|
6
6
|
* Provides transport-agnostic operation definitions, execution context,
|
|
7
|
-
* interceptors, and result types
|
|
7
|
+
* interceptors, and result types — plus bindings that expose the same
|
|
8
|
+
* operations over HTTP (a web-standard fetch handler), RPC, queues and
|
|
9
|
+
* the command line.
|
|
8
10
|
*
|
|
9
11
|
* @example
|
|
10
12
|
* ```ts
|
|
@@ -39,11 +41,12 @@ export { apiSuccess, apiFailure, isApiSuccess, isApiFailure, } from "./api/resul
|
|
|
39
41
|
export type { APIErrorOptions } from "./api/errors/index.js";
|
|
40
42
|
export { APIError, APIValidationError, APIAuthenticationError, APIAuthorizationError, APINotFoundError, APIConflictError, APIRateLimitError, APITimeoutError, APIUnavailableError, APIInternalError, APIVersionError, APIOperationNotFoundError, APIDuplicateOperationError, APIIdempotencyError, createAPIError, isAPIError, ErrorCode, } from "./api/errors/index.js";
|
|
41
43
|
export { DEFAULT_OPERATION_TIMEOUT, MAX_OPERATION_TIMEOUT, MAX_INTERCEPTORS, MAX_VALIDATION_ISSUES, MAX_VALIDATION_ISSUE_LENGTH, MAX_OPERATION_NAME_LENGTH, MAX_REQUEST_ID_LENGTH, } from "./api/constants.js";
|
|
42
|
-
export type { APIContext, APIContextKey } from "./api/context/context.type.js";
|
|
43
|
-
export { createAPIContext, createContextKey, isValidRequestId, normalizeRequestId, RequestIdContextKey, CorrelationIdContextKey, TenantIdContextKey, UserIdContextKey, StartTimeContextKey, } from "./api/context/context.type.js";
|
|
44
|
+
export type { APIContext, APIContextKey, APITransportKind, } from "./api/context/context.type.js";
|
|
45
|
+
export { createAPIContext, createContextKey, isValidRequestId, normalizeRequestId, RequestIdContextKey, CorrelationIdContextKey, TenantIdContextKey, UserIdContextKey, StartTimeContextKey, TransportContextKey, } from "./api/context/context.type.js";
|
|
44
46
|
export type { APIHandler } from "./api/handler/handler.type.js";
|
|
45
47
|
export type { AnyAPIOperation, APIOperation, APIOperationMetadata, DefineOperationOptions, } from "./api/operation/operation.type.js";
|
|
46
48
|
export { defineOperation, resolveOperationTimeout, } from "./api/operation/operation.type.js";
|
|
49
|
+
export type { APIInputSchema, DefineOperationWithSchemaOptions, InferAPISchemaOutput, } from "./api/operation/operationSchema.type.js";
|
|
47
50
|
export type { APISchemaIssue, APISchemaResult } from "./api/schema/index.js";
|
|
48
51
|
export { isAPISchema } from "./api/schema/index.js";
|
|
49
52
|
export { APIOperationRegistry } from "./api/registry/index.js";
|
|
@@ -51,4 +54,5 @@ export type { APIInterceptor, APIExecutionContext, } from "./api/interceptors/in
|
|
|
51
54
|
export { createNoopInterceptor } from "./api/interceptors/interceptor.type.js";
|
|
52
55
|
export type { APIExecutorOptions } from "./api/executor/index.js";
|
|
53
56
|
export { APIExecutor, normalizeAPIError } from "./api/executor/index.js";
|
|
57
|
+
export * from "./api/bindings/index.js";
|
|
54
58
|
//# sourceMappingURL=index.d.ts.map
|