@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.
Files changed (78) hide show
  1. package/README.md +162 -6
  2. package/dist/api/bindings/cli/apiCli.argv.d.ts +36 -0
  3. package/dist/api/bindings/cli/apiCli.argv.js +78 -0
  4. package/dist/api/bindings/cli/apiCli.binding.d.ts +45 -0
  5. package/dist/api/bindings/cli/apiCli.binding.js +77 -0
  6. package/dist/api/bindings/cli/apiCli.exitCode.d.ts +31 -0
  7. package/dist/api/bindings/cli/apiCli.exitCode.js +43 -0
  8. package/dist/api/bindings/cli/apiCli.values.d.ts +24 -0
  9. package/dist/api/bindings/cli/apiCli.values.js +71 -0
  10. package/dist/api/bindings/cli/index.d.ts +12 -0
  11. package/dist/api/bindings/cli/index.js +9 -0
  12. package/dist/api/bindings/fetch/apiFetch.handler.d.ts +35 -0
  13. package/dist/api/bindings/fetch/apiFetch.handler.js +99 -0
  14. package/dist/api/bindings/fetch/apiFetch.input.d.ts +29 -0
  15. package/dist/api/bindings/fetch/apiFetch.input.js +74 -0
  16. package/dist/api/bindings/fetch/index.d.ts +11 -0
  17. package/dist/api/bindings/fetch/index.js +9 -0
  18. package/dist/api/bindings/index.d.ts +25 -0
  19. package/dist/api/bindings/index.js +18 -0
  20. package/dist/api/bindings/openapi/apiOpenAPI.descriptor.d.ts +37 -0
  21. package/dist/api/bindings/openapi/apiOpenAPI.descriptor.js +67 -0
  22. package/dist/api/bindings/openapi/apiOpenAPI.schemas.d.ts +32 -0
  23. package/dist/api/bindings/openapi/apiOpenAPI.schemas.js +62 -0
  24. package/dist/api/bindings/openapi/index.d.ts +9 -0
  25. package/dist/api/bindings/openapi/index.js +8 -0
  26. package/dist/api/bindings/queue/apiQueue.binding.d.ts +39 -0
  27. package/dist/api/bindings/queue/apiQueue.binding.js +65 -0
  28. package/dist/api/bindings/queue/index.d.ts +7 -0
  29. package/dist/api/bindings/queue/index.js +6 -0
  30. package/dist/api/bindings/route/apiRoute.resolver.d.ts +33 -0
  31. package/dist/api/bindings/route/apiRoute.resolver.js +109 -0
  32. package/dist/api/bindings/route/apiRoute.table.d.ts +53 -0
  33. package/dist/api/bindings/route/apiRoute.table.js +95 -0
  34. package/dist/api/bindings/route/apiRoute.type.d.ts +65 -0
  35. package/dist/api/bindings/route/apiRoute.type.js +8 -0
  36. package/dist/api/bindings/route/index.d.ts +11 -0
  37. package/dist/api/bindings/route/index.js +9 -0
  38. package/dist/api/bindings/rpc/apiRpc.binding.d.ts +46 -0
  39. package/dist/api/bindings/rpc/apiRpc.binding.js +63 -0
  40. package/dist/api/bindings/rpc/apiRpc.errors.d.ts +20 -0
  41. package/dist/api/bindings/rpc/apiRpc.errors.js +64 -0
  42. package/dist/api/bindings/rpc/index.d.ts +9 -0
  43. package/dist/api/bindings/rpc/index.js +8 -0
  44. package/dist/api/bindings/shared/apiBinding.helper.d.ts +44 -0
  45. package/dist/api/bindings/shared/apiBinding.helper.js +66 -0
  46. package/dist/api/bindings/shared/apiBinding.type.d.ts +37 -0
  47. package/dist/api/bindings/shared/apiBinding.type.js +2 -0
  48. package/dist/api/bindings/shared/apiWireResult.helper.d.ts +47 -0
  49. package/dist/api/bindings/shared/apiWireResult.helper.js +38 -0
  50. package/dist/api/bindings/shared/index.d.ts +11 -0
  51. package/dist/api/bindings/shared/index.js +8 -0
  52. package/dist/api/context/context.type.d.ts +4 -4
  53. package/dist/api/context/context.type.js +2 -2
  54. package/dist/api/context/contextKey.type.d.ts +8 -0
  55. package/dist/api/context/contextKey.type.js +2 -0
  56. package/dist/api/executor/executor.core.d.ts +20 -63
  57. package/dist/api/executor/executor.core.js +36 -193
  58. package/dist/api/executor/executor.deadline.d.ts +40 -0
  59. package/dist/api/executor/executor.deadline.js +93 -0
  60. package/dist/api/executor/executor.issues.d.ts +19 -0
  61. package/dist/api/executor/executor.issues.js +46 -0
  62. package/dist/api/executor/executor.normalize.d.ts +16 -0
  63. package/dist/api/executor/executor.normalize.js +45 -0
  64. package/dist/api/executor/executor.type.d.ts +33 -0
  65. package/dist/api/executor/executor.type.js +2 -0
  66. package/dist/api/executor/executor.validation.d.ts +31 -0
  67. package/dist/api/executor/executor.validation.js +58 -0
  68. package/dist/api/executor/index.d.ts +9 -2
  69. package/dist/api/executor/index.js +8 -1
  70. package/dist/api/handler/handler.type.d.ts +7 -4
  71. package/dist/api/interceptors/interceptor.type.d.ts +6 -5
  72. package/dist/api/operation/operation.type.d.ts +18 -1
  73. package/dist/api/operation/operation.type.js +5 -11
  74. package/dist/api/operation/operationSchema.type.d.ts +56 -0
  75. package/dist/api/operation/operationSchema.type.js +2 -0
  76. package/dist/index.d.ts +7 -3
  77. package/dist/index.js +7 -2
  78. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=executor.type.js.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.core.js";
3
- export type { APIExecutorOptions } from "./executor.core.js";
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.core.js";
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. Input is validated by the executor only when the
7
- * operation's `input` is a Standard Schema (https://standardschema.dev,
8
- * implemented by Zod, Valibot, ArkType, …); otherwise it is passed
9
- * through as-is and the handler must validate it itself.
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 handler will receive.
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) and the
14
- * handler receives the replacement. The executor reads this field at
15
- * handler-invocation time, so a replacement made by any interceptor in
16
- * the chain takes effect.
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<TInput = unknown, TOutput = unknown>(options: DefineOperationOptions<TInput, TOutput>): APIOperation<TInput, TOutput>;
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
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=operationSchema.type.js.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