@fougere/schema 0.1.0-alpha.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 +32 -0
- package/dist/entity.d.ts +117 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/entity.js +285 -0
- package/dist/entity.js.map +1 -0
- package/dist/field/boundary.d.ts +75 -0
- package/dist/field/boundary.d.ts.map +1 -0
- package/dist/field/boundary.js +76 -0
- package/dist/field/boundary.js.map +1 -0
- package/dist/field/field.d.ts +64 -0
- package/dist/field/field.d.ts.map +1 -0
- package/dist/field/field.js +28 -0
- package/dist/field/field.js.map +1 -0
- package/dist/field/index.d.ts +7 -0
- package/dist/field/index.d.ts.map +1 -0
- package/dist/field/index.js +9 -0
- package/dist/field/index.js.map +1 -0
- package/dist/field/lifecycle.d.ts +40 -0
- package/dist/field/lifecycle.d.ts.map +1 -0
- package/dist/field/lifecycle.js +27 -0
- package/dist/field/lifecycle.js.map +1 -0
- package/dist/field/meta.d.ts +9 -0
- package/dist/field/meta.d.ts.map +1 -0
- package/dist/field/meta.js +7 -0
- package/dist/field/meta.js.map +1 -0
- package/dist/field/role.d.ts +54 -0
- package/dist/field/role.d.ts.map +1 -0
- package/dist/field/role.js +55 -0
- package/dist/field/role.js.map +1 -0
- package/dist/field/shape.d.ts +77 -0
- package/dist/field/shape.d.ts.map +1 -0
- package/dist/field/shape.js +49 -0
- package/dist/field/shape.js.map +1 -0
- package/dist/hints.d.ts +33 -0
- package/dist/hints.d.ts.map +1 -0
- package/dist/hints.js +2 -0
- package/dist/hints.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/projections/card.d.ts +117 -0
- package/dist/projections/card.d.ts.map +1 -0
- package/dist/projections/card.js +8 -0
- package/dist/projections/card.js.map +1 -0
- package/dist/projections/describe.d.ts +25 -0
- package/dist/projections/describe.d.ts.map +1 -0
- package/dist/projections/describe.js +129 -0
- package/dist/projections/describe.js.map +1 -0
- package/dist/projections/encode.d.ts +11 -0
- package/dist/projections/encode.d.ts.map +1 -0
- package/dist/projections/encode.js +26 -0
- package/dist/projections/encode.js.map +1 -0
- package/dist/projections/io.d.ts +30 -0
- package/dist/projections/io.d.ts.map +1 -0
- package/dist/projections/io.js +52 -0
- package/dist/projections/io.js.map +1 -0
- package/dist/projections/lifecycle.d.ts +43 -0
- package/dist/projections/lifecycle.d.ts.map +1 -0
- package/dist/projections/lifecycle.js +115 -0
- package/dist/projections/lifecycle.js.map +1 -0
- package/dist/projections/reconstruct.d.ts +36 -0
- package/dist/projections/reconstruct.d.ts.map +1 -0
- package/dist/projections/reconstruct.js +149 -0
- package/dist/projections/reconstruct.js.map +1 -0
- package/dist/projections/source.d.ts +41 -0
- package/dist/projections/source.d.ts.map +1 -0
- package/dist/projections/source.js +37 -0
- package/dist/projections/source.js.map +1 -0
- package/dist/projections/standard.d.ts +41 -0
- package/dist/projections/standard.d.ts.map +1 -0
- package/dist/projections/standard.js +10 -0
- package/dist/projections/standard.js.map +1 -0
- package/dist/projections/typescript.d.ts +53 -0
- package/dist/projections/typescript.d.ts.map +1 -0
- package/dist/projections/typescript.js +166 -0
- package/dist/projections/typescript.js.map +1 -0
- package/dist/projections/validation.d.ts +49 -0
- package/dist/projections/validation.d.ts.map +1 -0
- package/dist/projections/validation.js +140 -0
- package/dist/projections/validation.js.map +1 -0
- package/dist/unique.d.ts +65 -0
- package/dist/unique.d.ts.map +1 -0
- package/dist/unique.js +80 -0
- package/dist/unique.js.map +1 -0
- package/dist/vocabulary/auto.d.ts +8 -0
- package/dist/vocabulary/auto.d.ts.map +1 -0
- package/dist/vocabulary/auto.js +13 -0
- package/dist/vocabulary/auto.js.map +1 -0
- package/dist/vocabulary/bool.d.ts +6 -0
- package/dist/vocabulary/bool.d.ts.map +1 -0
- package/dist/vocabulary/bool.js +9 -0
- package/dist/vocabulary/bool.js.map +1 -0
- package/dist/vocabulary/date.d.ts +3 -0
- package/dist/vocabulary/date.d.ts.map +1 -0
- package/dist/vocabulary/date.js +5 -0
- package/dist/vocabulary/date.js.map +1 -0
- package/dist/vocabulary/email.d.ts +5 -0
- package/dist/vocabulary/email.d.ts.map +1 -0
- package/dist/vocabulary/email.js +6 -0
- package/dist/vocabulary/email.js.map +1 -0
- package/dist/vocabulary/immutable.d.ts +9 -0
- package/dist/vocabulary/immutable.d.ts.map +1 -0
- package/dist/vocabulary/immutable.js +11 -0
- package/dist/vocabulary/immutable.js.map +1 -0
- package/dist/vocabulary/indexed.d.ts +19 -0
- package/dist/vocabulary/indexed.d.ts.map +1 -0
- package/dist/vocabulary/indexed.js +21 -0
- package/dist/vocabulary/indexed.js.map +1 -0
- package/dist/vocabulary/json.d.ts +18 -0
- package/dist/vocabulary/json.d.ts.map +1 -0
- package/dist/vocabulary/json.js +16 -0
- package/dist/vocabulary/json.js.map +1 -0
- package/dist/vocabulary/list.d.ts +21 -0
- package/dist/vocabulary/list.d.ts.map +1 -0
- package/dist/vocabulary/list.js +21 -0
- package/dist/vocabulary/list.js.map +1 -0
- package/dist/vocabulary/many.d.ts +8 -0
- package/dist/vocabulary/many.d.ts.map +1 -0
- package/dist/vocabulary/many.js +12 -0
- package/dist/vocabulary/many.js.map +1 -0
- package/dist/vocabulary/nullable.d.ts +9 -0
- package/dist/vocabulary/nullable.d.ts.map +1 -0
- package/dist/vocabulary/nullable.js +13 -0
- package/dist/vocabulary/nullable.js.map +1 -0
- package/dist/vocabulary/number.d.ts +10 -0
- package/dist/vocabulary/number.d.ts.map +1 -0
- package/dist/vocabulary/number.js +9 -0
- package/dist/vocabulary/number.js.map +1 -0
- package/dist/vocabulary/oneOf.d.ts +16 -0
- package/dist/vocabulary/oneOf.d.ts.map +1 -0
- package/dist/vocabulary/oneOf.js +13 -0
- package/dist/vocabulary/oneOf.js.map +1 -0
- package/dist/vocabulary/optional.d.ts +10 -0
- package/dist/vocabulary/optional.d.ts.map +1 -0
- package/dist/vocabulary/optional.js +15 -0
- package/dist/vocabulary/optional.js.map +1 -0
- package/dist/vocabulary/primary.d.ts +22 -0
- package/dist/vocabulary/primary.d.ts.map +1 -0
- package/dist/vocabulary/primary.js +31 -0
- package/dist/vocabulary/primary.js.map +1 -0
- package/dist/vocabulary/readOnly.d.ts +10 -0
- package/dist/vocabulary/readOnly.d.ts.map +1 -0
- package/dist/vocabulary/readOnly.js +12 -0
- package/dist/vocabulary/readOnly.js.map +1 -0
- package/dist/vocabulary/ref.d.ts +12 -0
- package/dist/vocabulary/ref.d.ts.map +1 -0
- package/dist/vocabulary/ref.js +20 -0
- package/dist/vocabulary/ref.js.map +1 -0
- package/dist/vocabulary/text.d.ts +12 -0
- package/dist/vocabulary/text.d.ts.map +1 -0
- package/dist/vocabulary/text.js +9 -0
- package/dist/vocabulary/text.js.map +1 -0
- package/dist/vocabulary/unique.d.ts +23 -0
- package/dist/vocabulary/unique.d.ts.map +1 -0
- package/dist/vocabulary/unique.js +25 -0
- package/dist/vocabulary/unique.js.map +1 -0
- package/dist/vocabulary/updated.d.ts +8 -0
- package/dist/vocabulary/updated.d.ts.map +1 -0
- package/dist/vocabulary/updated.js +12 -0
- package/dist/vocabulary/updated.js.map +1 -0
- package/dist/vocabulary/url.d.ts +5 -0
- package/dist/vocabulary/url.d.ts.map +1 -0
- package/dist/vocabulary/url.js +6 -0
- package/dist/vocabulary/url.js.map +1 -0
- package/dist/vocabulary/writeOnly.d.ts +9 -0
- package/dist/vocabulary/writeOnly.d.ts.map +1 -0
- package/dist/vocabulary/writeOnly.js +11 -0
- package/dist/vocabulary/writeOnly.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* card → TypeScript source — the projection that was missing.
|
|
3
|
+
*
|
|
4
|
+
* `describe` turns a schema into a card and `reconstruct` turns it back into a
|
|
5
|
+
* living judge, so a consumer in another repository validates by the host's rules
|
|
6
|
+
* without holding its code. What it did NOT get is a type: left to infer, a rebuilt
|
|
7
|
+
* schema has an index signature for an instance type, so `post.title` was `any` and
|
|
8
|
+
* `post.titel` compiled. The declaration travelled and the compiler learned nothing.
|
|
9
|
+
*
|
|
10
|
+
* So the card gets a third reader, and it writes the shape where `reconstruct` now
|
|
11
|
+
* accepts it — as the type argument of the class's own base expression, one
|
|
12
|
+
* declaration for the judge and the rows it judges. Same input as the other two
|
|
13
|
+
* readers, same rule: the shape IS JSON Schema, and JSON Schema has a type form.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately NOT a JSON Schema→TS library: the input is not arbitrary JSON Schema,
|
|
16
|
+
* it is a Fougère card, whose vocabulary is closed (`describe` writes it). A general
|
|
17
|
+
* library would carry `$ref`, `allOf`, tuple `items`, `additionalProperties` and the
|
|
18
|
+
* rest of a spec we never emit, and would still need this file to know that
|
|
19
|
+
* `format: date-time` means `Date` here — a fact about our boundary, not about JSON.
|
|
20
|
+
*/
|
|
21
|
+
/** What a field's value looks like once `reconstruct` has decoded it. */
|
|
22
|
+
function typeOf(field) {
|
|
23
|
+
const types = Array.isArray(field.type) ? field.type : field.type ? [field.type] : [];
|
|
24
|
+
const nullable = types.includes('null');
|
|
25
|
+
const base = types.find((t) => t !== 'null');
|
|
26
|
+
const inner = baseTypeOf(base, field);
|
|
27
|
+
return nullable ? `${inner} | null` : inner;
|
|
28
|
+
}
|
|
29
|
+
function baseTypeOf(base, field) {
|
|
30
|
+
// A bounded value set IS a type — the enum travels, so the union does too.
|
|
31
|
+
if (field.enum?.length) {
|
|
32
|
+
return field.enum.map((v) => (v === null ? 'null' : JSON.stringify(v))).join(' | ');
|
|
33
|
+
}
|
|
34
|
+
switch (base) {
|
|
35
|
+
case 'string':
|
|
36
|
+
// `date-time` is the wire form of a value the boundary decodes to a Date, and
|
|
37
|
+
// the façade now hands the decoded value on. The type says what you receive.
|
|
38
|
+
return field.format === 'date-time' ? 'Date' : 'string';
|
|
39
|
+
case 'number':
|
|
40
|
+
case 'integer':
|
|
41
|
+
return 'number';
|
|
42
|
+
case 'boolean':
|
|
43
|
+
return 'boolean';
|
|
44
|
+
case 'array':
|
|
45
|
+
// Two distinct things share `array`: a value list carries `items`, a bare `many`
|
|
46
|
+
// relation carries none — the ids of the other side, which the card names as a
|
|
47
|
+
// relation rather than a shape.
|
|
48
|
+
return field.items ? `${typeOf(field.items)}[]` : 'string[]';
|
|
49
|
+
case 'object':
|
|
50
|
+
return field.properties ? objectTypeOf(field.properties, field.required ?? []) : 'Record<string, unknown>';
|
|
51
|
+
default:
|
|
52
|
+
return 'unknown';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function objectTypeOf(properties, required) {
|
|
56
|
+
const members = Object.entries(properties).map(([name, field]) => {
|
|
57
|
+
const optional = required.includes(name) ? '' : '?';
|
|
58
|
+
return `${propertyKey(name)}${optional}: ${typeOf(field)}`;
|
|
59
|
+
});
|
|
60
|
+
return `{ ${members.join('; ')} }`;
|
|
61
|
+
}
|
|
62
|
+
/** Quote a key only when it is not a plain identifier — generated source stays readable. */
|
|
63
|
+
function propertyKey(name) {
|
|
64
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The anonymous object type one card describes — the members, nothing around them.
|
|
68
|
+
*
|
|
69
|
+
* Every property is present: `required` on a Fougère card answers "what must a CALLER
|
|
70
|
+
* supply at creation", not "what is always there when read" — an `id` the system
|
|
71
|
+
* generates is absent from `required` and always present on a row. Typing the read
|
|
72
|
+
* shape from the create rule would make `post.id` possibly-undefined for everyone.
|
|
73
|
+
*/
|
|
74
|
+
export function shapeTypeOf(descriptor, indent = '') {
|
|
75
|
+
const entries = Object.entries(descriptor.properties ?? {});
|
|
76
|
+
if (entries.length === 0)
|
|
77
|
+
return '{}';
|
|
78
|
+
const lines = entries.map(([key, field]) => {
|
|
79
|
+
const doc = field.description ? `${indent} /** ${field.description} */\n` : '';
|
|
80
|
+
return `${doc}${indent} ${propertyKey(key)}: ${typeOf(field)};`;
|
|
81
|
+
});
|
|
82
|
+
return `{\n${lines.join('\n')}\n${indent}}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Emit the entity a card describes — ONE class, judge and shape together.
|
|
86
|
+
*
|
|
87
|
+
* An entity is a class here (`class Post extends entity({…}) {}`), and a class is the
|
|
88
|
+
* language's own answer to "a name that is both a value and a type". Emitting an
|
|
89
|
+
* interface next to a const said the same thing twice and relied on declaration
|
|
90
|
+
* merging to look like a class; this IS one.
|
|
91
|
+
*
|
|
92
|
+
* The card travels inline, so the rebuilt judge is exact — the shape written above it
|
|
93
|
+
* is read off that same card, never a second source to keep in step.
|
|
94
|
+
*
|
|
95
|
+
* Requires `reconstruct` in scope: the caller writes the import, since only it knows
|
|
96
|
+
* whether the consumer says `@fougere/schema` or a path.
|
|
97
|
+
*/
|
|
98
|
+
export function entitySourceOf(descriptor, options = {}) {
|
|
99
|
+
const name = identifierOf(options.name ?? capitalize(descriptor.title ?? 'Schema'));
|
|
100
|
+
const exported = options.exported === false ? '' : 'export ';
|
|
101
|
+
const card = JSON.stringify(descriptor, null, 2)
|
|
102
|
+
.split('\n')
|
|
103
|
+
.map((line, i) => (i === 0 ? line : ` ${line}`))
|
|
104
|
+
.join('\n');
|
|
105
|
+
return `${exported}class ${name} extends reconstruct<${shapeTypeOf(descriptor)}>(${card}) {}`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A card names itself, and a card can come from a stranger. Everything else here emits
|
|
109
|
+
* data — a string lands inside `JSON.stringify` — but a name lands in a DECLARATION,
|
|
110
|
+
* so it is the one value that could stop being data. Refuse it here as well as at the
|
|
111
|
+
* caller: the projection should not depend on who calls it to be safe.
|
|
112
|
+
*/
|
|
113
|
+
function identifierOf(name) {
|
|
114
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) {
|
|
115
|
+
throw new Error(`'${name}' is not a TypeScript identifier — it cannot name a generated declaration`);
|
|
116
|
+
}
|
|
117
|
+
return name;
|
|
118
|
+
}
|
|
119
|
+
function capitalize(s) {
|
|
120
|
+
return s ? s[0].toUpperCase() + s.slice(1) : s;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* What one operation hands back, in TypeScript.
|
|
124
|
+
*
|
|
125
|
+
* `output` is the shape of a ROW; `cardinality` is how many rows. Separating them is
|
|
126
|
+
* what makes this honest — `list` returns a page, not an array, and a generator that
|
|
127
|
+
* assumed otherwise would have written a signature that compiles and lies.
|
|
128
|
+
*
|
|
129
|
+
* A page is spelled as what it IS: `ListResult<T> extends Array<T>`, an array carrying
|
|
130
|
+
* its own totals. So the type is the array intersected with them, not a wrapper.
|
|
131
|
+
*/
|
|
132
|
+
function returnTypeOf(op, rowType) {
|
|
133
|
+
switch (op.cardinality) {
|
|
134
|
+
case 'many': return `${rowType}[]`;
|
|
135
|
+
case 'page': return `${rowType}[] & { total?: number; endCursor?: string; hasMore?: boolean }`;
|
|
136
|
+
case 'maybe': return `${rowType} | undefined`;
|
|
137
|
+
case 'one': return rowType;
|
|
138
|
+
// `none` says there is no shaped output — a boolean, a void. Saying `unknown` is
|
|
139
|
+
// the truth; guessing `void` would forbid reading a value that does come back.
|
|
140
|
+
case 'none': return 'unknown';
|
|
141
|
+
default: return 'unknown';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Emit the type of a façade — every operation an entity serves, as a caller meets it.
|
|
146
|
+
*
|
|
147
|
+
* This is what `Facade<T>` needs and what no consumer in another repository could have:
|
|
148
|
+
* writing `Facade<ArticleHandler>` used to require importing the handler's class, which
|
|
149
|
+
* `sync` does not carry and should not. The card carries the operations; this reads them.
|
|
150
|
+
*
|
|
151
|
+
* Each op takes an optional invocation and returns a promise — the door's signature, not
|
|
152
|
+
* the handler's. A handler's method takes positional arguments; its door takes the call.
|
|
153
|
+
*/
|
|
154
|
+
export function facadeTypeSourceOf(ops, options = {}) {
|
|
155
|
+
const name = options.name ?? 'Facade';
|
|
156
|
+
const exported = options.exported === false ? '' : 'export ';
|
|
157
|
+
const rowType = options.rowType ?? 'unknown';
|
|
158
|
+
const members = ops.map((op) => {
|
|
159
|
+
const doc = op.description ? ` /** ${op.description} */\n` : '';
|
|
160
|
+
return `${doc} ${propertyKey(op.name)}(invocation?: Invocation): Promise<${returnTypeOf(op, rowType)}>;`;
|
|
161
|
+
});
|
|
162
|
+
if (members.length === 0)
|
|
163
|
+
return `${exported}interface ${name} {}`;
|
|
164
|
+
return `${exported}interface ${name} {\n${members.join('\n')}\n}`;
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=typescript.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typescript.js","sourceRoot":"","sources":["../../src/projections/typescript.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AnyField, Fields } from '../field/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validation — the Field metadata IS the source of truth.
|
|
4
|
+
*
|
|
5
|
+
* Validation is a projection of one axis: `shape` (the value), a PURE predicate —
|
|
6
|
+
* it answers "is this value valid?" and never mutates the value. Null included:
|
|
7
|
+
* the shape's `[T,'null']` union is judged by the engine, there is no hand-rolled
|
|
8
|
+
* null branch. It never looks at `role` (persistence/domain) — a foreign key is
|
|
9
|
+
* just a string here. Converting a valid value (e.g. an ISO string → a `Date`)
|
|
10
|
+
* is a separate step, the `boundary` axis, applied after the predicate passes.
|
|
11
|
+
* Absence is judged by `lifecycle.create` — any create rule makes it legal and
|
|
12
|
+
* the field is OMITTED from the result, never filled: realising the rule
|
|
13
|
+
* (stamp 'now', apply { value }, generate) belongs to the storage adapter at
|
|
14
|
+
* the point of persistence, whose insert returns the complete row.
|
|
15
|
+
*/
|
|
16
|
+
export interface ValidationError {
|
|
17
|
+
path: string;
|
|
18
|
+
message: string;
|
|
19
|
+
}
|
|
20
|
+
export type ValidationResult<T> = {
|
|
21
|
+
success: true;
|
|
22
|
+
data: T;
|
|
23
|
+
} | {
|
|
24
|
+
success: false;
|
|
25
|
+
errors: ValidationError[];
|
|
26
|
+
};
|
|
27
|
+
export type Checked = {
|
|
28
|
+
value: unknown;
|
|
29
|
+
} | {
|
|
30
|
+
error: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Validate a present value (null included) against its field — pure, never mutates.
|
|
34
|
+
*
|
|
35
|
+
* Reads `shape` and nothing else, so it answers "is this a legal value?" without ever
|
|
36
|
+
* asking who is speaking. That is what makes it usable on the way OUT of the domain,
|
|
37
|
+
* where the client-only axes (`boundary`, `lifecycle`) do not apply.
|
|
38
|
+
*/
|
|
39
|
+
export declare function checkValue(field: AnyField, value: unknown): Checked;
|
|
40
|
+
export interface ValidateOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Patch mode (update). An absent field is OMITTED from the result — not filled,
|
|
43
|
+
* not required — so `validate({ name })` touches only `name`. Set by `partial()`.
|
|
44
|
+
* Distinguishes "absent → don't touch" (patch) from "absent → null" (create).
|
|
45
|
+
*/
|
|
46
|
+
patch?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export declare function validateFields(fields: Fields, input: unknown, pathPrefix?: string, opts?: ValidateOptions): ValidationResult<Record<string, unknown>>;
|
|
49
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/projections/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAS,MAAM,mBAAmB,CAAC;AAIjE;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAC1B;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GAC1B;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,eAAe,EAAE,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,OAAO,GAAG;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAmB7D;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAgCnE;AAED,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,EACd,UAAU,SAAK,EACf,IAAI,GAAE,eAAoB,GACzB,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CA8E3C"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { anatomy, boundaryOf, resolveBoundary } from '../field/index.js';
|
|
2
|
+
import { Validator } from '@cfworker/json-schema';
|
|
3
|
+
/**
|
|
4
|
+
* One validator per shape — the shape object IS the source JSON Schema (the closed
|
|
5
|
+
* grammar the helpers emit), handed straight to the engine. Keyed by the shape
|
|
6
|
+
* reference (stable: derivations copy field refs, `partial()` spreads the field but
|
|
7
|
+
* keeps the same `shape`), built once and reused so the parse cost is amortised.
|
|
8
|
+
*/
|
|
9
|
+
const validators = new WeakMap();
|
|
10
|
+
function validatorFor(shape) {
|
|
11
|
+
let v = validators.get(shape);
|
|
12
|
+
if (!v) {
|
|
13
|
+
v = new Validator(shape, '2020-12', true);
|
|
14
|
+
validators.set(shape, v);
|
|
15
|
+
}
|
|
16
|
+
return v;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Validate a present value (null included) against its field — pure, never mutates.
|
|
20
|
+
*
|
|
21
|
+
* Reads `shape` and nothing else, so it answers "is this a legal value?" without ever
|
|
22
|
+
* asking who is speaking. That is what makes it usable on the way OUT of the domain,
|
|
23
|
+
* where the client-only axes (`boundary`, `lifecycle`) do not apply.
|
|
24
|
+
*/
|
|
25
|
+
export function checkValue(field, value) {
|
|
26
|
+
const shape = field.shape;
|
|
27
|
+
// Relation-only field (`many`): no value shape, just an array of related rows.
|
|
28
|
+
if (!shape) {
|
|
29
|
+
if (field.role?.relation?.kind === 'many') {
|
|
30
|
+
return Array.isArray(value) ? { value } : { error: 'Expected an array' };
|
|
31
|
+
}
|
|
32
|
+
return { value };
|
|
33
|
+
}
|
|
34
|
+
// The pre-engine guards dispatch on the BASE type — `shape.type` itself may be
|
|
35
|
+
// the nullable union. They only short-circuit NON-null values: null always goes
|
|
36
|
+
// to the engine, whose union judges it (that is the whole nullability model).
|
|
37
|
+
const base = anatomy(shape).base;
|
|
38
|
+
if (value !== null) {
|
|
39
|
+
// Opaque JSON (`json()`): no nested shape → passes through unchecked.
|
|
40
|
+
// `json(Entity)` carries `properties` and falls through to the engine,
|
|
41
|
+
// which validates the nesting natively (it's plain JSON Schema).
|
|
42
|
+
if (base?.type === 'object' && !base.properties)
|
|
43
|
+
return { value };
|
|
44
|
+
// A date-time field also accepts a live Date — the domain value, not its JSON wire
|
|
45
|
+
// string — which the boundary then passes through. The engine validates JSON, so a
|
|
46
|
+
// Date (a non-JSON value) skips the schema check here; a date STRING is validated below.
|
|
47
|
+
if (base?.type === 'string' && base.format === 'date-time' && value instanceof Date) {
|
|
48
|
+
return Number.isNaN(value.getTime()) ? { error: 'Invalid date' } : { value };
|
|
49
|
+
}
|
|
50
|
+
// NaN is `typeof number` yet has no JSON form, so the JSON-centric engine accepts it —
|
|
51
|
+
// guard it here (same nature as the Date case: a JS value JSON can't represent).
|
|
52
|
+
if ((base?.type === 'number' || base?.type === 'integer') && typeof value === 'number' && Number.isNaN(value)) {
|
|
53
|
+
return { error: 'Expected a number' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const result = validatorFor(shape).validate(value);
|
|
57
|
+
return result.valid ? { value } : { error: result.errors[0]?.error ?? 'Invalid value' };
|
|
58
|
+
}
|
|
59
|
+
export function validateFields(fields, input, pathPrefix = '', opts = {}) {
|
|
60
|
+
if (typeof input !== 'object' || input === null) {
|
|
61
|
+
return { success: false, errors: [{ path: pathPrefix || '.', message: 'Expected an object' }] };
|
|
62
|
+
}
|
|
63
|
+
const data = input;
|
|
64
|
+
const errors = [];
|
|
65
|
+
const out = {};
|
|
66
|
+
// Refuse, never strip: a key outside the contract is an error, not a silent
|
|
67
|
+
// drop. The client derives from the same contract (a form cannot emit a
|
|
68
|
+
// foreign key), so a stranger is a bug or an attack — both deserve the 400.
|
|
69
|
+
// Stripping would also let the envelope diverge from GraphQL, which refuses
|
|
70
|
+
// unknown input keys by construction.
|
|
71
|
+
//
|
|
72
|
+
// A FACT is judged by this same rule, deliberately — the one place it was tempting to
|
|
73
|
+
// relax it. A subscriber's copy of a fact can be older than the sender's, so tolerating
|
|
74
|
+
// a stranger key would let a rolling deployment through; it would also mean a reader
|
|
75
|
+
// silently ignoring a field it was supposed to handle. If the judge refuses, that is
|
|
76
|
+
// the end of it: the sender re-syncs its readers before it ships.
|
|
77
|
+
for (const key of Object.keys(data)) {
|
|
78
|
+
if (!(key in fields)) {
|
|
79
|
+
errors.push({ path: pathPrefix ? `${pathPrefix}.${key}` : key, message: 'Unknown field' });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
83
|
+
const path = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
84
|
+
const value = data[key];
|
|
85
|
+
if (value === undefined) {
|
|
86
|
+
if (opts.patch)
|
|
87
|
+
continue; // patch: an unsent field is left untouched
|
|
88
|
+
// A read-only field is server-owned: its absence from a client input is
|
|
89
|
+
// never "Required" (same stance as OpenAPI readOnly+required).
|
|
90
|
+
if (boundaryOf(field).in === 'closed')
|
|
91
|
+
continue;
|
|
92
|
+
// Absence is answered by `lifecycle.create` — a key access on the normal
|
|
93
|
+
// form. The judge only asks "is there a rule?": any rule ('now',
|
|
94
|
+
// 'optional', { value }, { generate }) makes absence legal, and the field
|
|
95
|
+
// is omitted from the result. Realisation is the storage adapter's role.
|
|
96
|
+
if (field.lifecycle?.create !== undefined)
|
|
97
|
+
continue;
|
|
98
|
+
// No create rule: a `many` relation defaults to the empty collection
|
|
99
|
+
// (graph semantics, read on `role` — never a shared `{ value: [] }`).
|
|
100
|
+
if (field.role?.relation?.kind === 'many') {
|
|
101
|
+
out[key] = [];
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
errors.push({ path, message: 'Required' });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// A PRESENT value can be illegal by an axis other than shape:
|
|
108
|
+
// boundary `in: 'closed'` — a read-only field never crosses inbound;
|
|
109
|
+
// lifecycle `update: 'forbidden'` — re-supplying an immutable field in a patch.
|
|
110
|
+
if (boundaryOf(field).in === 'closed') {
|
|
111
|
+
errors.push({ path, message: 'Read-only' });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (opts.patch && field.lifecycle?.update === 'forbidden') {
|
|
115
|
+
errors.push({ path, message: 'Immutable' });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// The shape predicate judges the value — null included, via the `[T,'null']`
|
|
119
|
+
// union. Then the boundary's decode converts wire→domain; null skips decode
|
|
120
|
+
// (a legal null is already the domain value, there is nothing to convert).
|
|
121
|
+
const checked = checkValue(field, value);
|
|
122
|
+
if ('error' in checked) {
|
|
123
|
+
errors.push({ path, message: checked.error });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (checked.value === null) {
|
|
127
|
+
out[key] = null;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const decoded = resolveBoundary(field).decode(checked.value);
|
|
131
|
+
if ('error' in decoded)
|
|
132
|
+
errors.push({ path, message: decoded.error });
|
|
133
|
+
else
|
|
134
|
+
out[key] = decoded.value;
|
|
135
|
+
}
|
|
136
|
+
if (errors.length > 0)
|
|
137
|
+
return { success: false, errors };
|
|
138
|
+
return { success: true, data: out };
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/projections/validation.ts"],"names":[],"mappings":"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"}
|
package/dist/unique.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Fields } from './field/index.js';
|
|
2
|
+
import type { Hints } from './hints.js';
|
|
3
|
+
/**
|
|
4
|
+
* Field names that identify at most one row when taken together.
|
|
5
|
+
*
|
|
6
|
+
* `unique(field)` states a fact about one column; some facts are about a pair.
|
|
7
|
+
* "A book appears once in a list" is true of `(listId, docId)` and of neither
|
|
8
|
+
* alone, and there is no shape that can express it — judging one value never
|
|
9
|
+
* sees the other rows. So it is declared on the entity and realized by storage.
|
|
10
|
+
*
|
|
11
|
+
* Held here rather than in a handler because a handler cannot hold it: a check
|
|
12
|
+
* followed by a write is two round trips, and a concurrent request fits between
|
|
13
|
+
* them. The database is the only place where the promise is keepable.
|
|
14
|
+
*/
|
|
15
|
+
export type CompositeUnique<TFields extends Fields> = ReadonlyArray<ReadonlyArray<Extract<keyof TFields, string>>>;
|
|
16
|
+
/**
|
|
17
|
+
* What an entity declares about itself, beyond its fields — the 2nd argument of
|
|
18
|
+
* `entity()`.
|
|
19
|
+
*
|
|
20
|
+
* One object rather than a growing parameter list: a second fact about an entity
|
|
21
|
+
* adds a key here, never a positional argument.
|
|
22
|
+
*/
|
|
23
|
+
export interface EntityDeclarations<TFields extends Fields> {
|
|
24
|
+
/** Field groups that must be unique together. */
|
|
25
|
+
unique?: CompositeUnique<TFields>;
|
|
26
|
+
/** Per-consumer hints, keyed by registered adapter. See {@link Hints}. */
|
|
27
|
+
hints?: Hints<TFields>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Carry composite groups across a field-key transform — the entity-level twin of
|
|
31
|
+
* `deriveHints`.
|
|
32
|
+
*
|
|
33
|
+
* A group whose members did not all survive is dropped rather than narrowed: the
|
|
34
|
+
* pair `(listId, docId)` says nothing about `listId` alone, and keeping the
|
|
35
|
+
* remnant would silently state a stronger fact than the author ever wrote.
|
|
36
|
+
*/
|
|
37
|
+
export declare function deriveUnique(groups: CompositeUnique<Fields> | undefined, mapKey: (key: string) => string | undefined): CompositeUnique<Fields> | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Carry the projected groups across the same key transform — the field-level twin of
|
|
40
|
+
* {@link deriveUnique}, and it has to run with it: the group lives in TWO places once
|
|
41
|
+
* projected (the declaration and each member's role), so remapping one alone left
|
|
42
|
+
* `pick('id','listId')` with a role still claiming a pair whose other member was gone.
|
|
43
|
+
*
|
|
44
|
+
* Same rule as the declaration — a group that lost a member is dropped, not narrowed.
|
|
45
|
+
* The empty self-reference needs no remapping: it names no key, which is exactly why it
|
|
46
|
+
* survives a `rename()` for free.
|
|
47
|
+
*/
|
|
48
|
+
export declare function deriveUniqueRoles<TFields extends Fields>(fields: TFields, mapKey: (key: string) => string | undefined): TFields;
|
|
49
|
+
/**
|
|
50
|
+
* Project the entity's composite groups onto the role of every member — the single
|
|
51
|
+
* place the two ways of stating uniqueness meet.
|
|
52
|
+
*
|
|
53
|
+
* The author writes each fact where it lives: `unique(slug)` on the field it is about,
|
|
54
|
+
* `entity(fields, { unique: [['listId','docId']] })` on the entity, because a fact about
|
|
55
|
+
* a pair is held by neither field alone. Both then read as ONE normal form on the role
|
|
56
|
+
* axis, so every consumer — the DDL, the card, a foreign adapter — has a single shape to
|
|
57
|
+
* handle, and a field belonging to two constraints simply carries two member lists.
|
|
58
|
+
*
|
|
59
|
+
* The entity declaration stays the source (`getUnique()` keeps answering it); this is a
|
|
60
|
+
* projection of it, never a second place to edit. A named member group is spelled out
|
|
61
|
+
* here, while a lone `unique()` keeps its `[]` self-reference — resolved by whoever reads
|
|
62
|
+
* it, which is what makes `rename()` free.
|
|
63
|
+
*/
|
|
64
|
+
export declare function projectUniqueOntoFields<TFields extends Fields>(fields: TFields, groups: CompositeUnique<TFields> | undefined): TFields;
|
|
65
|
+
//# sourceMappingURL=unique.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unique.d.ts","sourceRoot":"","sources":["../src/unique.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,eAAe,CAAC,OAAO,SAAS,MAAM,IAAI,aAAa,CACjE,aAAa,CAAC,OAAO,CAAC,MAAM,OAAO,EAAE,MAAM,CAAC,CAAC,CAC9C,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB,CAAC,OAAO,SAAS,MAAM;IACxD,iDAAiD;IACjD,MAAM,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;IAClC,0EAA0E;IAC1E,KAAK,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,SAAS,EAC3C,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAC1C,eAAe,CAAC,MAAM,CAAC,GAAG,SAAS,CAQrC;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,SAAS,MAAM,EACtD,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAC1C,OAAO,CAkBT;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,SAAS,MAAM,EAC5D,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,GAAG,SAAS,GAC3C,OAAO,CAiBT"}
|
package/dist/unique.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carry composite groups across a field-key transform — the entity-level twin of
|
|
3
|
+
* `deriveHints`.
|
|
4
|
+
*
|
|
5
|
+
* A group whose members did not all survive is dropped rather than narrowed: the
|
|
6
|
+
* pair `(listId, docId)` says nothing about `listId` alone, and keeping the
|
|
7
|
+
* remnant would silently state a stronger fact than the author ever wrote.
|
|
8
|
+
*/
|
|
9
|
+
export function deriveUnique(groups, mapKey) {
|
|
10
|
+
if (!groups)
|
|
11
|
+
return undefined;
|
|
12
|
+
const carried = groups
|
|
13
|
+
.map((group) => group.map(mapKey))
|
|
14
|
+
.filter((group) => group.every((key) => key !== undefined));
|
|
15
|
+
return carried.length > 0 ? carried : undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Carry the projected groups across the same key transform — the field-level twin of
|
|
19
|
+
* {@link deriveUnique}, and it has to run with it: the group lives in TWO places once
|
|
20
|
+
* projected (the declaration and each member's role), so remapping one alone left
|
|
21
|
+
* `pick('id','listId')` with a role still claiming a pair whose other member was gone.
|
|
22
|
+
*
|
|
23
|
+
* Same rule as the declaration — a group that lost a member is dropped, not narrowed.
|
|
24
|
+
* The empty self-reference needs no remapping: it names no key, which is exactly why it
|
|
25
|
+
* survives a `rename()` for free.
|
|
26
|
+
*/
|
|
27
|
+
export function deriveUniqueRoles(fields, mapKey) {
|
|
28
|
+
const out = { ...fields };
|
|
29
|
+
for (const [key, field] of Object.entries(out)) {
|
|
30
|
+
const groups = field.role?.unique;
|
|
31
|
+
if (!groups?.length)
|
|
32
|
+
continue;
|
|
33
|
+
const carried = groups
|
|
34
|
+
.map((group) => (group.length === 0 ? [] : group.map(mapKey)))
|
|
35
|
+
.filter((group) => group.every((member) => member !== undefined));
|
|
36
|
+
if (carried.length === groups.length)
|
|
37
|
+
continue; // nothing lost, keep the field as is
|
|
38
|
+
const { unique: _dropped, ...rest } = field.role;
|
|
39
|
+
out[key] = {
|
|
40
|
+
...field,
|
|
41
|
+
role: carried.length ? { ...rest, unique: carried } : rest,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Project the entity's composite groups onto the role of every member — the single
|
|
48
|
+
* place the two ways of stating uniqueness meet.
|
|
49
|
+
*
|
|
50
|
+
* The author writes each fact where it lives: `unique(slug)` on the field it is about,
|
|
51
|
+
* `entity(fields, { unique: [['listId','docId']] })` on the entity, because a fact about
|
|
52
|
+
* a pair is held by neither field alone. Both then read as ONE normal form on the role
|
|
53
|
+
* axis, so every consumer — the DDL, the card, a foreign adapter — has a single shape to
|
|
54
|
+
* handle, and a field belonging to two constraints simply carries two member lists.
|
|
55
|
+
*
|
|
56
|
+
* The entity declaration stays the source (`getUnique()` keeps answering it); this is a
|
|
57
|
+
* projection of it, never a second place to edit. A named member group is spelled out
|
|
58
|
+
* here, while a lone `unique()` keeps its `[]` self-reference — resolved by whoever reads
|
|
59
|
+
* it, which is what makes `rename()` free.
|
|
60
|
+
*/
|
|
61
|
+
export function projectUniqueOntoFields(fields, groups) {
|
|
62
|
+
if (!groups || groups.length === 0)
|
|
63
|
+
return fields;
|
|
64
|
+
const projected = { ...fields };
|
|
65
|
+
for (const group of groups) {
|
|
66
|
+
const members = [...group];
|
|
67
|
+
for (const key of members) {
|
|
68
|
+
const field = projected[key];
|
|
69
|
+
if (!field)
|
|
70
|
+
continue; // a group naming an absent field states nothing here
|
|
71
|
+
const already = field.role?.unique ?? [];
|
|
72
|
+
projected[key] = {
|
|
73
|
+
...field,
|
|
74
|
+
role: { ...field.role, unique: [...already, members] },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return projected;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=unique.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unique.js","sourceRoot":"","sources":["../src/unique.ts"],"names":[],"mappings":"AAiCA;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,MAA2C,EAC3C,MAA2C;IAE3C,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAE9B,MAAM,OAAO,GAAG,MAAM;SACnB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;SACjC,MAAM,CAAC,CAAC,KAAK,EAAqB,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC;IAEjF,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AAClD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAe,EACf,MAA2C;IAE3C,MAAM,GAAG,GAAG,EAAE,GAAG,MAAM,EAAY,CAAC;IACpC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/C,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC;QAClC,IAAI,CAAC,MAAM,EAAE,MAAM;YAAE,SAAS;QAE9B,MAAM,OAAO,GAAG,MAAM;aACnB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;aAC7D,MAAM,CAAC,CAAC,KAAK,EAAqB,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC;QAEvF,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM;YAAE,SAAS,CAAG,qCAAqC;QACvF,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC,IAAK,CAAC;QAClD,GAAG,CAAC,GAAG,CAAC,GAAG;YACT,GAAG,KAAK;YACR,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI;SAC3D,CAAC;IACJ,CAAC;IACD,OAAO,GAAc,CAAC;AACxB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAe,EACf,MAA4C;IAE5C,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC;IAElD,MAAM,SAAS,GAAG,EAAE,GAAG,MAAM,EAAY,CAAC;IAC1C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;QAC3B,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YAC1B,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;YAC7B,IAAI,CAAC,KAAK;gBAAE,SAAS,CAAG,qDAAqD;YAC7E,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,MAAM,IAAI,EAAE,CAAC;YACzC,SAAS,CAAC,GAAG,CAAC,GAAG;gBACf,GAAG,KAAK;gBACR,IAAI,EAAE,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,EAAE,OAAO,CAAC,EAAE;aACvD,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,SAAoB,CAAC;AAC9B,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Field } from '../field/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Auto-managed timestamp, stamped at creation — optional in `new X(input)`.
|
|
4
|
+
* Immutable by default: a creation timestamp re-supplied in a patch is an
|
|
5
|
+
* error (decided 2026-07-15).
|
|
6
|
+
*/
|
|
7
|
+
export declare function auto(): Field<Date, true>;
|
|
8
|
+
//# sourceMappingURL=auto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto.d.ts","sourceRoot":"","sources":["../../src/vocabulary/auto.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE5D;;;;GAIG;AACH,wBAAgB,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAKxC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createField } from '../field/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Auto-managed timestamp, stamped at creation — optional in `new X(input)`.
|
|
4
|
+
* Immutable by default: a creation timestamp re-supplied in a patch is an
|
|
5
|
+
* error (decided 2026-07-15).
|
|
6
|
+
*/
|
|
7
|
+
export function auto() {
|
|
8
|
+
return createField({
|
|
9
|
+
shape: { type: 'string', format: 'date-time' },
|
|
10
|
+
lifecycle: { create: 'now', update: 'forbidden' },
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=auto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto.js","sourceRoot":"","sources":["../../src/vocabulary/auto.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAc,MAAM,mBAAmB,CAAC;AAE5D;;;;GAIG;AACH,MAAM,UAAU,IAAI;IAClB,OAAO,WAAW,CAAa;QAC7B,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE;QAC9C,SAAS,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE;KAClD,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bool.d.ts","sourceRoot":"","sources":["../../src/vocabulary/bool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE5D,wBAAgB,IAAI,CAAC,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAAC,OAAO,CAAC,CAMvF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createField } from '../field/index.js';
|
|
2
|
+
export function bool(opts) {
|
|
3
|
+
return createField({
|
|
4
|
+
shape: { type: 'boolean' },
|
|
5
|
+
lifecycle: opts?.default !== undefined ? { create: { value: opts.default } } : undefined,
|
|
6
|
+
meta: opts?.description !== undefined ? { description: opts.description } : undefined,
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=bool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bool.js","sourceRoot":"","sources":["../../src/vocabulary/bool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAc,MAAM,mBAAmB,CAAC;AAE5D,MAAM,UAAU,IAAI,CAAC,IAAkD;IACrE,OAAO,WAAW,CAAU;QAC1B,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;QAC1B,SAAS,EAAE,IAAI,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS;QACxF,IAAI,EAAE,IAAI,EAAE,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS;KACtF,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date.d.ts","sourceRoot":"","sources":["../../src/vocabulary/date.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE5D,wBAAgB,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,CAElC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date.js","sourceRoot":"","sources":["../../src/vocabulary/date.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAc,MAAM,mBAAmB,CAAC;AAE5D,MAAM,UAAU,IAAI;IAClB,OAAO,WAAW,CAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;AAC/E,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type TextOptions } from './text.js';
|
|
2
|
+
import type { Field } from '../field/index.js';
|
|
3
|
+
/** A string holding an email address — `text({ format: 'email' })`, named. */
|
|
4
|
+
export declare function email(opts?: Omit<TextOptions, 'format'>): Field<string>;
|
|
5
|
+
//# sourceMappingURL=email.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../../src/vocabulary/email.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,8EAA8E;AAC9E,wBAAgB,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAEvE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.js","sourceRoot":"","sources":["../../src/vocabulary/email.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAoB,MAAM,WAAW,CAAC;AAGnD,8EAA8E;AAC9E,MAAM,UAAU,KAAK,CAAC,IAAkC;IACtD,OAAO,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;AAC5C,CAAC"}
|