@zudojs/api 1.0.1 → 1.1.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 +27 -3
- package/dist/api/executor/executor.core.d.ts +5 -4
- package/dist/api/executor/executor.core.js +12 -25
- package/dist/api/operation/operation.type.d.ts +17 -11
- package/dist/api/operation/operation.type.js +7 -2
- package/dist/api/registry/operationRegistry.core.d.ts +1 -1
- package/dist/api/registry/operationRegistry.core.js +1 -1
- package/dist/api/schema/apiSchema.adapter.d.ts +56 -0
- package/dist/api/schema/apiSchema.adapter.js +109 -0
- package/dist/api/schema/index.d.ts +9 -0
- package/dist/api/schema/index.js +8 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/package.json +3 -5
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Higher-level API layer — operation definitions, execution context, interceptors, and a transport-agnostic executor. Sits above `@zudojs/http` and `@zudojs/cqrs`.
|
|
4
4
|
|
|
5
|
+
<!-- zudo-docs:start -->
|
|
6
|
+
|
|
7
|
+
**Documentation:** [zudojs.oyinlola.site/docs/packages-api](https://zudojs.oyinlola.site/docs/packages-api) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-api.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
|
|
8
|
+
|
|
9
|
+
<!-- zudo-docs:end -->
|
|
10
|
+
|
|
5
11
|
## When to use
|
|
6
12
|
|
|
7
13
|
Import this when you need:
|
|
@@ -78,7 +84,7 @@ import {
|
|
|
78
84
|
// 1. Define the operation. The handler is positional: (input, context).
|
|
79
85
|
const getUser = defineOperation<{ id: string }, { id: string; name: string }>({
|
|
80
86
|
name: "users.get",
|
|
81
|
-
input: GetUserSchema, // any Standard Schema (Zod, Valibot,
|
|
87
|
+
input: GetUserSchema, // @zudojs/schema, or any Standard Schema (Zod, Valibot, …)
|
|
82
88
|
output: UserSchema, // validated too — see "Output validation"
|
|
83
89
|
timeout: 5_000,
|
|
84
90
|
metadata: { tags: ["Users"] },
|
|
@@ -116,7 +122,25 @@ Results are frozen `{ ok: true, data }` / `{ ok: false, error }` objects — `ex
|
|
|
116
122
|
|
|
117
123
|
## Input validation
|
|
118
124
|
|
|
119
|
-
|
|
125
|
+
The executor accepts two kinds of schema for `input` and `output`, recognised structurally so the package depends on no validation library:
|
|
126
|
+
|
|
127
|
+
- a `@zudojs/schema` schema, or any other schema with a `safeParse` method returning `{ success, data }` / `{ success: false, issues }` (Zod-style `error.issues` also works);
|
|
128
|
+
- a [Standard Schema](https://standardschema.dev) (`"~standard".validate`): Zod, Valibot, ArkType, ….
|
|
129
|
+
|
|
130
|
+
Anything else fails closed. `defineOperation` and `APIOperationRegistry.register` throw a `TypeError` when `input` or `output` is set to a value that is neither, and the executor answers a hand-rolled operation carrying one with an `APIInternalError` (500) without running the handler. A schema is never silently skipped. `isAPISchema(value)` reports whether a value would be accepted.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { schema } from "@zudojs/schema";
|
|
134
|
+
|
|
135
|
+
const charge = defineOperation({
|
|
136
|
+
name: "payments.charge",
|
|
137
|
+
input: schema.object({ id: schema.string().uuid(), amount: schema.number().min(1) }),
|
|
138
|
+
output: schema.object({ ok: schema.boolean() }), // strips unknown keys
|
|
139
|
+
handler: async (input) => ({ ok: true }),
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The executor validates the input before the handler runs and passes the schema's *transformed* value to the handler. Failures return an `APIValidationError` (422).
|
|
120
144
|
|
|
121
145
|
Schema issue messages routinely interpolate the value that failed, so by default the executor does **not** copy them into the client-facing error: each issue becomes `"<path>: invalid"` (e.g. `"user.email: invalid"`), naming where validation failed without echoing what was submitted. The list is capped at `MAX_VALIDATION_ISSUES` entries with a trailing `"… and N more issue(s) omitted."` marker.
|
|
122
146
|
|
|
@@ -128,7 +152,7 @@ new APIExecutor({ exposeValidationMessages: true, maxValidationIssues: 10 });
|
|
|
128
152
|
|
|
129
153
|
## Output validation
|
|
130
154
|
|
|
131
|
-
When `operation.output` is
|
|
155
|
+
When `operation.output` is set, the handler's return value is validated too, and the validated (possibly stripped or transformed) value becomes `result.data`. A mismatch is a server bug, so it fails with an `APIInternalError` (500, `expose: false`) naming only the failing paths.
|
|
132
156
|
|
|
133
157
|
## Timeouts
|
|
134
158
|
|
|
@@ -53,8 +53,10 @@ export interface APIExecutorOptions {
|
|
|
53
53
|
* Executes an API operation through its interceptor pipeline.
|
|
54
54
|
*
|
|
55
55
|
* Enforces the operation timeout, honors the context AbortSignal, and
|
|
56
|
-
* validates input and output
|
|
57
|
-
* Standard Schema
|
|
56
|
+
* validates input and output against the operation's `input` / `output`
|
|
57
|
+
* schema (a Standard Schema or a `safeParse` schema such as
|
|
58
|
+
* `@zudojs/schema`). A declared schema of any other kind fails closed
|
|
59
|
+
* with an `APIInternalError`; it is never skipped.
|
|
58
60
|
*/
|
|
59
61
|
export declare class APIExecutor {
|
|
60
62
|
private readonly interceptors;
|
|
@@ -67,8 +69,7 @@ export declare class APIExecutor {
|
|
|
67
69
|
execute<TInput = unknown, TOutput = unknown>(operation: APIOperation<TInput, TOutput>, input: TInput, context: APIContext): Promise<APIResult<TOutput>>;
|
|
68
70
|
/**
|
|
69
71
|
* Invokes the operation handler under its timeout and abort signal, and
|
|
70
|
-
* validates the handler's output
|
|
71
|
-
* Schema.
|
|
72
|
+
* validates the handler's output against `operation.output`.
|
|
72
73
|
*/
|
|
73
74
|
private invokeHandler;
|
|
74
75
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { apiFailure, apiSuccess } from "../result/apiResult.type.js";
|
|
2
2
|
import { resolveOperationTimeout } from "../operation/operation.type.js";
|
|
3
3
|
import { APIError, APIInternalError, APITimeoutError, APIValidationError, createAPIError, ErrorCode, isAPIError, } from "../errors/index.js";
|
|
4
|
+
import { validateWithSchema } from "../schema/index.js";
|
|
4
5
|
import { MAX_INTERCEPTORS, MAX_VALIDATION_ISSUES, MAX_VALIDATION_ISSUE_LENGTH, } from "../constants.js";
|
|
5
6
|
/**
|
|
6
7
|
* Error normalizer for converting unknown errors into APIError instances.
|
|
@@ -45,27 +46,14 @@ function describeValueType(value) {
|
|
|
45
46
|
}
|
|
46
47
|
return typeof value;
|
|
47
48
|
}
|
|
48
|
-
function isStandardSchema(value) {
|
|
49
|
-
return (typeof value === "object" &&
|
|
50
|
-
value !== null &&
|
|
51
|
-
typeof value["~standard"]
|
|
52
|
-
?.validate === "function");
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* A schema result counts as a failure only when it carries at least one
|
|
56
|
-
* issue. Some adapters always populate `issues` and return an empty array
|
|
57
|
-
* on success; treating that as a failure produces a 422 with an empty
|
|
58
|
-
* issue list and no way to learn what was wrong.
|
|
59
|
-
*/
|
|
60
|
-
function hasIssues(result) {
|
|
61
|
-
return Array.isArray(result.issues) && result.issues.length > 0;
|
|
62
|
-
}
|
|
63
49
|
/**
|
|
64
50
|
* Executes an API operation through its interceptor pipeline.
|
|
65
51
|
*
|
|
66
52
|
* Enforces the operation timeout, honors the context AbortSignal, and
|
|
67
|
-
* validates input and output
|
|
68
|
-
* Standard Schema
|
|
53
|
+
* validates input and output against the operation's `input` / `output`
|
|
54
|
+
* schema (a Standard Schema or a `safeParse` schema such as
|
|
55
|
+
* `@zudojs/schema`). A declared schema of any other kind fails closed
|
|
56
|
+
* with an `APIInternalError`; it is never skipped.
|
|
69
57
|
*/
|
|
70
58
|
export class APIExecutor {
|
|
71
59
|
interceptors;
|
|
@@ -97,10 +85,10 @@ export class APIExecutor {
|
|
|
97
85
|
return apiFailure(abortedError(operation.name));
|
|
98
86
|
}
|
|
99
87
|
let effectiveInput = input;
|
|
100
|
-
if (
|
|
88
|
+
if (operation.input !== undefined) {
|
|
101
89
|
try {
|
|
102
|
-
const validation = await operation.input
|
|
103
|
-
if (
|
|
90
|
+
const validation = await validateWithSchema(operation.input, input);
|
|
91
|
+
if (!validation.ok) {
|
|
104
92
|
return apiFailure(new APIValidationError(`Invalid input for operation "${operation.name}".`, this.clientIssues(validation.issues)));
|
|
105
93
|
}
|
|
106
94
|
effectiveInput = validation.value;
|
|
@@ -127,8 +115,7 @@ export class APIExecutor {
|
|
|
127
115
|
}
|
|
128
116
|
/**
|
|
129
117
|
* Invokes the operation handler under its timeout and abort signal, and
|
|
130
|
-
* validates the handler's output
|
|
131
|
-
* Schema.
|
|
118
|
+
* validates the handler's output against `operation.output`.
|
|
132
119
|
*/
|
|
133
120
|
async invokeHandler(operation, input, context) {
|
|
134
121
|
if (context.signal?.aborted) {
|
|
@@ -154,17 +141,17 @@ export class APIExecutor {
|
|
|
154
141
|
* internal audit columns) that should not reach a client.
|
|
155
142
|
*/
|
|
156
143
|
async validateOutput(operation, output) {
|
|
157
|
-
if (
|
|
144
|
+
if (operation.output === undefined) {
|
|
158
145
|
return apiSuccess(output);
|
|
159
146
|
}
|
|
160
147
|
let validation;
|
|
161
148
|
try {
|
|
162
|
-
validation = await operation.output
|
|
149
|
+
validation = await validateWithSchema(operation.output, output);
|
|
163
150
|
}
|
|
164
151
|
catch (error) {
|
|
165
152
|
return apiFailure(normalizeAPIError(error, operation.name));
|
|
166
153
|
}
|
|
167
|
-
if (
|
|
154
|
+
if (!validation.ok) {
|
|
168
155
|
const paths = validation.issues
|
|
169
156
|
.slice(0, this.maxValidationIssues)
|
|
170
157
|
.map(formatIssuePath)
|
|
@@ -23,19 +23,21 @@ export interface APIOperationMetadata {
|
|
|
23
23
|
export interface APIOperation<TInput = unknown, TOutput = unknown> {
|
|
24
24
|
readonly name: string;
|
|
25
25
|
/**
|
|
26
|
-
* Input schema
|
|
27
|
-
*
|
|
28
|
-
* before invoking the handler
|
|
26
|
+
* Input schema: a Standard Schema (https://standardschema.dev) or a
|
|
27
|
+
* `safeParse` schema such as `@zudojs/schema`. The executor validates
|
|
28
|
+
* input against it before invoking the handler. Any other non-`undefined`
|
|
29
|
+
* value is rejected by {@link defineOperation} and the registry, and
|
|
30
|
+
* fails closed in the executor.
|
|
29
31
|
*/
|
|
30
32
|
readonly input?: unknown;
|
|
31
33
|
/**
|
|
32
|
-
* Output schema
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* Output schema: a Standard Schema or a `safeParse` schema such as
|
|
35
|
+
* `@zudojs/schema`. The executor validates the handler's return value
|
|
36
|
+
* against it and returns the validated (possibly transformed or
|
|
37
|
+
* stripped) value as the result data; a handler returning the wrong
|
|
36
38
|
* shape fails with an `APIInternalError` (`expose: false`) rather than
|
|
37
|
-
* leaking through to the transport.
|
|
38
|
-
*
|
|
39
|
+
* leaking through to the transport. Any other non-`undefined` value is
|
|
40
|
+
* rejected, exactly like {@link APIOperation.input}.
|
|
39
41
|
*/
|
|
40
42
|
readonly output?: unknown;
|
|
41
43
|
readonly handler: APIHandler<TInput, TOutput>;
|
|
@@ -105,13 +107,16 @@ export declare function resolveOperationTimeout(source: {
|
|
|
105
107
|
* `APIOperationRegistry.register`, since `APIOperation` is a bare
|
|
106
108
|
* interface that callers can satisfy without `defineOperation`.
|
|
107
109
|
*
|
|
108
|
-
* @throws {TypeError} if `name` or `handler` has the wrong type
|
|
110
|
+
* @throws {TypeError} if `name` or `handler` has the wrong type, or if
|
|
111
|
+
* `input` / `output` is set but is not a recognised schema.
|
|
109
112
|
* @throws {RangeError} if `name` is empty, over-long, or contains
|
|
110
113
|
* characters outside `[A-Za-z0-9._:/-]`.
|
|
111
114
|
*/
|
|
112
115
|
export declare function assertValidOperationShape(operation: {
|
|
113
116
|
readonly name?: unknown;
|
|
114
117
|
readonly handler?: unknown;
|
|
118
|
+
readonly input?: unknown;
|
|
119
|
+
readonly output?: unknown;
|
|
115
120
|
}): void;
|
|
116
121
|
/**
|
|
117
122
|
* Deeply freezes an operation's metadata so a registered operation cannot
|
|
@@ -125,7 +130,8 @@ export declare function freezeOperationMetadata(metadata: APIOperationMetadata |
|
|
|
125
130
|
* unusable timeout fails here, at startup, rather than on the first
|
|
126
131
|
* request that reaches the operation.
|
|
127
132
|
*
|
|
128
|
-
* @throws {TypeError} if `name` or `handler` has the wrong type
|
|
133
|
+
* @throws {TypeError} if `name` or `handler` has the wrong type, or if
|
|
134
|
+
* `input` / `output` is set but is not a recognised schema.
|
|
129
135
|
* @throws {RangeError} if `name` or a supplied `timeout` is out of range.
|
|
130
136
|
*/
|
|
131
137
|
export declare function defineOperation<TInput = unknown, TOutput = unknown>(options: DefineOperationOptions<TInput, TOutput>): APIOperation<TInput, TOutput>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertAPISchema } from "../schema/index.js";
|
|
1
2
|
import { DEFAULT_OPERATION_TIMEOUT, MAX_OPERATION_NAME_LENGTH, MAX_OPERATION_TIMEOUT, } from "../constants.js";
|
|
2
3
|
const OPERATION_NAME_PATTERN = /^[A-Za-z0-9._:/-]+$/;
|
|
3
4
|
/**
|
|
@@ -56,7 +57,8 @@ export function resolveOperationTimeout(source) {
|
|
|
56
57
|
* `APIOperationRegistry.register`, since `APIOperation` is a bare
|
|
57
58
|
* interface that callers can satisfy without `defineOperation`.
|
|
58
59
|
*
|
|
59
|
-
* @throws {TypeError} if `name` or `handler` has the wrong type
|
|
60
|
+
* @throws {TypeError} if `name` or `handler` has the wrong type, or if
|
|
61
|
+
* `input` / `output` is set but is not a recognised schema.
|
|
60
62
|
* @throws {RangeError} if `name` is empty, over-long, or contains
|
|
61
63
|
* characters outside `[A-Za-z0-9._:/-]`.
|
|
62
64
|
*/
|
|
@@ -77,6 +79,8 @@ export function assertValidOperationShape(operation) {
|
|
|
77
79
|
if (typeof handler !== "function") {
|
|
78
80
|
throw new TypeError(`Operation "${name}" must have a handler function, received ${typeof handler}.`);
|
|
79
81
|
}
|
|
82
|
+
assertAPISchema(operation.input, "input", name);
|
|
83
|
+
assertAPISchema(operation.output, "output", name);
|
|
80
84
|
}
|
|
81
85
|
/**
|
|
82
86
|
* Deeply freezes an operation's metadata so a registered operation cannot
|
|
@@ -98,7 +102,8 @@ export function freezeOperationMetadata(metadata) {
|
|
|
98
102
|
* unusable timeout fails here, at startup, rather than on the first
|
|
99
103
|
* request that reaches the operation.
|
|
100
104
|
*
|
|
101
|
-
* @throws {TypeError} if `name` or `handler` has the wrong type
|
|
105
|
+
* @throws {TypeError} if `name` or `handler` has the wrong type, or if
|
|
106
|
+
* `input` / `output` is set but is not a recognised schema.
|
|
102
107
|
* @throws {RangeError} if `name` or a supplied `timeout` is out of range.
|
|
103
108
|
*/
|
|
104
109
|
export function defineOperation(options) {
|
|
@@ -19,7 +19,7 @@ export declare class APIOperationRegistry {
|
|
|
19
19
|
*
|
|
20
20
|
* @throws {APIDuplicateOperationError} if an operation with the same name is already registered.
|
|
21
21
|
* @throws {APIError} if the registry is frozen.
|
|
22
|
-
* @throws {TypeError | RangeError} if the operation's name or
|
|
22
|
+
* @throws {TypeError | RangeError} if the operation's name, handler, or input/output schema is invalid.
|
|
23
23
|
*/
|
|
24
24
|
register(operation: AnyAPIOperation): void;
|
|
25
25
|
/**
|
|
@@ -20,7 +20,7 @@ export class APIOperationRegistry {
|
|
|
20
20
|
*
|
|
21
21
|
* @throws {APIDuplicateOperationError} if an operation with the same name is already registered.
|
|
22
22
|
* @throws {APIError} if the registry is frozen.
|
|
23
|
-
* @throws {TypeError | RangeError} if the operation's name or
|
|
23
|
+
* @throws {TypeError | RangeError} if the operation's name, handler, or input/output schema is invalid.
|
|
24
24
|
*/
|
|
25
25
|
register(operation) {
|
|
26
26
|
if (this.frozen) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema recognition and validation for operation `input` / `output`.
|
|
3
|
+
*
|
|
4
|
+
* Two contracts are recognised structurally, so the package depends on no
|
|
5
|
+
* particular validation library:
|
|
6
|
+
*
|
|
7
|
+
* 1. Standard Schema (https://standardschema.dev): a `"~standard"` object
|
|
8
|
+
* with a `validate` function (Zod, Valibot, ArkType, …).
|
|
9
|
+
* 2. A `safeParse` schema returning `{ success, data }` /
|
|
10
|
+
* `{ success: false, issues }` (`@zudojs/schema`) or
|
|
11
|
+
* `{ success: false, error: { issues } }` (Zod-style).
|
|
12
|
+
*
|
|
13
|
+
* Anything else is *unrecognised*. The executor fails closed on an
|
|
14
|
+
* unrecognised schema rather than treating it as documentation, because a
|
|
15
|
+
* schema that is silently skipped validates nothing.
|
|
16
|
+
*/
|
|
17
|
+
type IssuePathSegment = PropertyKey | {
|
|
18
|
+
readonly key: PropertyKey;
|
|
19
|
+
};
|
|
20
|
+
/** A validation issue normalised from either supported contract. */
|
|
21
|
+
export interface APISchemaIssue {
|
|
22
|
+
readonly message: string;
|
|
23
|
+
readonly path?: ReadonlyArray<IssuePathSegment>;
|
|
24
|
+
}
|
|
25
|
+
/** Normalised validation outcome. */
|
|
26
|
+
export type APISchemaResult = {
|
|
27
|
+
readonly ok: true;
|
|
28
|
+
readonly value: unknown;
|
|
29
|
+
} | {
|
|
30
|
+
readonly ok: false;
|
|
31
|
+
readonly issues: ReadonlyArray<APISchemaIssue>;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Whether `value` is a schema the executor can validate against.
|
|
35
|
+
*
|
|
36
|
+
* `undefined` (no schema declared) is not a schema; callers treat it as
|
|
37
|
+
* "nothing to validate" and must reject every other unrecognised value.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isAPISchema(value: unknown): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Throws when `schema` is declared but is not a recognised schema.
|
|
42
|
+
*
|
|
43
|
+
* @throws {TypeError} naming the operation and the offending field.
|
|
44
|
+
*/
|
|
45
|
+
export declare function assertAPISchema(schema: unknown, field: "input" | "output", operationName: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Validates `value` against a recognised schema.
|
|
48
|
+
*
|
|
49
|
+
* Fails closed: an unrecognised schema, or a result in an unexpected
|
|
50
|
+
* shape, throws rather than letting the value through.
|
|
51
|
+
*
|
|
52
|
+
* @throws {TypeError} for an unrecognised schema or malformed result.
|
|
53
|
+
*/
|
|
54
|
+
export declare function validateWithSchema(schema: unknown, value: unknown): Promise<APISchemaResult>;
|
|
55
|
+
export {};
|
|
56
|
+
//# sourceMappingURL=apiSchema.adapter.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema recognition and validation for operation `input` / `output`.
|
|
3
|
+
*
|
|
4
|
+
* Two contracts are recognised structurally, so the package depends on no
|
|
5
|
+
* particular validation library:
|
|
6
|
+
*
|
|
7
|
+
* 1. Standard Schema (https://standardschema.dev): a `"~standard"` object
|
|
8
|
+
* with a `validate` function (Zod, Valibot, ArkType, …).
|
|
9
|
+
* 2. A `safeParse` schema returning `{ success, data }` /
|
|
10
|
+
* `{ success: false, issues }` (`@zudojs/schema`) or
|
|
11
|
+
* `{ success: false, error: { issues } }` (Zod-style).
|
|
12
|
+
*
|
|
13
|
+
* Anything else is *unrecognised*. The executor fails closed on an
|
|
14
|
+
* unrecognised schema rather than treating it as documentation, because a
|
|
15
|
+
* schema that is silently skipped validates nothing.
|
|
16
|
+
*/
|
|
17
|
+
function isStandardSchema(value) {
|
|
18
|
+
if (typeof value !== "object" && typeof value !== "function")
|
|
19
|
+
return false;
|
|
20
|
+
if (value === null)
|
|
21
|
+
return false;
|
|
22
|
+
const standard = value["~standard"];
|
|
23
|
+
return typeof standard?.validate === "function";
|
|
24
|
+
}
|
|
25
|
+
function isSafeParseSchema(value) {
|
|
26
|
+
if (typeof value !== "object" && typeof value !== "function")
|
|
27
|
+
return false;
|
|
28
|
+
if (value === null)
|
|
29
|
+
return false;
|
|
30
|
+
return typeof value.safeParse === "function";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether `value` is a schema the executor can validate against.
|
|
34
|
+
*
|
|
35
|
+
* `undefined` (no schema declared) is not a schema; callers treat it as
|
|
36
|
+
* "nothing to validate" and must reject every other unrecognised value.
|
|
37
|
+
*/
|
|
38
|
+
export function isAPISchema(value) {
|
|
39
|
+
return isStandardSchema(value) || isSafeParseSchema(value);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Throws when `schema` is declared but is not a recognised schema.
|
|
43
|
+
*
|
|
44
|
+
* @throws {TypeError} naming the operation and the offending field.
|
|
45
|
+
*/
|
|
46
|
+
export function assertAPISchema(schema, field, operationName) {
|
|
47
|
+
if (schema === undefined || isAPISchema(schema))
|
|
48
|
+
return;
|
|
49
|
+
throw new TypeError(`Operation "${operationName}" declares an ${field} schema that is neither a Standard Schema ("~standard".validate) nor a safeParse schema; refusing to run it unvalidated.`);
|
|
50
|
+
}
|
|
51
|
+
const MALFORMED = "Schema returned a result in an unrecognised shape.";
|
|
52
|
+
function isRecord(value) {
|
|
53
|
+
return typeof value === "object" && value !== null;
|
|
54
|
+
}
|
|
55
|
+
function toIssues(raw) {
|
|
56
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
57
|
+
return [{ message: "Validation failed without a recorded issue." }];
|
|
58
|
+
}
|
|
59
|
+
return raw.map((issue) => {
|
|
60
|
+
const record = isRecord(issue) ? issue : {};
|
|
61
|
+
const path = Array.isArray(record["path"])
|
|
62
|
+
? record["path"]
|
|
63
|
+
: undefined;
|
|
64
|
+
const message = typeof record["message"] === "string" ? record["message"] : "invalid";
|
|
65
|
+
return path === undefined ? { message } : { message, path };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function fromStandard(result) {
|
|
69
|
+
if (!isRecord(result))
|
|
70
|
+
throw new TypeError(MALFORMED);
|
|
71
|
+
const issues = result["issues"];
|
|
72
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
73
|
+
return { ok: false, issues: toIssues(issues) };
|
|
74
|
+
}
|
|
75
|
+
if (!("value" in result))
|
|
76
|
+
throw new TypeError(MALFORMED);
|
|
77
|
+
return { ok: true, value: result["value"] };
|
|
78
|
+
}
|
|
79
|
+
function fromSafeParse(result) {
|
|
80
|
+
if (!isRecord(result))
|
|
81
|
+
throw new TypeError(MALFORMED);
|
|
82
|
+
if (result["success"] === true) {
|
|
83
|
+
return { ok: true, value: result["data"] };
|
|
84
|
+
}
|
|
85
|
+
if (result["success"] === false) {
|
|
86
|
+
const error = result["error"];
|
|
87
|
+
const issues = result["issues"] ?? (isRecord(error) ? error["issues"] : undefined);
|
|
88
|
+
return { ok: false, issues: toIssues(issues) };
|
|
89
|
+
}
|
|
90
|
+
throw new TypeError(MALFORMED);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Validates `value` against a recognised schema.
|
|
94
|
+
*
|
|
95
|
+
* Fails closed: an unrecognised schema, or a result in an unexpected
|
|
96
|
+
* shape, throws rather than letting the value through.
|
|
97
|
+
*
|
|
98
|
+
* @throws {TypeError} for an unrecognised schema or malformed result.
|
|
99
|
+
*/
|
|
100
|
+
export async function validateWithSchema(schema, value) {
|
|
101
|
+
if (isStandardSchema(schema)) {
|
|
102
|
+
return fromStandard(await schema["~standard"].validate(value));
|
|
103
|
+
}
|
|
104
|
+
if (isSafeParseSchema(schema)) {
|
|
105
|
+
return fromSafeParse(await schema.safeParse(value));
|
|
106
|
+
}
|
|
107
|
+
throw new TypeError("Unrecognised schema: refusing to validate.");
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=apiSchema.adapter.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/api/schema
|
|
3
|
+
*
|
|
4
|
+
* Structural recognition of operation schemas (Standard Schema and
|
|
5
|
+
* `safeParse` schemas such as `@zudojs/schema`) and fail-closed validation.
|
|
6
|
+
*/
|
|
7
|
+
export { assertAPISchema, isAPISchema, validateWithSchema, } from "./apiSchema.adapter.js";
|
|
8
|
+
export type { APISchemaIssue, APISchemaResult } from "./apiSchema.adapter.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/api/schema
|
|
3
|
+
*
|
|
4
|
+
* Structural recognition of operation schemas (Standard Schema and
|
|
5
|
+
* `safeParse` schemas such as `@zudojs/schema`) and fail-closed validation.
|
|
6
|
+
*/
|
|
7
|
+
export { assertAPISchema, isAPISchema, validateWithSchema, } from "./apiSchema.adapter.js";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* const getUser = defineOperation({
|
|
19
19
|
* name: "users.get",
|
|
20
|
-
* input: GetUserSchema, // any Standard Schema (Zod, Valibot,
|
|
20
|
+
* input: GetUserSchema, // @zudojs/schema, or any Standard Schema (Zod, Valibot, …)
|
|
21
21
|
* output: UserSchema,
|
|
22
22
|
* handler: async (input, context) => userService.findById(input.id),
|
|
23
23
|
* });
|
|
@@ -44,6 +44,8 @@ export { createAPIContext, createContextKey, isValidRequestId, normalizeRequestI
|
|
|
44
44
|
export type { APIHandler } from "./api/handler/handler.type.js";
|
|
45
45
|
export type { AnyAPIOperation, APIOperation, APIOperationMetadata, DefineOperationOptions, } from "./api/operation/operation.type.js";
|
|
46
46
|
export { defineOperation, resolveOperationTimeout, } from "./api/operation/operation.type.js";
|
|
47
|
+
export type { APISchemaIssue, APISchemaResult } from "./api/schema/index.js";
|
|
48
|
+
export { isAPISchema } from "./api/schema/index.js";
|
|
47
49
|
export { APIOperationRegistry } from "./api/registry/index.js";
|
|
48
50
|
export type { APIInterceptor, APIExecutionContext, } from "./api/interceptors/interceptor.type.js";
|
|
49
51
|
export { createNoopInterceptor } from "./api/interceptors/interceptor.type.js";
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* const getUser = defineOperation({
|
|
19
19
|
* name: "users.get",
|
|
20
|
-
* input: GetUserSchema, // any Standard Schema (Zod, Valibot,
|
|
20
|
+
* input: GetUserSchema, // @zudojs/schema, or any Standard Schema (Zod, Valibot, …)
|
|
21
21
|
* output: UserSchema,
|
|
22
22
|
* handler: async (input, context) => userService.findById(input.id),
|
|
23
23
|
* });
|
|
@@ -40,6 +40,7 @@ export { APIError, APIValidationError, APIAuthenticationError, APIAuthorizationE
|
|
|
40
40
|
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";
|
|
41
41
|
export { createAPIContext, createContextKey, isValidRequestId, normalizeRequestId, RequestIdContextKey, CorrelationIdContextKey, TenantIdContextKey, UserIdContextKey, StartTimeContextKey, } from "./api/context/context.type.js";
|
|
42
42
|
export { defineOperation, resolveOperationTimeout, } from "./api/operation/operation.type.js";
|
|
43
|
+
export { isAPISchema } from "./api/schema/index.js";
|
|
43
44
|
// Registry
|
|
44
45
|
export { APIOperationRegistry } from "./api/registry/index.js";
|
|
45
46
|
export { createNoopInterceptor } from "./api/interceptors/interceptor.type.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/api",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Application-facing API layer for Zudojs — operation definitions, execution context, interceptors, and transport-agnostic contracts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -27,12 +27,10 @@
|
|
|
27
27
|
"node": ">=24.0.0"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@zudojs/errors": "1.0
|
|
31
|
-
"@zudojs/constants": "1.0.1",
|
|
32
|
-
"@zudojs/types": "1.0.0",
|
|
33
|
-
"@zudojs/schema": "1.0.1"
|
|
30
|
+
"@zudojs/errors": "1.1.0"
|
|
34
31
|
},
|
|
35
32
|
"devDependencies": {
|
|
33
|
+
"@zudojs/schema": "1.1.0",
|
|
36
34
|
"typescript": "7.0.2",
|
|
37
35
|
"vitest": "^4.1.11"
|
|
38
36
|
},
|