@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.
Files changed (167) hide show
  1. package/README.md +4 -2
  2. package/dist/Schema.d.ts +1 -1
  3. package/dist/Schema.d.ts.map +1 -1
  4. package/dist/Schema.js +4 -3
  5. package/dist/Schema.js.map +1 -1
  6. package/dist/SchemaConstructor.d.ts +3 -4
  7. package/dist/SchemaConstructor.d.ts.map +1 -1
  8. package/dist/SchemaDefinition.d.ts.map +1 -1
  9. package/dist/SchemaDefinition.js +2 -1
  10. package/dist/SchemaDefinition.js.map +1 -1
  11. package/dist/SchemaError.d.ts +4 -0
  12. package/dist/SchemaError.d.ts.map +1 -0
  13. package/dist/SchemaError.js +7 -0
  14. package/dist/SchemaError.js.map +1 -0
  15. package/dist/SchemaView.d.ts +1 -1
  16. package/dist/SchemaView.d.ts.map +1 -1
  17. package/dist/axis/boundary/Boundary.d.ts.map +1 -1
  18. package/dist/axis/boundary/Boundary.js +3 -2
  19. package/dist/axis/boundary/Boundary.js.map +1 -1
  20. package/dist/axis/boundary/Decoder.d.ts +2 -5
  21. package/dist/axis/boundary/Decoder.d.ts.map +1 -1
  22. package/dist/axis/boundary/Decoder.js +2 -2
  23. package/dist/axis/boundary/Decoder.js.map +1 -1
  24. package/dist/axis/lifecycle/Clock.d.ts +4 -12
  25. package/dist/axis/lifecycle/Clock.d.ts.map +1 -1
  26. package/dist/axis/lifecycle/Clock.js +7 -13
  27. package/dist/axis/lifecycle/Clock.js.map +1 -1
  28. package/dist/axis/lifecycle/Generators.d.ts +5 -5
  29. package/dist/axis/lifecycle/Generators.d.ts.map +1 -1
  30. package/dist/axis/lifecycle/Generators.js +5 -15
  31. package/dist/axis/lifecycle/Generators.js.map +1 -1
  32. package/dist/axis/lifecycle/Lifecycle.d.ts +3 -3
  33. package/dist/axis/lifecycle/Lifecycle.d.ts.map +1 -1
  34. package/dist/axis/lifecycle/Lifecycle.js +11 -7
  35. package/dist/axis/lifecycle/Lifecycle.js.map +1 -1
  36. package/dist/axis/lifecycle/apply.d.ts.map +1 -1
  37. package/dist/axis/lifecycle/apply.js +3 -19
  38. package/dist/axis/lifecycle/apply.js.map +1 -1
  39. package/dist/axis/role/Relation.d.ts +4 -2
  40. package/dist/axis/role/Relation.d.ts.map +1 -1
  41. package/dist/axis/role/Relation.js +2 -2
  42. package/dist/axis/role/Relation.js.map +1 -1
  43. package/dist/axis/shape/Shape.d.ts +2 -2
  44. package/dist/axis/shape/Shape.d.ts.map +1 -1
  45. package/dist/axis/shape/Shape.js +2 -2
  46. package/dist/axis/shape/Shape.js.map +1 -1
  47. package/dist/entity/EntityAdapterSet.d.ts.map +1 -1
  48. package/dist/entity/EntityAdapterSet.js +3 -2
  49. package/dist/entity/EntityAdapterSet.js.map +1 -1
  50. package/dist/entity.d.ts +0 -1
  51. package/dist/entity.d.ts.map +1 -1
  52. package/dist/entity.js +0 -1
  53. package/dist/entity.js.map +1 -1
  54. package/dist/field/Field.d.ts.map +1 -1
  55. package/dist/field/Field.js +5 -4
  56. package/dist/field/Field.js.map +1 -1
  57. package/dist/field/FieldSet.d.ts.map +1 -1
  58. package/dist/field/FieldSet.js +5 -3
  59. package/dist/field/FieldSet.js.map +1 -1
  60. package/dist/field/Values.d.ts +6 -0
  61. package/dist/field/Values.d.ts.map +1 -0
  62. package/dist/field/Values.js.map +1 -0
  63. package/dist/index.d.ts +3 -0
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +2 -0
  66. package/dist/index.js.map +1 -1
  67. package/dist/lib/Registry.d.ts +1 -2
  68. package/dist/lib/Registry.d.ts.map +1 -1
  69. package/dist/lib/Registry.js +4 -4
  70. package/dist/lib/Registry.js.map +1 -1
  71. package/dist/lib/Verdict.d.ts +12 -0
  72. package/dist/lib/Verdict.d.ts.map +1 -0
  73. package/dist/lib/Verdict.js +2 -0
  74. package/dist/lib/Verdict.js.map +1 -0
  75. package/dist/projection/Cases.js +52 -54
  76. package/dist/projection/Cases.js.map +1 -1
  77. package/dist/projection/card/Bundle.d.ts.map +1 -1
  78. package/dist/projection/card/Bundle.js +2 -1
  79. package/dist/projection/card/Bundle.js.map +1 -1
  80. package/dist/projection/card/Card.d.ts +1 -1
  81. package/dist/projection/card/Card.d.ts.map +1 -1
  82. package/dist/projection/card/Card.js +7 -2
  83. package/dist/projection/card/Card.js.map +1 -1
  84. package/dist/projection/card/Diff.d.ts.map +1 -1
  85. package/dist/projection/card/Diff.js +34 -37
  86. package/dist/projection/card/Diff.js.map +1 -1
  87. package/dist/projection/card/admission.d.ts.map +1 -1
  88. package/dist/projection/card/admission.js +2 -1
  89. package/dist/projection/card/admission.js.map +1 -1
  90. package/dist/validator/AdapterFieldValidator.d.ts.map +1 -1
  91. package/dist/validator/AdapterFieldValidator.js +3 -2
  92. package/dist/validator/AdapterFieldValidator.js.map +1 -1
  93. package/dist/validator/FieldValueValidator.d.ts +3 -3
  94. package/dist/validator/FieldValueValidator.d.ts.map +1 -1
  95. package/dist/validator/FieldValueValidator.js +12 -11
  96. package/dist/validator/FieldValueValidator.js.map +1 -1
  97. package/dist/validator/InputValidator.d.ts +3 -0
  98. package/dist/validator/InputValidator.d.ts.map +1 -1
  99. package/dist/validator/InputValidator.js +28 -31
  100. package/dist/validator/InputValidator.js.map +1 -1
  101. package/dist/vocabulary/primitive/list.d.ts.map +1 -1
  102. package/dist/vocabulary/primitive/list.js +2 -1
  103. package/dist/vocabulary/primitive/list.js.map +1 -1
  104. package/dist/vocabulary/primitive/ref.d.ts +6 -1
  105. package/dist/vocabulary/primitive/ref.d.ts.map +1 -1
  106. package/dist/vocabulary/primitive/ref.js +1 -1
  107. package/dist/vocabulary/primitive/ref.js.map +1 -1
  108. package/dist/vocabulary/vocabulary.d.ts.map +1 -1
  109. package/dist/vocabulary/vocabulary.js +2 -1
  110. package/dist/vocabulary/vocabulary.js.map +1 -1
  111. package/package.json +1 -1
  112. package/src/Schema.ts +6 -4
  113. package/src/SchemaConstructor.ts +4 -4
  114. package/src/SchemaDefinition.ts +2 -1
  115. package/src/SchemaError.ts +7 -0
  116. package/src/SchemaView.ts +1 -1
  117. package/src/axis/boundary/Boundary.ts +3 -2
  118. package/src/axis/boundary/Decoder.ts +4 -3
  119. package/src/axis/lifecycle/Clock.ts +9 -13
  120. package/src/axis/lifecycle/Generators.ts +6 -18
  121. package/src/axis/lifecycle/Lifecycle.ts +14 -9
  122. package/src/axis/lifecycle/apply.ts +2 -16
  123. package/src/axis/role/Relation.ts +6 -3
  124. package/src/axis/shape/Shape.ts +3 -3
  125. package/src/entity/EntityAdapterSet.ts +3 -2
  126. package/src/entity.ts +0 -1
  127. package/src/field/Field.ts +5 -4
  128. package/src/field/FieldSet.ts +5 -3
  129. package/src/{Values.ts → field/Values.ts} +2 -2
  130. package/src/index.ts +3 -0
  131. package/src/lib/Registry.ts +5 -4
  132. package/src/lib/Verdict.ts +10 -0
  133. package/src/projection/Cases.ts +61 -57
  134. package/src/projection/card/Bundle.ts +2 -1
  135. package/src/projection/card/Card.ts +12 -4
  136. package/src/projection/card/Diff.ts +42 -42
  137. package/src/projection/card/admission.ts +2 -1
  138. package/src/validator/AdapterFieldValidator.ts +3 -2
  139. package/src/validator/FieldValueValidator.ts +15 -14
  140. package/src/validator/InputValidator.ts +30 -32
  141. package/src/vocabulary/primitive/list.ts +2 -1
  142. package/src/vocabulary/primitive/ref.ts +7 -3
  143. package/src/vocabulary/vocabulary.ts +2 -1
  144. package/dist/PartialValues.d.ts +0 -4
  145. package/dist/PartialValues.d.ts.map +0 -1
  146. package/dist/PartialValues.js +0 -2
  147. package/dist/PartialValues.js.map +0 -1
  148. package/dist/Values.d.ts +0 -6
  149. package/dist/Values.d.ts.map +0 -1
  150. package/dist/Values.js.map +0 -1
  151. package/dist/axis/boundary/Boundaries.d.ts +0 -17
  152. package/dist/axis/boundary/Boundaries.d.ts.map +0 -1
  153. package/dist/axis/boundary/Boundaries.js +0 -25
  154. package/dist/axis/boundary/Boundaries.js.map +0 -1
  155. package/dist/lib/Checked.d.ts +0 -11
  156. package/dist/lib/Checked.d.ts.map +0 -1
  157. package/dist/lib/Checked.js +0 -2
  158. package/dist/lib/Checked.js.map +0 -1
  159. package/dist/projection/standard.d.ts +0 -14
  160. package/dist/projection/standard.d.ts.map +0 -1
  161. package/dist/projection/standard.js +0 -2
  162. package/dist/projection/standard.js.map +0 -1
  163. package/src/PartialValues.ts +0 -4
  164. package/src/axis/boundary/Boundaries.ts +0 -36
  165. package/src/lib/Checked.ts +0 -5
  166. package/src/projection/standard.ts +0 -13
  167. /package/dist/{Values.js → field/Values.js} +0 -0
@@ -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
- if (Boundary.of(field).readOnly) {
127
- cases.push({
128
- why: `${name} supplied although read-only`,
129
- input: withField(name, wrongTypeFor(field)),
130
- patch: false,
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
- if (Lifecycle.of(field).immutable && !Role.of(field).isPrimary) {
136
- cases.push({
137
- why: `${name} supplied on an update`,
138
- input: { [name]: valid[name] ?? wrongTypeFor(field) },
139
- patch: true,
140
- expect: { reject: name },
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
- if (name in valid && !isRef) {
145
- cases.push({
146
- why: `${name} of the wrong type`,
147
- input: withField(name, wrongTypeFor(field)),
148
- patch: false,
149
- expect: { reject: name },
150
- });
151
- for (const { why, value } of outOfBoundsFor(field))
152
- cases.push({
153
- why: `${name} ${why}`,
154
- input: withField(name, value),
155
- patch: false,
156
- expect: { reject: name },
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 => (path.length === 0 ? '.' : path[0]!);
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 Error(
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) ? property.type : property.type ? [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 Error(
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(property: FieldDescriptor | undefined, group: readonly string[]): void {
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 requiredBefore = new Set(was.required ?? []);
35
- const requiredAfter = new Set(is.required ?? []);
33
+ const wasRequired = new Set(was.required ?? []);
34
+ const isRequired = new Set(is.required ?? []);
36
35
 
37
- // Apply a declared rename first so subsequent differences use the new field name.
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 = nameAfter(field);
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
- if (now !== field)
49
- changes.push({ kind: 'renamed', from: field, to: now, field: target });
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
- const wasType = typesOf(descriptor);
52
- const isType = typesOf(target);
53
- if (!dequal(wasType, isType))
54
- changes.push({ kind: 'retyped', field: now, from: wasType, to: isType });
55
- else if (!dequal(boundsOf(descriptor), boundsOf(target))) {
56
- changes.push({ kind: 'reshaped', field: now, from: descriptor, to: target });
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
- const wasRequired = requiredBefore.has(field);
60
- const isRequired = requiredAfter.has(now);
61
- if (wasRequired !== isRequired) {
62
- changes.push({ kind: 'required', field: now, from: wasRequired, to: isRequired });
63
- }
64
- changes.push(...restated(now, descriptor['x-fougere'], target['x-fougere']));
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 claimed = new Set(Object.values(renamed));
68
- const added = Object.keys(after).filter(
69
- (field) => !(field in before) && !claimed.has(field),
70
- );
71
- for (const field of removed) {
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
- return { changes, ambiguous: candidates(removed, added, before, after) };
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 Error(`This card cannot be read: ${what}.\n ${fix}`);
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 Error(
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 Error(`${path}.${field}${nested}: ${failure?.error ?? 'does not match the format'}`);
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 { Checked } from '../lib/Checked.js';
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): Checked {
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()) ? { error: 'Invalid date' } : { value };
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 { error: 'Expected a number' };
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 { error: `String does not match format "${plan.formatName}".` };
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): Checked {
54
- const checked = this.validate(value);
55
- if ('error' in checked) return checked;
56
- if (checked.value === null) return { value: null };
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(checked.value);
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 Error(
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[]): Checked {
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 { error: 'Invalid value' };
99
+ if (!deepest) return { message: 'Invalid value' };
99
100
  const path = locationOf(deepest.instanceLocation);
100
101
 
101
- return path.length > 0 ? { error: deepest.error, path } : { error: deepest.error };
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.keys(data)) {
43
- if (!Object.hasOwn(this.fields, key)) {
44
- errors.push({ path: [key], message: InputRefusal.unknownField });
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
- for (const [key, field] of Object.entries(this.fields)) {
49
- const path = [key];
50
- const value = data[key];
50
+ if (errors.length > 0) return { success: false, errors };
51
51
 
52
- if (value === undefined) {
53
- if (this.options.patch) continue;
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
- if (Boundary.of(field).readOnly) {
64
- errors.push({ path, message: InputRefusal.readOnly });
65
- continue;
66
- }
67
- if (this.options.patch && Lifecycle.of(field).immutable) {
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
- const parsed = FieldValueValidator.of(field).parse(value);
73
- if ('error' in parsed) errors.push({ path: [...path, ...parsed.path ?? []], message: parsed.error });
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 (errors.length > 0) return { success: false, errors };
78
- return { success: true, data: row };
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 Error(
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
- cascade?: boolean;
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?.cascade) },
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 Error(
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
  );
@@ -1,4 +0,0 @@
1
- import type { Fields } from './field/Fields.js';
2
- import type { Values } from './Values.js';
3
- export type PartialValues<TFields extends Fields> = Partial<Values<TFields>>;
4
- //# sourceMappingURL=PartialValues.d.ts.map
@@ -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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=PartialValues.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PartialValues.js","sourceRoot":"","sources":["../src/PartialValues.ts"],"names":[],"mappings":""}
package/dist/Values.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import type { Field } from './field/Field.js';
2
- import type { Fields } from './field/Fields.js';
3
- export type Values<TFields extends Fields> = {
4
- [K in keyof TFields]: TFields[K] extends Field<infer T> ? T : never;
5
- };
6
- //# sourceMappingURL=Values.d.ts.map
@@ -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"}
@@ -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"}