@fougere/schema 0.10.0-alpha.0 → 0.12.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/README.md +4 -2
- package/dist/Schema.d.ts +1 -1
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +4 -3
- package/dist/Schema.js.map +1 -1
- package/dist/SchemaConstructor.d.ts +3 -4
- package/dist/SchemaConstructor.d.ts.map +1 -1
- package/dist/SchemaDefinition.d.ts.map +1 -1
- package/dist/SchemaDefinition.js +2 -1
- package/dist/SchemaDefinition.js.map +1 -1
- package/dist/SchemaError.d.ts +4 -0
- package/dist/SchemaError.d.ts.map +1 -0
- package/dist/SchemaError.js +7 -0
- package/dist/SchemaError.js.map +1 -0
- package/dist/SchemaView.d.ts +1 -1
- package/dist/SchemaView.d.ts.map +1 -1
- package/dist/axis/boundary/Boundary.d.ts.map +1 -1
- package/dist/axis/boundary/Boundary.js +3 -2
- package/dist/axis/boundary/Boundary.js.map +1 -1
- package/dist/axis/boundary/Decoder.d.ts +2 -5
- package/dist/axis/boundary/Decoder.d.ts.map +1 -1
- package/dist/axis/boundary/Decoder.js +2 -2
- package/dist/axis/boundary/Decoder.js.map +1 -1
- package/dist/axis/lifecycle/Clock.d.ts +4 -12
- package/dist/axis/lifecycle/Clock.d.ts.map +1 -1
- package/dist/axis/lifecycle/Clock.js +7 -13
- package/dist/axis/lifecycle/Clock.js.map +1 -1
- package/dist/axis/lifecycle/Generators.d.ts +5 -5
- package/dist/axis/lifecycle/Generators.d.ts.map +1 -1
- package/dist/axis/lifecycle/Generators.js +5 -15
- package/dist/axis/lifecycle/Generators.js.map +1 -1
- package/dist/axis/lifecycle/Lifecycle.d.ts +3 -3
- package/dist/axis/lifecycle/Lifecycle.d.ts.map +1 -1
- package/dist/axis/lifecycle/Lifecycle.js +11 -7
- package/dist/axis/lifecycle/Lifecycle.js.map +1 -1
- package/dist/axis/lifecycle/apply.d.ts.map +1 -1
- package/dist/axis/lifecycle/apply.js +3 -19
- package/dist/axis/lifecycle/apply.js.map +1 -1
- package/dist/axis/role/Relation.d.ts +4 -2
- package/dist/axis/role/Relation.d.ts.map +1 -1
- package/dist/axis/role/Relation.js +2 -2
- package/dist/axis/role/Relation.js.map +1 -1
- package/dist/axis/shape/Shape.d.ts +2 -2
- package/dist/axis/shape/Shape.d.ts.map +1 -1
- package/dist/axis/shape/Shape.js +2 -2
- package/dist/axis/shape/Shape.js.map +1 -1
- package/dist/entity/EntityAdapterSet.d.ts.map +1 -1
- package/dist/entity/EntityAdapterSet.js +3 -2
- package/dist/entity/EntityAdapterSet.js.map +1 -1
- package/dist/entity.d.ts +0 -1
- package/dist/entity.d.ts.map +1 -1
- package/dist/entity.js +0 -1
- package/dist/entity.js.map +1 -1
- package/dist/field/Field.d.ts.map +1 -1
- package/dist/field/Field.js +5 -4
- package/dist/field/Field.js.map +1 -1
- package/dist/field/FieldSet.d.ts.map +1 -1
- package/dist/field/FieldSet.js +5 -3
- package/dist/field/FieldSet.js.map +1 -1
- package/dist/field/Values.d.ts +6 -0
- package/dist/field/Values.d.ts.map +1 -0
- package/dist/field/Values.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/Registry.d.ts +1 -2
- package/dist/lib/Registry.d.ts.map +1 -1
- package/dist/lib/Registry.js +4 -4
- package/dist/lib/Registry.js.map +1 -1
- package/dist/lib/Verdict.d.ts +12 -0
- package/dist/lib/Verdict.d.ts.map +1 -0
- package/dist/lib/Verdict.js +2 -0
- package/dist/lib/Verdict.js.map +1 -0
- package/dist/projection/Cases.js +52 -54
- package/dist/projection/Cases.js.map +1 -1
- package/dist/projection/card/Bundle.d.ts.map +1 -1
- package/dist/projection/card/Bundle.js +2 -1
- package/dist/projection/card/Bundle.js.map +1 -1
- package/dist/projection/card/Card.d.ts +1 -1
- package/dist/projection/card/Card.d.ts.map +1 -1
- package/dist/projection/card/Card.js +7 -2
- package/dist/projection/card/Card.js.map +1 -1
- package/dist/projection/card/Diff.d.ts.map +1 -1
- package/dist/projection/card/Diff.js +34 -37
- package/dist/projection/card/Diff.js.map +1 -1
- package/dist/projection/card/admission.d.ts.map +1 -1
- package/dist/projection/card/admission.js +2 -1
- package/dist/projection/card/admission.js.map +1 -1
- package/dist/validator/AdapterFieldValidator.d.ts.map +1 -1
- package/dist/validator/AdapterFieldValidator.js +3 -2
- package/dist/validator/AdapterFieldValidator.js.map +1 -1
- package/dist/validator/FieldValueValidator.d.ts +3 -3
- package/dist/validator/FieldValueValidator.d.ts.map +1 -1
- package/dist/validator/FieldValueValidator.js +12 -11
- package/dist/validator/FieldValueValidator.js.map +1 -1
- package/dist/validator/InputValidator.d.ts +3 -0
- package/dist/validator/InputValidator.d.ts.map +1 -1
- package/dist/validator/InputValidator.js +28 -31
- package/dist/validator/InputValidator.js.map +1 -1
- package/dist/vocabulary/primitive/list.d.ts.map +1 -1
- package/dist/vocabulary/primitive/list.js +2 -1
- package/dist/vocabulary/primitive/list.js.map +1 -1
- package/dist/vocabulary/primitive/ref.d.ts +6 -1
- package/dist/vocabulary/primitive/ref.d.ts.map +1 -1
- package/dist/vocabulary/primitive/ref.js +1 -1
- package/dist/vocabulary/primitive/ref.js.map +1 -1
- package/dist/vocabulary/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary/vocabulary.js +2 -1
- package/dist/vocabulary/vocabulary.js.map +1 -1
- package/package.json +1 -1
- package/src/Schema.ts +6 -4
- package/src/SchemaConstructor.ts +4 -4
- package/src/SchemaDefinition.ts +2 -1
- package/src/SchemaError.ts +7 -0
- package/src/SchemaView.ts +1 -1
- package/src/axis/boundary/Boundary.ts +3 -2
- package/src/axis/boundary/Decoder.ts +4 -3
- package/src/axis/lifecycle/Clock.ts +9 -13
- package/src/axis/lifecycle/Generators.ts +6 -18
- package/src/axis/lifecycle/Lifecycle.ts +14 -9
- package/src/axis/lifecycle/apply.ts +2 -16
- package/src/axis/role/Relation.ts +6 -3
- package/src/axis/shape/Shape.ts +3 -3
- package/src/entity/EntityAdapterSet.ts +3 -2
- package/src/entity.ts +0 -1
- package/src/field/Field.ts +5 -4
- package/src/field/FieldSet.ts +5 -3
- package/src/{Values.ts → field/Values.ts} +2 -2
- package/src/index.ts +3 -0
- package/src/lib/Registry.ts +5 -4
- package/src/lib/Verdict.ts +10 -0
- package/src/projection/Cases.ts +61 -57
- package/src/projection/card/Bundle.ts +2 -1
- package/src/projection/card/Card.ts +12 -4
- package/src/projection/card/Diff.ts +42 -42
- package/src/projection/card/admission.ts +2 -1
- package/src/validator/AdapterFieldValidator.ts +3 -2
- package/src/validator/FieldValueValidator.ts +15 -14
- package/src/validator/InputValidator.ts +30 -32
- package/src/vocabulary/primitive/list.ts +2 -1
- package/src/vocabulary/primitive/ref.ts +7 -3
- package/src/vocabulary/vocabulary.ts +2 -1
- package/dist/PartialValues.d.ts +0 -4
- package/dist/PartialValues.d.ts.map +0 -1
- package/dist/PartialValues.js +0 -2
- package/dist/PartialValues.js.map +0 -1
- package/dist/Values.d.ts +0 -6
- package/dist/Values.d.ts.map +0 -1
- package/dist/Values.js.map +0 -1
- package/dist/axis/boundary/Boundaries.d.ts +0 -17
- package/dist/axis/boundary/Boundaries.d.ts.map +0 -1
- package/dist/axis/boundary/Boundaries.js +0 -25
- package/dist/axis/boundary/Boundaries.js.map +0 -1
- package/dist/lib/Checked.d.ts +0 -11
- package/dist/lib/Checked.d.ts.map +0 -1
- package/dist/lib/Checked.js +0 -2
- package/dist/lib/Checked.js.map +0 -1
- package/dist/projection/standard.d.ts +0 -14
- package/dist/projection/standard.d.ts.map +0 -1
- package/dist/projection/standard.js +0 -2
- package/dist/projection/standard.js.map +0 -1
- package/src/PartialValues.ts +0 -4
- package/src/axis/boundary/Boundaries.ts +0 -36
- package/src/lib/Checked.ts +0 -5
- package/src/projection/standard.ts +0 -13
- /package/dist/{Values.js → field/Values.js} +0 -0
package/src/projection/Cases.ts
CHANGED
|
@@ -95,66 +95,70 @@ export class Cases {
|
|
|
95
95
|
function enumerate(entity: SchemaView, valid: Record<string, unknown>): ValidationCase[] {
|
|
96
96
|
const fields = entity.getFields();
|
|
97
97
|
const validator = InputValidator.of(fields);
|
|
98
|
+
|
|
99
|
+
return [
|
|
100
|
+
...aboutTheInput(valid),
|
|
101
|
+
...Object.entries(fields).flatMap(([name, field]) => aboutField(name, field as Field, valid, validator)),
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function aboutTheInput(valid: Record<string, unknown>): ValidationCase[] {
|
|
106
|
+
return [
|
|
107
|
+
{ why: 'a valid input', input: valid, patch: false, expect: 'accept' },
|
|
108
|
+
{
|
|
109
|
+
why: 'a key outside the contract',
|
|
110
|
+
input: { ...valid, __unknown__: 'x' },
|
|
111
|
+
patch: false,
|
|
112
|
+
expect: { reject: '__unknown__' },
|
|
113
|
+
},
|
|
114
|
+
{ why: 'not an object at all', input: 'a string', patch: false, expect: { reject: '.' } },
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function aboutField(
|
|
119
|
+
name: string,
|
|
120
|
+
field: Field,
|
|
121
|
+
valid: Record<string, unknown>,
|
|
122
|
+
validator: InputValidator,
|
|
123
|
+
): ValidationCase[] {
|
|
124
|
+
const withField = (value: unknown) => ({ ...valid, [name]: value });
|
|
98
125
|
const cases: ValidationCase[] = [];
|
|
99
|
-
const withField = (name: string, value: unknown) => ({ ...valid, [name]: value });
|
|
100
|
-
|
|
101
|
-
cases.push({ why: 'a valid input', input: valid, patch: false, expect: 'accept' });
|
|
102
|
-
cases.push({
|
|
103
|
-
why: 'a key outside the contract',
|
|
104
|
-
input: { ...valid, __unknown__: 'x' },
|
|
105
|
-
patch: false,
|
|
106
|
-
expect: { reject: '__unknown__' },
|
|
107
|
-
});
|
|
108
|
-
cases.push({
|
|
109
|
-
why: 'not an object at all',
|
|
110
|
-
input: 'a string',
|
|
111
|
-
patch: false,
|
|
112
|
-
expect: { reject: '.' },
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
for (const [name, field] of Object.entries(fields) as [string, Field][]) {
|
|
116
|
-
// A reference names a row that must exist; the caller supplied its id and we do not
|
|
117
|
-
// get to invent a second one, so the only case we can state about it is the bound one.
|
|
118
|
-
const isRef = Role.of(field).isReference;
|
|
119
|
-
|
|
120
|
-
if (validator.onAbsent(field) === null && name in valid) {
|
|
121
|
-
const input = { ...valid };
|
|
122
|
-
delete input[name];
|
|
123
|
-
cases.push({ why: `${name} absent`, input, patch: false, expect: { reject: name } });
|
|
124
|
-
}
|
|
125
126
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
expect: { reject: name },
|
|
132
|
-
});
|
|
133
|
-
}
|
|
127
|
+
if (validator.onAbsent(field) === null && name in valid) {
|
|
128
|
+
const input = { ...valid };
|
|
129
|
+
delete input[name];
|
|
130
|
+
cases.push({ why: `${name} absent`, input, patch: false, expect: { reject: name } });
|
|
131
|
+
}
|
|
134
132
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
133
|
+
if (Boundary.of(field).readOnly) {
|
|
134
|
+
cases.push({
|
|
135
|
+
why: `${name} supplied although read-only`,
|
|
136
|
+
input: withField(wrongTypeFor(field)),
|
|
137
|
+
patch: false,
|
|
138
|
+
expect: { reject: name },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
143
141
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
142
|
+
if (Lifecycle.of(field).immutable && !Role.of(field).isPrimary) {
|
|
143
|
+
cases.push({
|
|
144
|
+
why: `${name} supplied on an update`,
|
|
145
|
+
input: { [name]: valid[name] ?? wrongTypeFor(field) },
|
|
146
|
+
patch: true,
|
|
147
|
+
expect: { reject: name },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// A reference names a row that must exist; the caller supplied its id and we do not
|
|
152
|
+
// get to invent a second one, so the only case we can state about it is the bound one.
|
|
153
|
+
if (name in valid && !Role.of(field).isReference) {
|
|
154
|
+
cases.push({
|
|
155
|
+
why: `${name} of the wrong type`,
|
|
156
|
+
input: withField(wrongTypeFor(field)),
|
|
157
|
+
patch: false,
|
|
158
|
+
expect: { reject: name },
|
|
159
|
+
});
|
|
160
|
+
for (const { why, value } of outOfBoundsFor(field)) {
|
|
161
|
+
cases.push({ why: `${name} ${why}`, input: withField(value), patch: false, expect: { reject: name } });
|
|
158
162
|
}
|
|
159
163
|
}
|
|
160
164
|
|
|
@@ -162,4 +166,4 @@ function enumerate(entity: SchemaView, valid: Record<string, unknown>): Validati
|
|
|
162
166
|
}
|
|
163
167
|
|
|
164
168
|
/** What a case names: the input itself, or the field the refusal lands on. */
|
|
165
|
-
const rejected = (path: readonly string[]): string =>
|
|
169
|
+
const rejected = (path: readonly string[]): string => path[0] ?? '.';
|
|
@@ -8,6 +8,7 @@ import type { SchemaDescriptor } from './SchemaDescriptor.js';
|
|
|
8
8
|
import type { Diff } from './Diff.js';
|
|
9
9
|
import type { SetDiff } from './SetDiff.js';
|
|
10
10
|
import type { SetDiffOptions } from './SetDiffOptions.js';
|
|
11
|
+
import { SchemaError } from '../../SchemaError.js';
|
|
11
12
|
|
|
12
13
|
type SchemaSet = Record<string, SchemaView> | SchemaView[];
|
|
13
14
|
|
|
@@ -26,7 +27,7 @@ export class Bundle {
|
|
|
26
27
|
const key = lowerFirst(entry.name);
|
|
27
28
|
const previous = claimedBy.get(key);
|
|
28
29
|
if (previous !== undefined) {
|
|
29
|
-
throw new
|
|
30
|
+
throw new SchemaError(
|
|
30
31
|
`Schemas '${previous}' and '${entry.name}' both claim bundle key '${key}'. `
|
|
31
32
|
+ 'Each schema in a bundle must have a distinct registration key.',
|
|
32
33
|
);
|
|
@@ -8,7 +8,7 @@ import { InputValidator } from '../../validator/InputValidator.js';
|
|
|
8
8
|
import { Schema } from '../../Schema.js';
|
|
9
9
|
import { type SchemaConstructor } from '../../SchemaConstructor.js';
|
|
10
10
|
import type { SchemaView } from '../../SchemaView.js';
|
|
11
|
-
import type { Values } from '../../Values.js';
|
|
11
|
+
import type { Values } from '../../field/Values.js';
|
|
12
12
|
import { admitPatterns, refuse } from './admission.js';
|
|
13
13
|
import type { DerivedFrom } from './DerivedFrom.js';
|
|
14
14
|
import type { FieldDescriptor } from './FieldDescriptor.js';
|
|
@@ -16,6 +16,7 @@ import type { FieldExtension } from './FieldExtension.js';
|
|
|
16
16
|
import type { SchemaDescriptor } from './SchemaDescriptor.js';
|
|
17
17
|
import { compare, type Diff } from './Diff.js';
|
|
18
18
|
import { type DiffOptions } from './DiffOptions.js';
|
|
19
|
+
import { SchemaError } from '../../SchemaError.js';
|
|
19
20
|
|
|
20
21
|
type FieldsOf<T> = { [K in keyof T]-?: Field<T[K]> };
|
|
21
22
|
|
|
@@ -165,7 +166,11 @@ function originOf(schema: SchemaView): DerivedFrom | undefined {
|
|
|
165
166
|
* `{ type: 'string', maxLength: 200 }` → the same shape `text({ max: 200 })` states
|
|
166
167
|
*/
|
|
167
168
|
function reconstructShape(property: FieldDescriptor): Field['shape'] | undefined {
|
|
168
|
-
const types = Array.isArray(property.type)
|
|
169
|
+
const types = Array.isArray(property.type)
|
|
170
|
+
? property.type
|
|
171
|
+
: property.type
|
|
172
|
+
? [property.type]
|
|
173
|
+
: [];
|
|
169
174
|
if (!types.some((type) => type !== 'null')) return undefined;
|
|
170
175
|
|
|
171
176
|
// `describeField` writes the shape whole, so it is read whole: a list of keywords here
|
|
@@ -188,7 +193,7 @@ function reconstructField(
|
|
|
188
193
|
): Field {
|
|
189
194
|
const shape = reconstructShape(property);
|
|
190
195
|
if (!shape) {
|
|
191
|
-
throw new
|
|
196
|
+
throw new SchemaError(
|
|
192
197
|
`Field '${key}': the card carries no \`type\` for it, so there is no shape to rebuild. ` +
|
|
193
198
|
'A field always states one.',
|
|
194
199
|
);
|
|
@@ -215,7 +220,10 @@ function reconstructField(
|
|
|
215
220
|
* FR : écrit un groupe sur CHAQUE membre — un lecteur du fil ne voit qu'un champ.
|
|
216
221
|
* `carryGroup(properties.listId, ['listId', 'docId'])` → the pair lands under its `role`
|
|
217
222
|
*/
|
|
218
|
-
function carryGroup(
|
|
223
|
+
function carryGroup(
|
|
224
|
+
property: FieldDescriptor | undefined,
|
|
225
|
+
group: readonly string[],
|
|
226
|
+
): void {
|
|
219
227
|
if (!property) return;
|
|
220
228
|
const extension = (property['x-fougere'] ??= {}) as { role?: { unique?: string[][] } };
|
|
221
229
|
const role = (extension.role ??= {});
|
|
@@ -27,65 +27,65 @@ export function compare(
|
|
|
27
27
|
is: SchemaDescriptor,
|
|
28
28
|
options: DiffOptions = {},
|
|
29
29
|
): Diff {
|
|
30
|
-
const changes: Change[] = [];
|
|
31
30
|
const renamed = options.renamed ?? {};
|
|
32
31
|
const before = was.properties ?? {};
|
|
33
32
|
const after = is.properties ?? {};
|
|
34
|
-
const
|
|
35
|
-
const
|
|
33
|
+
const wasRequired = new Set(was.required ?? []);
|
|
34
|
+
const isRequired = new Set(is.required ?? []);
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
const nameAfter = (field: string): string => renamed[field] ?? field;
|
|
36
|
+
const kept: Change[] = [];
|
|
39
37
|
const removed: string[] = [];
|
|
38
|
+
// A declared rename first, so every difference below is read under the new name.
|
|
40
39
|
for (const [field, descriptor] of Object.entries(before)) {
|
|
41
|
-
const now =
|
|
40
|
+
const now = renamed[field] ?? field;
|
|
42
41
|
const target = after[now];
|
|
43
42
|
if (target === undefined) {
|
|
44
43
|
removed.push(field);
|
|
45
44
|
continue;
|
|
46
45
|
}
|
|
46
|
+
kept.push(...movedIn(field, now, descriptor, target, wasRequired.has(field), isRequired.has(now)));
|
|
47
|
+
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
const claimed = new Set(Object.values(renamed));
|
|
50
|
+
const added = Object.keys(after).filter((field) => !(field in before) && !claimed.has(field));
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
return {
|
|
53
|
+
changes: [
|
|
54
|
+
...kept,
|
|
55
|
+
...removed.map((field): Change => ({
|
|
56
|
+
kind: 'removed', field, from: before[field]!, required: wasRequired.has(field),
|
|
57
|
+
})),
|
|
58
|
+
...added.map((field): Change => ({
|
|
59
|
+
kind: 'added', field, to: after[field]!, required: isRequired.has(field),
|
|
60
|
+
})),
|
|
61
|
+
],
|
|
62
|
+
ambiguous: candidates(removed, added, before, after),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
/** What one field that survived the two descriptions says about itself now. */
|
|
67
|
+
function movedIn(
|
|
68
|
+
field: string,
|
|
69
|
+
now: string,
|
|
70
|
+
before: FieldDescriptor,
|
|
71
|
+
after: FieldDescriptor,
|
|
72
|
+
was: boolean,
|
|
73
|
+
is: boolean,
|
|
74
|
+
): Change[] {
|
|
75
|
+
const changes: Change[] = [];
|
|
76
|
+
if (now !== field) changes.push({ kind: 'renamed', from: field, to: now, field: after });
|
|
66
77
|
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
changes.push({
|
|
73
|
-
kind: 'removed',
|
|
74
|
-
field,
|
|
75
|
-
from: before[field],
|
|
76
|
-
required: requiredBefore.has(field),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
for (const field of added) {
|
|
80
|
-
changes.push({
|
|
81
|
-
kind: 'added',
|
|
82
|
-
field,
|
|
83
|
-
to: after[field],
|
|
84
|
-
required: requiredAfter.has(field),
|
|
85
|
-
});
|
|
78
|
+
const wasType = typesOf(before);
|
|
79
|
+
const isType = typesOf(after);
|
|
80
|
+
if (!dequal(wasType, isType)) changes.push({ kind: 'retyped', field: now, from: wasType, to: isType });
|
|
81
|
+
else if (!dequal(boundsOf(before), boundsOf(after))) {
|
|
82
|
+
changes.push({ kind: 'reshaped', field: now, from: before, to: after });
|
|
86
83
|
}
|
|
87
84
|
|
|
88
|
-
|
|
85
|
+
if (was !== is) changes.push({ kind: 'required', field: now, from: was, to: is });
|
|
86
|
+
changes.push(...restated(now, before['x-fougere'], after['x-fougere']));
|
|
87
|
+
|
|
88
|
+
return changes;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
function restated(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ValidationError } from '../../lib/ValidationError.js';
|
|
2
2
|
import { Shapes } from '../../axis/shape/Shape.js';
|
|
3
3
|
import { dotted } from '../../lib/ValidationResult.js';
|
|
4
|
+
import { SchemaError } from '../../SchemaError.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Throws with what is wrong AND what to write: a card's author is in another process.
|
|
@@ -8,7 +9,7 @@ import { dotted } from '../../lib/ValidationResult.js';
|
|
|
8
9
|
* `refuse('role.relation.kind is "maybe"', 'Expected one of one, many.')`
|
|
9
10
|
*/
|
|
10
11
|
export function refuse(what: string, fix: string): never {
|
|
11
|
-
throw new
|
|
12
|
+
throw new SchemaError(`This card cannot be read: ${what}.\n ${fix}`);
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Validator } from '@cfworker/json-schema';
|
|
2
2
|
import type { Shape } from '../axis/shape/Shape.js';
|
|
3
3
|
import { isObject } from '../lib/utils.js';
|
|
4
|
+
import { SchemaError } from '../SchemaError.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The format is DATA, not a TypeScript interface: an interface is erased before a JS
|
|
@@ -19,7 +20,7 @@ export class AdapterFieldValidator {
|
|
|
19
20
|
if (entries === undefined) return;
|
|
20
21
|
|
|
21
22
|
if (!isObject(entries)) {
|
|
22
|
-
throw new
|
|
23
|
+
throw new SchemaError(
|
|
23
24
|
`${path}: expected an object keyed by field name, got ${typeof entries}.`,
|
|
24
25
|
);
|
|
25
26
|
}
|
|
@@ -34,7 +35,7 @@ export class AdapterFieldValidator {
|
|
|
34
35
|
const failure = units[units.length - 1] ?? verdict.errors[0];
|
|
35
36
|
const nested = (failure?.instanceLocation ?? '#').slice(1).replaceAll('/', '.');
|
|
36
37
|
|
|
37
|
-
throw new
|
|
38
|
+
throw new SchemaError(`${path}.${field}${nested}: ${failure?.error ?? 'does not match the format'}`);
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
}
|
|
@@ -5,8 +5,9 @@ import { Formats } from '../axis/shape/Formats.js';
|
|
|
5
5
|
import { type Shape } from '../axis/shape/Shape.js';
|
|
6
6
|
import { Shapes } from '../axis/shape/Shape.js';
|
|
7
7
|
import type { Field } from '../field/Field.js';
|
|
8
|
-
import type {
|
|
8
|
+
import type { Verdict } from '../lib/Verdict.js';
|
|
9
9
|
import type { OutputUnit } from '@cfworker/json-schema';
|
|
10
|
+
import { SchemaError } from '../SchemaError.js';
|
|
10
11
|
|
|
11
12
|
interface ShapePlan {
|
|
12
13
|
validator: Validator;
|
|
@@ -24,24 +25,24 @@ export class FieldValueValidator {
|
|
|
24
25
|
return new FieldValueValidator(field);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
validate(value: unknown):
|
|
28
|
+
validate(value: unknown): Verdict {
|
|
28
29
|
const shape = this.field.shape;
|
|
29
30
|
const type = Shapes.typeOf(shape);
|
|
30
31
|
const base = Shapes.of(shape).base;
|
|
31
32
|
if (value !== null) {
|
|
32
33
|
if (base?.type === 'object' && !base.properties) return { value };
|
|
33
34
|
if (type === 'date' && value instanceof Date) {
|
|
34
|
-
return Number.isNaN(value.getTime()) ? {
|
|
35
|
+
return Number.isNaN(value.getTime()) ? { message: 'Invalid date' } : { value };
|
|
35
36
|
}
|
|
36
37
|
if ((type === 'number' || type === 'integer') && typeof value === 'number' && Number.isNaN(value)) {
|
|
37
|
-
return {
|
|
38
|
+
return { message: 'Expected a number' };
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
const plan = FieldValueValidator.planFor(shape);
|
|
41
42
|
const result = plan.validator.validate(value);
|
|
42
43
|
if (!result.valid) return refusalOf(result.errors);
|
|
43
44
|
if (plan.custom && typeof value === 'string' && !plan.custom(value)) {
|
|
44
|
-
return {
|
|
45
|
+
return { message: `String does not match format "${plan.formatName}".` };
|
|
45
46
|
}
|
|
46
47
|
return { value };
|
|
47
48
|
}
|
|
@@ -50,12 +51,12 @@ export class FieldValueValidator {
|
|
|
50
51
|
* The value admitted, then handed back in the form the domain writes — an ISO string
|
|
51
52
|
* arrives as a `Date`. `null` passes untouched, and a refusal stops before the codec.
|
|
52
53
|
*/
|
|
53
|
-
parse(value: unknown):
|
|
54
|
-
const
|
|
55
|
-
if ('
|
|
56
|
-
if (
|
|
54
|
+
parse(value: unknown): Verdict {
|
|
55
|
+
const verdict = this.validate(value);
|
|
56
|
+
if ('message' in verdict) return verdict;
|
|
57
|
+
if (verdict.value === null) return { value: null };
|
|
57
58
|
|
|
58
|
-
return Boundary.of(this.field).decode(
|
|
59
|
+
return Boundary.of(this.field).decode(verdict.value);
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
private static planFor(shape: Shape): ShapePlan {
|
|
@@ -76,7 +77,7 @@ export class FieldValueValidator {
|
|
|
76
77
|
private static customFormatOf(name: string): FormatPredicate | undefined {
|
|
77
78
|
const custom = Formats.find(name);
|
|
78
79
|
if (!custom && !(name in engineFormats)) {
|
|
79
|
-
throw new
|
|
80
|
+
throw new SchemaError(
|
|
80
81
|
`Unknown format: '${name}'. Register it with Formats.register('${name}', …) — ` +
|
|
81
82
|
`the engine validates ${Object.keys(engineFormats).length} formats natively and this is not one of them.`,
|
|
82
83
|
);
|
|
@@ -90,15 +91,15 @@ export class FieldValueValidator {
|
|
|
90
91
|
* parent's `Property "street" does not match schema.` — true, and never the reason. The
|
|
91
92
|
* DEEPEST one is the reason, and it is the one that says where.
|
|
92
93
|
*/
|
|
93
|
-
function refusalOf(errors: readonly OutputUnit[]):
|
|
94
|
+
function refusalOf(errors: readonly OutputUnit[]): Verdict {
|
|
94
95
|
const deepest = errors.reduce<OutputUnit | undefined>(
|
|
95
96
|
(held, one) => (held && depthOf(held) >= depthOf(one) ? held : one),
|
|
96
97
|
undefined,
|
|
97
98
|
);
|
|
98
|
-
if (!deepest) return {
|
|
99
|
+
if (!deepest) return { message: 'Invalid value' };
|
|
99
100
|
const path = locationOf(deepest.instanceLocation);
|
|
100
101
|
|
|
101
|
-
return path.length > 0 ? {
|
|
102
|
+
return path.length > 0 ? { message: deepest.error, path } : { message: deepest.error };
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
const depthOf = (unit: OutputUnit): number => locationOf(unit.instanceLocation).length;
|
|
@@ -3,6 +3,7 @@ import { Lifecycle } from '../axis/lifecycle/Lifecycle.js';
|
|
|
3
3
|
import { Role } from '../axis/role/Role.js';
|
|
4
4
|
import type { Field } from '../field/Field.js';
|
|
5
5
|
import type { Fields } from '../field/Fields.js';
|
|
6
|
+
import type { Verdict } from '../lib/Verdict.js';
|
|
6
7
|
import type { ValidationError } from '../lib/ValidationError.js';
|
|
7
8
|
import type { ValidationResult } from '../lib/ValidationResult.js';
|
|
8
9
|
import { FieldValueValidator } from './FieldValueValidator.js';
|
|
@@ -36,45 +37,42 @@ export class InputValidator {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
const data = input as Record<string, unknown>;
|
|
39
|
-
const errors: ValidationError[] =
|
|
40
|
+
const errors: ValidationError[] = this.unknownIn(data);
|
|
40
41
|
const row: Record<string, unknown> = {};
|
|
41
42
|
|
|
42
|
-
for (const key of Object.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
43
|
+
for (const [key, field] of Object.entries(this.fields)) {
|
|
44
|
+
const verdict = this.admit(field, data[key]);
|
|
45
|
+
if (verdict === undefined) continue;
|
|
46
|
+
if ('message' in verdict) errors.push({ path: [key, ...(verdict.path ?? [])], message: verdict.message });
|
|
47
|
+
else row[key] = verdict.value;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
const path = [key];
|
|
50
|
-
const value = data[key];
|
|
50
|
+
if (errors.length > 0) return { success: false, errors };
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const absence = this.onAbsent(field);
|
|
55
|
-
if (absence === null) {
|
|
56
|
-
errors.push({ path, message: InputRefusal.required });
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (absence === 'empty-list') row[key] = [];
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
52
|
+
return { success: true, data: row };
|
|
53
|
+
}
|
|
62
54
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
errors.push({ path, message: InputRefusal.immutable });
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
55
|
+
private unknownIn(data: Record<string, unknown>): ValidationError[] {
|
|
56
|
+
return Object.keys(data)
|
|
57
|
+
.filter((key) => !Object.hasOwn(this.fields, key))
|
|
58
|
+
.map((key) => ({ path: [key], message: InputRefusal.unknownField }));
|
|
59
|
+
}
|
|
71
60
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
else row[key] = parsed.value;
|
|
75
|
-
}
|
|
61
|
+
private admit(field: Field, value: unknown): Verdict | undefined {
|
|
62
|
+
if (value === undefined) return this.whenAbsent(field);
|
|
76
63
|
|
|
77
|
-
if (
|
|
78
|
-
return {
|
|
64
|
+
if (Boundary.of(field).readOnly) return { message: InputRefusal.readOnly };
|
|
65
|
+
if (this.options.patch && Lifecycle.of(field).immutable) return { message: InputRefusal.immutable };
|
|
66
|
+
|
|
67
|
+
return FieldValueValidator.of(field).parse(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private whenAbsent(field: Field): Verdict | undefined {
|
|
71
|
+
if (this.options.patch) return undefined;
|
|
72
|
+
|
|
73
|
+
const absence = this.onAbsent(field);
|
|
74
|
+
if (absence === null) return { message: InputRefusal.required };
|
|
75
|
+
|
|
76
|
+
return absence === 'empty-list' ? { value: [] } : undefined;
|
|
79
77
|
}
|
|
80
78
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Field } from '../../field/Field.js';
|
|
2
|
+
import { SchemaError } from '../../SchemaError.js';
|
|
2
3
|
|
|
3
4
|
export interface ListOptions {
|
|
4
5
|
min?: number;
|
|
@@ -12,7 +13,7 @@ export interface ListOptions {
|
|
|
12
13
|
*/
|
|
13
14
|
export function list<T>(item: Field<T>, opts?: ListOptions): Field<T[]> {
|
|
14
15
|
if (item.role?.relation)
|
|
15
|
-
throw new
|
|
16
|
+
throw new SchemaError(
|
|
16
17
|
'list() takes a value field (text(), number()…) — a relation has no value shape',
|
|
17
18
|
);
|
|
18
19
|
return new Field<T[]>({
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { type EntityConstructor } from '../../axis/role/EntityConstructor.js';
|
|
2
|
-
import { Relation } from '../../axis/role/Relation.js';
|
|
2
|
+
import { Relation, type OnDelete } from '../../axis/role/Relation.js';
|
|
3
3
|
import { Field } from '../../field/Field.js';
|
|
4
4
|
|
|
5
5
|
export interface RefOptions {
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* What becomes of this row when the target's is deleted. `restrict` is the default because
|
|
8
|
+
* it is what a foreign key already does when nothing is stated.
|
|
9
|
+
*/
|
|
10
|
+
onDelete?: OnDelete;
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
export function ref<E extends EntityConstructor>(
|
|
@@ -12,6 +16,6 @@ export function ref<E extends EntityConstructor>(
|
|
|
12
16
|
): Field<string> {
|
|
13
17
|
return new Field<string>({
|
|
14
18
|
shape: { type: 'string' },
|
|
15
|
-
role: { relation: Relation.one(target, opts?.
|
|
19
|
+
role: { relation: Relation.one(target, opts?.onDelete) },
|
|
16
20
|
});
|
|
17
21
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Field } from '../field/Field.js';
|
|
2
2
|
import { EXTENSION_SLOTS } from '../axis/Axis.js';
|
|
3
3
|
import { dequal } from 'dequal';
|
|
4
|
+
import { SchemaError } from '../SchemaError.js';
|
|
4
5
|
|
|
5
6
|
const MEMBER_SLOTS = [...EXTENSION_SLOTS, 'meta'] as const;
|
|
6
7
|
|
|
@@ -38,7 +39,7 @@ function merge(name: string, field: Field, given: Partial<Field>): Partial<Field
|
|
|
38
39
|
for (const [member, value] of Object.entries(members)) {
|
|
39
40
|
const previous = already?.[member];
|
|
40
41
|
if (previous === undefined || dequal(previous, value)) continue;
|
|
41
|
-
throw new
|
|
42
|
+
throw new SchemaError(
|
|
42
43
|
`vocabulary: \`${name}\` states ${slot}.${member} = ${JSON.stringify(value)}, but the ` +
|
|
43
44
|
`field already states ${JSON.stringify(previous)}. Apply one or the other, not both.`,
|
|
44
45
|
);
|
package/dist/PartialValues.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"PartialValues.d.ts","sourceRoot":"","sources":["../src/PartialValues.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,MAAM,aAAa,CAAC,OAAO,SAAS,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC"}
|
package/dist/PartialValues.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"PartialValues.js","sourceRoot":"","sources":["../src/PartialValues.ts"],"names":[],"mappings":""}
|
package/dist/Values.d.ts
DELETED
package/dist/Values.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Values.d.ts","sourceRoot":"","sources":["../src/Values.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,MAAM,MAAM,CAAC,OAAO,SAAS,MAAM,IAAI;KAC1C,CAAC,IAAI,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK;CACpE,CAAC"}
|
package/dist/Values.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Values.js","sourceRoot":"","sources":["../src/Values.ts"],"names":[],"mappings":""}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Decoder } from './Decoder.js';
|
|
2
|
-
import type { Encoder } from './Encoder.js';
|
|
3
|
-
import type { BoundaryRules } from './BoundaryRules.js';
|
|
4
|
-
import { Registry } from '../../lib/Registry.js';
|
|
5
|
-
/**
|
|
6
|
-
* `decoders` for a value coming in, `encoders` for one going out, `aliases` for the word
|
|
7
|
-
* that names a pair. Three registries, so a codec is never half declared.
|
|
8
|
-
* FR : `decoders` à l'entrée, `encoders` à la sortie, `aliases` pour le mot qui nomme la paire.
|
|
9
|
-
* `Boundaries.aliases.register('isoDate', { in: { decode: 'isoDate' }, out: { encode: 'isoDate' } })`
|
|
10
|
-
* → a field saying `boundary: 'isoDate'` decodes to a `Date` and leaves as a string
|
|
11
|
-
*/
|
|
12
|
-
export declare const Boundaries: {
|
|
13
|
-
decoders: Registry<Decoder>;
|
|
14
|
-
encoders: Registry<Encoder>;
|
|
15
|
-
aliases: Registry<BoundaryRules>;
|
|
16
|
-
};
|
|
17
|
-
//# sourceMappingURL=Boundaries.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Boundaries.d.ts","sourceRoot":"","sources":["../../../src/axis/boundary/Boundaries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,UAAU;IACrB,QAAQ;IAIR,QAAQ;IAIR,OAAO;CACR,CAAC"}
|