@ultimat3/schema 1.0.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/LICENSE +21 -0
- package/README.md +100 -0
- package/package.json +33 -0
- package/src/builder.d.ts +62 -0
- package/src/builder.d.ts.map +1 -0
- package/src/builder.js +93 -0
- package/src/builder.js.map +1 -0
- package/src/builder.ts +173 -0
- package/src/coerce.d.ts +15 -0
- package/src/coerce.d.ts.map +1 -0
- package/src/coerce.js +123 -0
- package/src/coerce.js.map +1 -0
- package/src/coerce.ts +125 -0
- package/src/errors.d.ts +62 -0
- package/src/errors.d.ts.map +1 -0
- package/src/errors.js +85 -0
- package/src/errors.js.map +1 -0
- package/src/errors.ts +140 -0
- package/src/index.d.ts +19 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +11 -0
- package/src/index.js.map +1 -0
- package/src/index.ts +102 -0
- package/src/json-schema.d.ts +37 -0
- package/src/json-schema.d.ts.map +1 -0
- package/src/json-schema.js +108 -0
- package/src/json-schema.js.map +1 -0
- package/src/json-schema.ts +149 -0
- package/src/node.d.ts +32 -0
- package/src/node.d.ts.map +1 -0
- package/src/node.js +24 -0
- package/src/node.js.map +1 -0
- package/src/node.ts +75 -0
- package/src/provider.d.ts +32 -0
- package/src/provider.d.ts.map +1 -0
- package/src/provider.js +49 -0
- package/src/provider.js.map +1 -0
- package/src/provider.ts +67 -0
- package/src/standard.d.ts +44 -0
- package/src/standard.d.ts.map +1 -0
- package/src/standard.js +71 -0
- package/src/standard.js.map +1 -0
- package/src/standard.ts +160 -0
- package/src/t.d.ts +6 -0
- package/src/t.d.ts.map +1 -0
- package/src/t.js +79 -0
- package/src/t.js.map +1 -0
- package/src/t.ts +101 -0
- package/src/validators.d.ts +63 -0
- package/src/validators.d.ts.map +1 -0
- package/src/validators.js +271 -0
- package/src/validators.js.map +1 -0
- package/src/validators.ts +389 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 developerz.ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# ✅ @ultimat3/schema
|
|
2
|
+
|
|
3
|
+
Tier 0. The validation seam. One schema declaration projects to a runtime validator, a TypeScript
|
|
4
|
+
type, a JSON Schema, an OpenAPI body, an MCP tool `inputSchema` and HTTP query coercion.
|
|
5
|
+
|
|
6
|
+
| Owns | Module |
|
|
7
|
+
|---|---|
|
|
8
|
+
| Standard Schema v1 types + `validate()` / `parse()` for **any** conforming library | `standard.ts` |
|
|
9
|
+
| the blessed `t` namespace | `t.ts` |
|
|
10
|
+
| builtin validators behind `t` | `validators.ts` |
|
|
11
|
+
| the introspectable IR every generator walks | `node.ts` |
|
|
12
|
+
| `Schema` factory, `Infer` machinery | `builder.ts` |
|
|
13
|
+
| `configureSchemaProvider()` — the swap point | `provider.ts` |
|
|
14
|
+
| schema → JSON Schema (OpenAPI + MCP) | `json-schema.ts` |
|
|
15
|
+
| HTTP-boundary coercion, kept out of validation | `coerce.ts` |
|
|
16
|
+
| `X_VALIDATION_FAILED` with path-annotated issues | `errors.ts` |
|
|
17
|
+
|
|
18
|
+
## `t`
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { t, type Infer } from '@ultimat3/schema';
|
|
22
|
+
|
|
23
|
+
export const publishPost = t.object({
|
|
24
|
+
postId: t.uuid,
|
|
25
|
+
notify: t.boolean.default(true),
|
|
26
|
+
title: t.string.min(3).max(80),
|
|
27
|
+
tags: t.array(t.slug),
|
|
28
|
+
price: t.money, // { minor: 1999, currency: 'EUR' } — never a float
|
|
29
|
+
timeZone: t.timezone, // real IANA validation, not an annotation
|
|
30
|
+
cursor: t.optional(t.cursor),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
type PublishPost = Infer<typeof publishPost>;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Value schemas | Factories | Methods |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `string` `number` `boolean` `date` `uuid` `email` `url` | `object` `array` `enum` `literal` `union` `record` `optional` `nullable` | `.default(v)` `.optional()` `.nullable()` `.describe(s)` |
|
|
39
|
+
| `money` `timezone` `locale` `slug` `cursor` | `object(...).extend/.pick/.omit` | `string.min/.max/.pattern`, `number.min/.max/.int` |
|
|
40
|
+
|
|
41
|
+
`nullable` is a value the row holds; `optional` is the caller omitting the key. Both ship as a
|
|
42
|
+
namespace member (`t.nullable`) and a free function (`nullableSchema`) — symmetrically.
|
|
43
|
+
|
|
44
|
+
Unknown object keys are **dropped**, never forwarded — an action cannot be mass-assigned.
|
|
45
|
+
|
|
46
|
+
**You rarely import this package.** `action`, `query`, `jobs` and `entity` each re-export the same
|
|
47
|
+
`t`, so an authoring file imports its primitive and nothing else. Import `@ultimat3/schema` directly
|
|
48
|
+
only where there is no primitive to hang the schema off — app config, a standalone view schema, a
|
|
49
|
+
provider swap.
|
|
50
|
+
|
|
51
|
+
## Errors tell an agent exactly what to send
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
parse(t.object({ postId: t.uuid, notify: t.boolean }), { postId: 'abc', notify: 'yes' }, 'input');
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
X_VALIDATION_FAILED: value did not match its schema
|
|
59
|
+
cause: postId: expected a uuid, received "abc"; notify: expected a boolean, received "yes"
|
|
60
|
+
fix: send input with the field(s) named in cause corrected to the expected type
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`error.issues` is `{ path, expected, received, message }[]` with paths like `items[0].price`;
|
|
64
|
+
`formatIssues()` renders one line per issue for the dev overlay. `validate()` never throws and
|
|
65
|
+
returns the Standard Schema result. Async libraries: `parseAsync()` / `validateAsync()`.
|
|
66
|
+
|
|
67
|
+
This package is tier 0 and cannot import `@ultimat3/core`, so `SchemaError` reproduces
|
|
68
|
+
`UltimateError` structurally and carries the same `Symbol.for('ultimate.error')` brand —
|
|
69
|
+
`isUltimateError()` from core matches it. Register the codes once from any package that imports
|
|
70
|
+
both: `registerErrorCodes(SCHEMA_ERROR_CODES)`.
|
|
71
|
+
|
|
72
|
+
## Swapping the library
|
|
73
|
+
|
|
74
|
+
The builtin validators (`validators.ts`) are the shipped default — small, dependency-free, no
|
|
75
|
+
adapter to install. No ArkType, Zod or Valibot adapter ships in this package; swapping to one
|
|
76
|
+
means writing the ~40-line adapter below yourself. All three implement Standard Schema v1, so
|
|
77
|
+
any of them works identically once wired.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { configureSchemaProvider } from '@ultimat3/schema';
|
|
81
|
+
import { arkNamespace, arkToNode } from './ark'; // your ~40-line adapter
|
|
82
|
+
|
|
83
|
+
configureSchemaProvider({ vendor: 'arktype', t: arkNamespace, introspect: arkToNode });
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`t` reads the active provider on every access, so modules that imported `t` earlier pick up the
|
|
87
|
+
swap. `introspect()` is what OpenAPI/MCP generation needs; a provider without it throws
|
|
88
|
+
`X_SCHEMA_UNSUPPORTED` with the exact fix rather than emitting an empty request body.
|
|
89
|
+
|
|
90
|
+
## JSON Schema and coercion
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
toJsonSchema(publishPost); // OpenAPI 3.1 (2020-12)
|
|
94
|
+
toMcpInputSchema(publishPost); // MCP tools/list (draft-07, no $schema)
|
|
95
|
+
parse(publishPost, coerceQuery(publishPost, url.searchParams));
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Coercion is separate from validation on purpose: only the HTTP layer has strings that mean
|
|
99
|
+
numbers. `coerceQuery` promotes repeated params to arrays and leaves anything ambiguous
|
|
100
|
+
untouched so validation still produces the real error.
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ultimat3/schema",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Ultimate's validation seam: Standard Schema interface, the t namespace, JSON Schema output",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/developerz-ai/ultimate.git",
|
|
10
|
+
"directory": "packages/schema"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public",
|
|
14
|
+
"provenance": true
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./src/index.ts"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"!src/**/*.test.ts",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"bun": ">=1.3.0"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
30
|
+
"test": "bun test"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {}
|
|
33
|
+
}
|
package/src/builder.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SchemaNode } from './node';
|
|
2
|
+
import { type InferInput, type InferOutput, type StandardIssue, type StandardResult, type StandardSchemaV1 } from './standard';
|
|
3
|
+
export declare const VENDOR = "ultimate";
|
|
4
|
+
export type Path = readonly PropertyKey[];
|
|
5
|
+
export interface CheckOk<Out> {
|
|
6
|
+
readonly ok: true;
|
|
7
|
+
readonly value: Out;
|
|
8
|
+
}
|
|
9
|
+
export interface CheckErr {
|
|
10
|
+
readonly ok: false;
|
|
11
|
+
readonly issues: readonly StandardIssue[];
|
|
12
|
+
}
|
|
13
|
+
export type CheckResult<Out> = CheckOk<Out> | CheckErr;
|
|
14
|
+
export type Check<Out> = (value: unknown, path: Path) => CheckResult<Out>;
|
|
15
|
+
export declare function pass<Out>(value: Out): CheckOk<Out>;
|
|
16
|
+
export declare function fail(path: Path, message: string): CheckErr;
|
|
17
|
+
export declare function failWith(issues: readonly StandardIssue[]): CheckErr;
|
|
18
|
+
/** `expected uuid, received "abc"` — the message an agent can act on without guessing. */
|
|
19
|
+
export declare function expected(what: string, value: unknown): string;
|
|
20
|
+
export declare function describeValue(value: unknown): string;
|
|
21
|
+
export interface Schema<In = unknown, Out = In> extends StandardSchemaV1<In, Out> {
|
|
22
|
+
/** The introspectable IR. JSON Schema, OpenAPI, MCP and coercion all read this. */
|
|
23
|
+
readonly node: SchemaNode;
|
|
24
|
+
parse(value: unknown, root?: string): Out;
|
|
25
|
+
safeParse(value: unknown): StandardResult<Out>;
|
|
26
|
+
optional(): Schema<In | undefined, Out | undefined>;
|
|
27
|
+
/**
|
|
28
|
+
* Distinct from `optional()`: absent and null are different facts. A nullable column holds a
|
|
29
|
+
* row that says "no cover image"; an optional field says the caller did not mention it. SQL
|
|
30
|
+
* has always drawn that line, so the schema has to as well or a round-trip loses it.
|
|
31
|
+
*/
|
|
32
|
+
nullable(): Schema<In | null, Out | null>;
|
|
33
|
+
default(value: Out): Schema<In | undefined, Out>;
|
|
34
|
+
describe(description: string): Schema<In, Out>;
|
|
35
|
+
}
|
|
36
|
+
/** The general constraint for "some schema". Methods are bivariant, so this is a real supertype. */
|
|
37
|
+
export type AnySchema = Schema<unknown, unknown>;
|
|
38
|
+
export type Shape = Readonly<Record<string, AnySchema>>;
|
|
39
|
+
export type Simplify<T> = {
|
|
40
|
+
[K in keyof T]: T[K];
|
|
41
|
+
} & {};
|
|
42
|
+
type OptionalOutputKeys<S extends Shape> = {
|
|
43
|
+
[K in keyof S]: undefined extends InferOutput<S[K]> ? K : never;
|
|
44
|
+
}[keyof S];
|
|
45
|
+
type OptionalInputKeys<S extends Shape> = {
|
|
46
|
+
[K in keyof S]: undefined extends InferInput<S[K]> ? K : never;
|
|
47
|
+
}[keyof S];
|
|
48
|
+
export type ShapeOutput<S extends Shape> = Simplify<{
|
|
49
|
+
[K in Exclude<keyof S, OptionalOutputKeys<S>>]: InferOutput<S[K]>;
|
|
50
|
+
} & {
|
|
51
|
+
[K in OptionalOutputKeys<S>]?: InferOutput<S[K]>;
|
|
52
|
+
}>;
|
|
53
|
+
export type ShapeInput<S extends Shape> = Simplify<{
|
|
54
|
+
[K in Exclude<keyof S, OptionalInputKeys<S>>]: InferInput<S[K]>;
|
|
55
|
+
} & {
|
|
56
|
+
[K in OptionalInputKeys<S>]?: InferInput<S[K]>;
|
|
57
|
+
}>;
|
|
58
|
+
export declare function makeSchema<In, Out>(node: SchemaNode, check: Check<Out>): Schema<In, Out>;
|
|
59
|
+
/** Read the check back off a builtin schema so composites can reuse it. */
|
|
60
|
+
export declare function checkOf<In, Out>(schema: Schema<In, Out>): Check<Out>;
|
|
61
|
+
export {};
|
|
62
|
+
//# sourceMappingURL=builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["builder.ts"],"names":[],"mappings":"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"}
|
package/src/builder.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Single responsibility: the `Schema` type every builtin validator returns, and the factory
|
|
2
|
+
// that turns a check function plus an IR node into a Standard-Schema-conforming object.
|
|
3
|
+
import { ValidationFailedError } from './errors';
|
|
4
|
+
import { formatPath, } from './standard';
|
|
5
|
+
export const VENDOR = 'ultimate';
|
|
6
|
+
export function pass(value) {
|
|
7
|
+
return { ok: true, value };
|
|
8
|
+
}
|
|
9
|
+
export function fail(path, message) {
|
|
10
|
+
return { ok: false, issues: [{ message, path: [...path] }] };
|
|
11
|
+
}
|
|
12
|
+
export function failWith(issues) {
|
|
13
|
+
return { ok: false, issues };
|
|
14
|
+
}
|
|
15
|
+
/** `expected uuid, received "abc"` — the message an agent can act on without guessing. */
|
|
16
|
+
export function expected(what, value) {
|
|
17
|
+
return `expected ${what}, received ${describeValue(value)}`;
|
|
18
|
+
}
|
|
19
|
+
export function describeValue(value) {
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
return 'undefined';
|
|
22
|
+
if (value === null)
|
|
23
|
+
return 'null';
|
|
24
|
+
if (typeof value === 'string')
|
|
25
|
+
return JSON.stringify(value);
|
|
26
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
27
|
+
return String(value);
|
|
28
|
+
if (Array.isArray(value))
|
|
29
|
+
return `array(${value.length})`;
|
|
30
|
+
if (value instanceof Date)
|
|
31
|
+
return `Date(${value.toISOString()})`;
|
|
32
|
+
return typeof value;
|
|
33
|
+
}
|
|
34
|
+
function toIssues(result) {
|
|
35
|
+
return result.issues.map((issue) => ({
|
|
36
|
+
path: formatPath(issue.path),
|
|
37
|
+
expected: issue.message,
|
|
38
|
+
received: '',
|
|
39
|
+
message: issue.message,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
export function makeSchema(node, check) {
|
|
43
|
+
const schema = {
|
|
44
|
+
'~standard': {
|
|
45
|
+
version: 1,
|
|
46
|
+
vendor: VENDOR,
|
|
47
|
+
validate: (value) => {
|
|
48
|
+
const result = check(value, []);
|
|
49
|
+
return result.ok ? { value: result.value } : { issues: result.issues };
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
node,
|
|
53
|
+
parse(value, root = 'value') {
|
|
54
|
+
const result = check(value, []);
|
|
55
|
+
if (result.ok)
|
|
56
|
+
return result.value;
|
|
57
|
+
throw new ValidationFailedError(toIssues(result), root);
|
|
58
|
+
},
|
|
59
|
+
safeParse(value) {
|
|
60
|
+
const result = check(value, []);
|
|
61
|
+
return result.ok ? { value: result.value } : { issues: result.issues };
|
|
62
|
+
},
|
|
63
|
+
optional() {
|
|
64
|
+
return makeSchema({ ...node, optional: true }, (value, path) => (value === undefined ? pass(undefined) : check(value, path)));
|
|
65
|
+
},
|
|
66
|
+
nullable() {
|
|
67
|
+
return makeSchema({ ...node, nullable: true }, (value, path) => value === null ? pass(null) : check(value, path));
|
|
68
|
+
},
|
|
69
|
+
default(fallback) {
|
|
70
|
+
return makeSchema({ ...node, hasDefault: true, default: fallback }, (value, path) => (value === undefined ? pass(fallback) : check(value, path)));
|
|
71
|
+
},
|
|
72
|
+
describe(description) {
|
|
73
|
+
return makeSchema({ ...node, description }, check);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
return schema;
|
|
77
|
+
}
|
|
78
|
+
/** Read the check back off a builtin schema so composites can reuse it. */
|
|
79
|
+
export function checkOf(schema) {
|
|
80
|
+
return (value, path) => {
|
|
81
|
+
const result = schema['~standard'].validate(value);
|
|
82
|
+
if (result instanceof Promise) {
|
|
83
|
+
return fail(path, 'expected a synchronous schema, received an async one');
|
|
84
|
+
}
|
|
85
|
+
if (result.issues === undefined)
|
|
86
|
+
return pass(result.value);
|
|
87
|
+
return failWith(result.issues.map((issue) => ({
|
|
88
|
+
message: issue.message,
|
|
89
|
+
path: [...path, ...(issue.path ?? [])],
|
|
90
|
+
})));
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.js","sourceRoot":"","sources":["builder.ts"],"names":[],"mappings":"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"}
|
package/src/builder.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Single responsibility: the `Schema` type every builtin validator returns, and the factory
|
|
2
|
+
// that turns a check function plus an IR node into a Standard-Schema-conforming object.
|
|
3
|
+
|
|
4
|
+
import { ValidationFailedError, type ValidationIssue } from './errors';
|
|
5
|
+
import type { SchemaNode } from './node';
|
|
6
|
+
import {
|
|
7
|
+
formatPath,
|
|
8
|
+
type InferInput,
|
|
9
|
+
type InferOutput,
|
|
10
|
+
type StandardIssue,
|
|
11
|
+
type StandardResult,
|
|
12
|
+
type StandardSchemaV1,
|
|
13
|
+
} from './standard';
|
|
14
|
+
|
|
15
|
+
export const VENDOR = 'ultimate';
|
|
16
|
+
|
|
17
|
+
export type Path = readonly PropertyKey[];
|
|
18
|
+
|
|
19
|
+
export interface CheckOk<Out> {
|
|
20
|
+
readonly ok: true;
|
|
21
|
+
readonly value: Out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CheckErr {
|
|
25
|
+
readonly ok: false;
|
|
26
|
+
readonly issues: readonly StandardIssue[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type CheckResult<Out> = CheckOk<Out> | CheckErr;
|
|
30
|
+
|
|
31
|
+
export type Check<Out> = (value: unknown, path: Path) => CheckResult<Out>;
|
|
32
|
+
|
|
33
|
+
export function pass<Out>(value: Out): CheckOk<Out> {
|
|
34
|
+
return { ok: true, value };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function fail(path: Path, message: string): CheckErr {
|
|
38
|
+
return { ok: false, issues: [{ message, path: [...path] }] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function failWith(issues: readonly StandardIssue[]): CheckErr {
|
|
42
|
+
return { ok: false, issues };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `expected uuid, received "abc"` — the message an agent can act on without guessing. */
|
|
46
|
+
export function expected(what: string, value: unknown): string {
|
|
47
|
+
return `expected ${what}, received ${describeValue(value)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function describeValue(value: unknown): string {
|
|
51
|
+
if (value === undefined) return 'undefined';
|
|
52
|
+
if (value === null) return 'null';
|
|
53
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
54
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
55
|
+
if (Array.isArray(value)) return `array(${value.length})`;
|
|
56
|
+
if (value instanceof Date) {
|
|
57
|
+
return Number.isNaN(value.getTime()) ? 'Date(Invalid Date)' : `Date(${value.toISOString()})`;
|
|
58
|
+
}
|
|
59
|
+
return typeof value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface Schema<In = unknown, Out = In> extends StandardSchemaV1<In, Out> {
|
|
63
|
+
/** The introspectable IR. JSON Schema, OpenAPI, MCP and coercion all read this. */
|
|
64
|
+
readonly node: SchemaNode;
|
|
65
|
+
parse(value: unknown, root?: string): Out;
|
|
66
|
+
safeParse(value: unknown): StandardResult<Out>;
|
|
67
|
+
optional(): Schema<In | undefined, Out | undefined>;
|
|
68
|
+
/**
|
|
69
|
+
* Distinct from `optional()`: absent and null are different facts. A nullable column holds a
|
|
70
|
+
* row that says "no cover image"; an optional field says the caller did not mention it. SQL
|
|
71
|
+
* has always drawn that line, so the schema has to as well or a round-trip loses it.
|
|
72
|
+
*/
|
|
73
|
+
nullable(): Schema<In | null, Out | null>;
|
|
74
|
+
default(value: Out): Schema<In | undefined, Out>;
|
|
75
|
+
describe(description: string): Schema<In, Out>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The general constraint for "some schema". Methods are bivariant, so this is a real supertype. */
|
|
79
|
+
export type AnySchema = Schema<unknown, unknown>;
|
|
80
|
+
|
|
81
|
+
export type Shape = Readonly<Record<string, AnySchema>>;
|
|
82
|
+
|
|
83
|
+
export type Simplify<T> = { [K in keyof T]: T[K] } & {};
|
|
84
|
+
|
|
85
|
+
type OptionalOutputKeys<S extends Shape> = {
|
|
86
|
+
[K in keyof S]: undefined extends InferOutput<S[K]> ? K : never;
|
|
87
|
+
}[keyof S];
|
|
88
|
+
|
|
89
|
+
type OptionalInputKeys<S extends Shape> = {
|
|
90
|
+
[K in keyof S]: undefined extends InferInput<S[K]> ? K : never;
|
|
91
|
+
}[keyof S];
|
|
92
|
+
|
|
93
|
+
export type ShapeOutput<S extends Shape> = Simplify<
|
|
94
|
+
{ [K in Exclude<keyof S, OptionalOutputKeys<S>>]: InferOutput<S[K]> } & {
|
|
95
|
+
[K in OptionalOutputKeys<S>]?: InferOutput<S[K]>;
|
|
96
|
+
}
|
|
97
|
+
>;
|
|
98
|
+
|
|
99
|
+
export type ShapeInput<S extends Shape> = Simplify<
|
|
100
|
+
{ [K in Exclude<keyof S, OptionalInputKeys<S>>]: InferInput<S[K]> } & {
|
|
101
|
+
[K in OptionalInputKeys<S>]?: InferInput<S[K]>;
|
|
102
|
+
}
|
|
103
|
+
>;
|
|
104
|
+
|
|
105
|
+
function toIssues(result: CheckErr): readonly ValidationIssue[] {
|
|
106
|
+
return result.issues.map((issue) => ({
|
|
107
|
+
path: formatPath(issue.path),
|
|
108
|
+
expected: issue.message,
|
|
109
|
+
received: '',
|
|
110
|
+
message: issue.message,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function makeSchema<In, Out>(node: SchemaNode, check: Check<Out>): Schema<In, Out> {
|
|
115
|
+
const schema: Schema<In, Out> = {
|
|
116
|
+
'~standard': {
|
|
117
|
+
version: 1,
|
|
118
|
+
vendor: VENDOR,
|
|
119
|
+
validate: (value: unknown): StandardResult<Out> => {
|
|
120
|
+
const result = check(value, []);
|
|
121
|
+
return result.ok ? { value: result.value } : { issues: result.issues };
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
node,
|
|
125
|
+
parse(value: unknown, root = 'value'): Out {
|
|
126
|
+
const result = check(value, []);
|
|
127
|
+
if (result.ok) return result.value;
|
|
128
|
+
throw new ValidationFailedError(toIssues(result), root);
|
|
129
|
+
},
|
|
130
|
+
safeParse(value: unknown): StandardResult<Out> {
|
|
131
|
+
const result = check(value, []);
|
|
132
|
+
return result.ok ? { value: result.value } : { issues: result.issues };
|
|
133
|
+
},
|
|
134
|
+
optional(): Schema<In | undefined, Out | undefined> {
|
|
135
|
+
return makeSchema<In | undefined, Out | undefined>(
|
|
136
|
+
{ ...node, optional: true },
|
|
137
|
+
(value, path) => (value === undefined ? pass(undefined) : check(value, path)),
|
|
138
|
+
);
|
|
139
|
+
},
|
|
140
|
+
nullable(): Schema<In | null, Out | null> {
|
|
141
|
+
return makeSchema<In | null, Out | null>({ ...node, nullable: true }, (value, path) =>
|
|
142
|
+
value === null ? pass(null) : check(value, path),
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
default(fallback: Out): Schema<In | undefined, Out> {
|
|
146
|
+
return makeSchema<In | undefined, Out>(
|
|
147
|
+
{ ...node, hasDefault: true, default: fallback },
|
|
148
|
+
(value, path) => (value === undefined ? pass(fallback) : check(value, path)),
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
describe(description: string): Schema<In, Out> {
|
|
152
|
+
return makeSchema<In, Out>({ ...node, description }, check);
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
return schema;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Read the check back off a builtin schema so composites can reuse it. */
|
|
159
|
+
export function checkOf<In, Out>(schema: Schema<In, Out>): Check<Out> {
|
|
160
|
+
return (value, path) => {
|
|
161
|
+
const result = schema['~standard'].validate(value);
|
|
162
|
+
if (result instanceof Promise) {
|
|
163
|
+
return fail(path, 'expected a synchronous schema, received an async one');
|
|
164
|
+
}
|
|
165
|
+
if (result.issues === undefined) return pass(result.value);
|
|
166
|
+
return failWith(
|
|
167
|
+
result.issues.map((issue) => ({
|
|
168
|
+
message: issue.message,
|
|
169
|
+
path: [...path, ...(issue.path ?? [])],
|
|
170
|
+
})),
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
}
|
package/src/coerce.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SchemaNode } from './node';
|
|
2
|
+
export type QuerySource = URLSearchParams | Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
3
|
+
/**
|
|
4
|
+
* Coerce one raw value toward `node`. Never throws and never invents data: anything it cannot
|
|
5
|
+
* confidently convert is returned untouched so validation produces the real error message.
|
|
6
|
+
*/
|
|
7
|
+
export declare function coerceNode(node: SchemaNode, raw: unknown): unknown;
|
|
8
|
+
/**
|
|
9
|
+
* Coerce a query string / route params object against a schema, ready to `parse()`.
|
|
10
|
+
* Unknown keys are passed through — the schema decides what to keep.
|
|
11
|
+
*/
|
|
12
|
+
export declare function coerceQuery(schema: unknown, source: QuerySource): Record<string, unknown>;
|
|
13
|
+
/** Coerce an arbitrary record (route params, form data) against a schema. */
|
|
14
|
+
export declare function coerceInput(schema: unknown, raw: Readonly<Record<string, unknown>>): Record<string, unknown>;
|
|
15
|
+
//# sourceMappingURL=coerce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coerce.d.ts","sourceRoot":"","sources":["coerce.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAMzC,MAAM,MAAM,WAAW,GACnB,eAAe,GACf,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC;AAErE;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CA2DlE;AAcD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYzF;AAMD,6EAA6E;AAC7E,wBAAgB,WAAW,CACzB,MAAM,EAAE,OAAO,EACf,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACrC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOzB"}
|
package/src/coerce.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Single responsibility: HTTP-boundary coercion. Kept out of validation on purpose — only the
|
|
2
|
+
// HTTP layer has strings that "mean" numbers. Actions, jobs and MCP calls receive real JSON and
|
|
3
|
+
// must never get this leniency.
|
|
4
|
+
import { tryIntrospect } from './provider';
|
|
5
|
+
const TRUE_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
6
|
+
const FALSE_VALUES = new Set(['0', 'false', 'no', 'off', '']);
|
|
7
|
+
/**
|
|
8
|
+
* Coerce one raw value toward `node`. Never throws and never invents data: anything it cannot
|
|
9
|
+
* confidently convert is returned untouched so validation produces the real error message.
|
|
10
|
+
*/
|
|
11
|
+
export function coerceNode(node, raw) {
|
|
12
|
+
if (raw === undefined || raw === null)
|
|
13
|
+
return raw;
|
|
14
|
+
switch (node.kind) {
|
|
15
|
+
case 'number': {
|
|
16
|
+
if (typeof raw !== 'string')
|
|
17
|
+
return raw;
|
|
18
|
+
const value = Number(raw);
|
|
19
|
+
return raw.trim() !== '' && Number.isFinite(value) ? value : raw;
|
|
20
|
+
}
|
|
21
|
+
case 'boolean': {
|
|
22
|
+
if (typeof raw !== 'string')
|
|
23
|
+
return raw;
|
|
24
|
+
const lowered = raw.toLowerCase();
|
|
25
|
+
if (TRUE_VALUES.has(lowered))
|
|
26
|
+
return true;
|
|
27
|
+
if (FALSE_VALUES.has(lowered))
|
|
28
|
+
return false;
|
|
29
|
+
return raw;
|
|
30
|
+
}
|
|
31
|
+
case 'date': {
|
|
32
|
+
if (typeof raw !== 'string')
|
|
33
|
+
return raw;
|
|
34
|
+
const parsed = new Date(raw);
|
|
35
|
+
return Number.isNaN(parsed.getTime()) ? raw : parsed;
|
|
36
|
+
}
|
|
37
|
+
case 'array': {
|
|
38
|
+
const items = Array.isArray(raw) ? raw : [raw];
|
|
39
|
+
const itemNode = node.items;
|
|
40
|
+
return itemNode === undefined ? items : items.map((item) => coerceNode(itemNode, item));
|
|
41
|
+
}
|
|
42
|
+
case 'record': {
|
|
43
|
+
if (typeof raw !== 'object' || node.valueNode === undefined)
|
|
44
|
+
return raw;
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
47
|
+
out[key] = coerceNode(node.valueNode, value);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
case 'object': {
|
|
52
|
+
if (typeof raw !== 'object' || node.properties === undefined)
|
|
53
|
+
return raw;
|
|
54
|
+
const source = raw;
|
|
55
|
+
const out = { ...source };
|
|
56
|
+
for (const [key, child] of Object.entries(node.properties)) {
|
|
57
|
+
if (key in source)
|
|
58
|
+
out[key] = coerceNode(child, source[key]);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
case 'money': {
|
|
63
|
+
if (typeof raw !== 'object')
|
|
64
|
+
return raw;
|
|
65
|
+
const source = raw;
|
|
66
|
+
const minor = typeof source['minor'] === 'string' ? Number(source['minor']) : source['minor'];
|
|
67
|
+
return Number.isFinite(minor) ? { ...source, minor } : raw;
|
|
68
|
+
}
|
|
69
|
+
case 'union': {
|
|
70
|
+
// Only unambiguous single-kind unions (e.g. `number | undefined`) are safe to coerce.
|
|
71
|
+
const kinds = new Set((node.anyOf ?? []).map((member) => member.kind));
|
|
72
|
+
if (kinds.size !== 1)
|
|
73
|
+
return raw;
|
|
74
|
+
const [member] = node.anyOf ?? [];
|
|
75
|
+
return member === undefined ? raw : coerceNode(member, raw);
|
|
76
|
+
}
|
|
77
|
+
default:
|
|
78
|
+
return raw;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function toRecord(source) {
|
|
82
|
+
if (!(source instanceof URLSearchParams)) {
|
|
83
|
+
return { ...source };
|
|
84
|
+
}
|
|
85
|
+
const out = {};
|
|
86
|
+
for (const key of new Set(source.keys())) {
|
|
87
|
+
const all = source.getAll(key);
|
|
88
|
+
out[key] = all.length > 1 ? all : all[0];
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Coerce a query string / route params object against a schema, ready to `parse()`.
|
|
94
|
+
* Unknown keys are passed through — the schema decides what to keep.
|
|
95
|
+
*/
|
|
96
|
+
export function coerceQuery(schema, source) {
|
|
97
|
+
const record = toRecord(source);
|
|
98
|
+
const node = tryIntrospect(schema);
|
|
99
|
+
if (node?.properties === undefined)
|
|
100
|
+
return record;
|
|
101
|
+
const out = { ...record };
|
|
102
|
+
for (const [key, child] of Object.entries(node.properties)) {
|
|
103
|
+
if (!(key in record))
|
|
104
|
+
continue;
|
|
105
|
+
const raw = record[key];
|
|
106
|
+
out[key] = coerceNode(child, child.kind === 'array' ? (raw ?? []) : normaliseSingle(raw));
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
function normaliseSingle(raw) {
|
|
111
|
+
return Array.isArray(raw) ? raw[raw.length - 1] : raw;
|
|
112
|
+
}
|
|
113
|
+
/** Coerce an arbitrary record (route params, form data) against a schema. */
|
|
114
|
+
export function coerceInput(schema, raw) {
|
|
115
|
+
const node = tryIntrospect(schema);
|
|
116
|
+
if (node === undefined)
|
|
117
|
+
return { ...raw };
|
|
118
|
+
const coerced = coerceNode(node, raw);
|
|
119
|
+
return typeof coerced === 'object' && coerced !== null
|
|
120
|
+
? coerced
|
|
121
|
+
: { ...raw };
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=coerce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coerce.js","sourceRoot":"","sources":["coerce.ts"],"names":[],"mappings":"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"}
|