@fougere/schema 0.11.0-alpha.0 → 0.12.1-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 (287) hide show
  1. package/dist/Schema.d.ts +1 -1
  2. package/dist/Schema.d.ts.map +1 -1
  3. package/dist/Schema.js +4 -3
  4. package/dist/Schema.js.map +1 -1
  5. package/dist/SchemaConstructor.d.ts +3 -4
  6. package/dist/SchemaConstructor.d.ts.map +1 -1
  7. package/dist/SchemaDefinition.d.ts.map +1 -1
  8. package/dist/SchemaDefinition.js +2 -1
  9. package/dist/SchemaDefinition.js.map +1 -1
  10. package/dist/SchemaError.d.ts +4 -0
  11. package/dist/SchemaError.d.ts.map +1 -0
  12. package/dist/SchemaError.js +7 -0
  13. package/dist/SchemaError.js.map +1 -0
  14. package/dist/SchemaView.d.ts +1 -1
  15. package/dist/SchemaView.d.ts.map +1 -1
  16. package/dist/axis/Axes.d.ts +16 -0
  17. package/dist/axis/Axes.d.ts.map +1 -0
  18. package/dist/axis/Axes.js +27 -0
  19. package/dist/axis/Axes.js.map +1 -0
  20. package/dist/axis/Axis.d.ts +5 -9
  21. package/dist/axis/Axis.d.ts.map +1 -1
  22. package/dist/axis/Axis.js +1 -10
  23. package/dist/axis/Axis.js.map +1 -1
  24. package/dist/axis/boundary/Boundary.d.ts +29 -3
  25. package/dist/axis/boundary/Boundary.d.ts.map +1 -1
  26. package/dist/axis/boundary/Boundary.js +15 -11
  27. package/dist/axis/boundary/Boundary.js.map +1 -1
  28. package/dist/axis/boundary/Decoder.d.ts +10 -7
  29. package/dist/axis/boundary/Decoder.d.ts.map +1 -1
  30. package/dist/axis/boundary/Decoder.js +2 -2
  31. package/dist/axis/boundary/Decoder.js.map +1 -1
  32. package/dist/axis/lifecycle/Generators.d.ts +5 -5
  33. package/dist/axis/lifecycle/Generators.d.ts.map +1 -1
  34. package/dist/axis/lifecycle/Generators.js +5 -15
  35. package/dist/axis/lifecycle/Generators.js.map +1 -1
  36. package/dist/axis/lifecycle/Lifecycle.d.ts +18 -6
  37. package/dist/axis/lifecycle/Lifecycle.d.ts.map +1 -1
  38. package/dist/axis/lifecycle/Lifecycle.js +29 -14
  39. package/dist/axis/lifecycle/Lifecycle.js.map +1 -1
  40. package/dist/axis/lifecycle/apply.d.ts.map +1 -1
  41. package/dist/axis/lifecycle/apply.js +5 -22
  42. package/dist/axis/lifecycle/apply.js.map +1 -1
  43. package/dist/axis/role/Role.d.ts +18 -6
  44. package/dist/axis/role/Role.d.ts.map +1 -1
  45. package/dist/axis/role/Role.js +30 -16
  46. package/dist/axis/role/Role.js.map +1 -1
  47. package/dist/axis/shape/Shape.d.ts +2 -2
  48. package/dist/axis/shape/Shape.d.ts.map +1 -1
  49. package/dist/axis/shape/Shape.js +2 -2
  50. package/dist/axis/shape/Shape.js.map +1 -1
  51. package/dist/axis/shape/{Formats.d.ts → StringFormats.d.ts} +3 -3
  52. package/dist/axis/shape/StringFormats.d.ts.map +1 -0
  53. package/dist/axis/shape/{Formats.js → StringFormats.js} +3 -3
  54. package/dist/axis/shape/StringFormats.js.map +1 -0
  55. package/dist/entity/EntityAdapterSet.d.ts.map +1 -1
  56. package/dist/entity/EntityAdapterSet.js +3 -2
  57. package/dist/entity/EntityAdapterSet.js.map +1 -1
  58. package/dist/entity.d.ts +0 -1
  59. package/dist/entity.d.ts.map +1 -1
  60. package/dist/entity.js +0 -1
  61. package/dist/entity.js.map +1 -1
  62. package/dist/field/Field.d.ts +11 -5
  63. package/dist/field/Field.d.ts.map +1 -1
  64. package/dist/field/Field.js +10 -7
  65. package/dist/field/Field.js.map +1 -1
  66. package/dist/field/FieldFormat.d.ts +9 -0
  67. package/dist/field/FieldFormat.d.ts.map +1 -0
  68. package/dist/field/FieldFormat.js +12 -0
  69. package/dist/field/FieldFormat.js.map +1 -0
  70. package/dist/field/FieldSet.d.ts.map +1 -1
  71. package/dist/field/FieldSet.js +5 -3
  72. package/dist/field/FieldSet.js.map +1 -1
  73. package/dist/field/FougereFieldAxes.d.ts +3 -0
  74. package/dist/field/FougereFieldAxes.d.ts.map +1 -0
  75. package/dist/field/FougereFieldAxes.js +2 -0
  76. package/dist/field/FougereFieldAxes.js.map +1 -0
  77. package/dist/field/Meta.d.ts +5 -3
  78. package/dist/field/Meta.d.ts.map +1 -1
  79. package/dist/field/Meta.js +2 -1
  80. package/dist/field/Meta.js.map +1 -1
  81. package/dist/field/Values.d.ts +6 -0
  82. package/dist/field/Values.d.ts.map +1 -0
  83. package/dist/field/Values.js.map +1 -0
  84. package/dist/index.d.ts +8 -3
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +5 -3
  87. package/dist/index.js.map +1 -1
  88. package/dist/lib/Format.d.ts +56 -0
  89. package/dist/lib/Format.d.ts.map +1 -0
  90. package/dist/lib/Format.js +95 -0
  91. package/dist/lib/Format.js.map +1 -0
  92. package/dist/lib/JsonSchema.d.ts +2 -0
  93. package/dist/lib/JsonSchema.d.ts.map +1 -0
  94. package/dist/lib/JsonSchema.js +2 -0
  95. package/dist/lib/JsonSchema.js.map +1 -0
  96. package/dist/lib/Registry.d.ts +5 -4
  97. package/dist/lib/Registry.d.ts.map +1 -1
  98. package/dist/lib/Registry.js +14 -8
  99. package/dist/lib/Registry.js.map +1 -1
  100. package/dist/lib/ValidationResult.d.ts +2 -0
  101. package/dist/lib/ValidationResult.d.ts.map +1 -1
  102. package/dist/lib/ValidationResult.js +6 -0
  103. package/dist/lib/ValidationResult.js.map +1 -1
  104. package/dist/lib/Verdict.d.ts +12 -0
  105. package/dist/lib/Verdict.d.ts.map +1 -0
  106. package/dist/lib/Verdict.js +2 -0
  107. package/dist/lib/Verdict.js.map +1 -0
  108. package/dist/projection/Cases.js +52 -54
  109. package/dist/projection/Cases.js.map +1 -1
  110. package/dist/projection/Visibility.d.ts +11 -0
  111. package/dist/projection/Visibility.d.ts.map +1 -1
  112. package/dist/projection/Visibility.js +22 -0
  113. package/dist/projection/Visibility.js.map +1 -1
  114. package/dist/projection/card/Bundle.d.ts.map +1 -1
  115. package/dist/projection/card/Bundle.js +2 -1
  116. package/dist/projection/card/Bundle.js.map +1 -1
  117. package/dist/projection/card/Card.d.ts +1 -1
  118. package/dist/projection/card/Card.d.ts.map +1 -1
  119. package/dist/projection/card/Card.js +19 -11
  120. package/dist/projection/card/Card.js.map +1 -1
  121. package/dist/projection/card/CardForm.d.ts +7 -0
  122. package/dist/projection/card/CardForm.d.ts.map +1 -0
  123. package/dist/projection/card/CardForm.js +2 -0
  124. package/dist/projection/card/CardForm.js.map +1 -0
  125. package/dist/projection/card/CardForms.d.ts +10 -0
  126. package/dist/projection/card/CardForms.d.ts.map +1 -0
  127. package/dist/projection/card/CardForms.js +12 -0
  128. package/dist/projection/card/CardForms.js.map +1 -0
  129. package/dist/projection/card/Change.d.ts +2 -2
  130. package/dist/projection/card/Change.d.ts.map +1 -1
  131. package/dist/projection/card/Diff.d.ts.map +1 -1
  132. package/dist/projection/card/Diff.js +36 -39
  133. package/dist/projection/card/Diff.js.map +1 -1
  134. package/dist/projection/card/FieldExtension.d.ts +4 -2
  135. package/dist/projection/card/FieldExtension.d.ts.map +1 -1
  136. package/dist/projection/card/RoleDescriptor.d.ts +9 -2
  137. package/dist/projection/card/RoleDescriptor.d.ts.map +1 -1
  138. package/dist/projection/card/RoleDescriptor.js +40 -1
  139. package/dist/projection/card/RoleDescriptor.js.map +1 -1
  140. package/dist/projection/card/admission.d.ts +0 -14
  141. package/dist/projection/card/admission.d.ts.map +1 -1
  142. package/dist/projection/card/admission.js +2 -21
  143. package/dist/projection/card/admission.js.map +1 -1
  144. package/dist/validator/FieldDeclarationValidator.d.ts.map +1 -1
  145. package/dist/validator/FieldDeclarationValidator.js +5 -17
  146. package/dist/validator/FieldDeclarationValidator.js.map +1 -1
  147. package/dist/validator/FieldValueValidator.d.ts +3 -3
  148. package/dist/validator/FieldValueValidator.d.ts.map +1 -1
  149. package/dist/validator/FieldValueValidator.js +18 -35
  150. package/dist/validator/FieldValueValidator.js.map +1 -1
  151. package/dist/validator/InputValidator.d.ts +3 -0
  152. package/dist/validator/InputValidator.d.ts.map +1 -1
  153. package/dist/validator/InputValidator.js +28 -31
  154. package/dist/validator/InputValidator.js.map +1 -1
  155. package/dist/validator/JsonSchemaValidator.d.ts +23 -0
  156. package/dist/validator/JsonSchemaValidator.d.ts.map +1 -0
  157. package/dist/validator/JsonSchemaValidator.js +113 -0
  158. package/dist/validator/JsonSchemaValidator.js.map +1 -0
  159. package/dist/vocabulary/primitive/list.d.ts.map +1 -1
  160. package/dist/vocabulary/primitive/list.js +2 -1
  161. package/dist/vocabulary/primitive/list.js.map +1 -1
  162. package/dist/vocabulary/vocabulary.d.ts.map +1 -1
  163. package/dist/vocabulary/vocabulary.js +9 -6
  164. package/dist/vocabulary/vocabulary.js.map +1 -1
  165. package/package.json +1 -1
  166. package/src/Schema.ts +6 -4
  167. package/src/SchemaConstructor.ts +4 -4
  168. package/src/SchemaDefinition.ts +2 -1
  169. package/src/SchemaError.ts +7 -0
  170. package/src/SchemaView.ts +1 -1
  171. package/src/axis/Axes.ts +34 -0
  172. package/src/axis/Axis.ts +5 -20
  173. package/src/axis/boundary/Boundary.ts +26 -13
  174. package/src/axis/boundary/Decoder.ts +5 -4
  175. package/src/axis/lifecycle/Generators.ts +6 -18
  176. package/src/axis/lifecycle/Lifecycle.ts +41 -19
  177. package/src/axis/lifecycle/apply.ts +13 -21
  178. package/src/axis/role/Role.ts +35 -19
  179. package/src/axis/shape/Shape.ts +3 -3
  180. package/src/axis/shape/{Formats.ts → StringFormats.ts} +2 -2
  181. package/src/entity/EntityAdapterSet.ts +3 -2
  182. package/src/entity.ts +0 -1
  183. package/src/field/Field.ts +23 -12
  184. package/src/field/FieldFormat.ts +18 -0
  185. package/src/field/FieldSet.ts +5 -3
  186. package/src/field/FougereFieldAxes.ts +2 -0
  187. package/src/field/Meta.ts +5 -3
  188. package/src/{Values.ts → field/Values.ts} +2 -2
  189. package/src/index.ts +8 -3
  190. package/src/lib/Format.ts +139 -0
  191. package/src/lib/JsonSchema.ts +1 -0
  192. package/src/lib/Registry.ts +18 -9
  193. package/src/lib/ValidationResult.ts +8 -0
  194. package/src/lib/Verdict.ts +10 -0
  195. package/src/projection/Cases.ts +61 -57
  196. package/src/projection/Visibility.ts +22 -0
  197. package/src/projection/card/Bundle.ts +2 -1
  198. package/src/projection/card/Card.ts +24 -13
  199. package/src/projection/card/CardForm.ts +8 -0
  200. package/src/projection/card/CardForms.ts +13 -0
  201. package/src/projection/card/Change.ts +2 -2
  202. package/src/projection/card/Diff.ts +44 -44
  203. package/src/projection/card/FieldExtension.ts +5 -2
  204. package/src/projection/card/RoleDescriptor.ts +48 -2
  205. package/src/projection/card/admission.ts +2 -34
  206. package/src/validator/FieldDeclarationValidator.ts +5 -17
  207. package/src/validator/FieldValueValidator.ts +22 -46
  208. package/src/validator/InputValidator.ts +30 -32
  209. package/src/validator/JsonSchemaValidator.ts +142 -0
  210. package/src/vocabulary/primitive/list.ts +2 -1
  211. package/src/vocabulary/vocabulary.ts +9 -6
  212. package/dist/PartialValues.d.ts +0 -4
  213. package/dist/PartialValues.d.ts.map +0 -1
  214. package/dist/PartialValues.js +0 -2
  215. package/dist/PartialValues.js.map +0 -1
  216. package/dist/Values.d.ts +0 -6
  217. package/dist/Values.d.ts.map +0 -1
  218. package/dist/Values.js.map +0 -1
  219. package/dist/axis/boundary/Boundaries.d.ts +0 -17
  220. package/dist/axis/boundary/Boundaries.d.ts.map +0 -1
  221. package/dist/axis/boundary/Boundaries.js +0 -25
  222. package/dist/axis/boundary/Boundaries.js.map +0 -1
  223. package/dist/axis/boundary/BoundaryAxis.d.ts +0 -4
  224. package/dist/axis/boundary/BoundaryAxis.d.ts.map +0 -1
  225. package/dist/axis/boundary/BoundaryAxis.js +0 -30
  226. package/dist/axis/boundary/BoundaryAxis.js.map +0 -1
  227. package/dist/axis/boundary/BoundaryRef.d.ts +0 -3
  228. package/dist/axis/boundary/BoundaryRef.d.ts.map +0 -1
  229. package/dist/axis/boundary/BoundaryRef.js +0 -2
  230. package/dist/axis/boundary/BoundaryRef.js.map +0 -1
  231. package/dist/axis/boundary/BoundaryRules.d.ts +0 -9
  232. package/dist/axis/boundary/BoundaryRules.d.ts.map +0 -1
  233. package/dist/axis/boundary/BoundaryRules.js +0 -2
  234. package/dist/axis/boundary/BoundaryRules.js.map +0 -1
  235. package/dist/axis/lifecycle/Clock.d.ts +0 -18
  236. package/dist/axis/lifecycle/Clock.d.ts.map +0 -1
  237. package/dist/axis/lifecycle/Clock.js +0 -25
  238. package/dist/axis/lifecycle/Clock.js.map +0 -1
  239. package/dist/axis/lifecycle/LifecycleAxis.d.ts +0 -4
  240. package/dist/axis/lifecycle/LifecycleAxis.d.ts.map +0 -1
  241. package/dist/axis/lifecycle/LifecycleAxis.js +0 -52
  242. package/dist/axis/lifecycle/LifecycleAxis.js.map +0 -1
  243. package/dist/axis/lifecycle/LifecycleRules.d.ts +0 -12
  244. package/dist/axis/lifecycle/LifecycleRules.d.ts.map +0 -1
  245. package/dist/axis/lifecycle/LifecycleRules.js +0 -3
  246. package/dist/axis/lifecycle/LifecycleRules.js.map +0 -1
  247. package/dist/axis/role/RoleAxis.d.ts +0 -5
  248. package/dist/axis/role/RoleAxis.d.ts.map +0 -1
  249. package/dist/axis/role/RoleAxis.js +0 -93
  250. package/dist/axis/role/RoleAxis.js.map +0 -1
  251. package/dist/axis/role/RoleRules.d.ts +0 -8
  252. package/dist/axis/role/RoleRules.d.ts.map +0 -1
  253. package/dist/axis/role/RoleRules.js +0 -2
  254. package/dist/axis/role/RoleRules.js.map +0 -1
  255. package/dist/axis/shape/Formats.d.ts.map +0 -1
  256. package/dist/axis/shape/Formats.js.map +0 -1
  257. package/dist/lib/Checked.d.ts +0 -11
  258. package/dist/lib/Checked.d.ts.map +0 -1
  259. package/dist/lib/Checked.js +0 -2
  260. package/dist/lib/Checked.js.map +0 -1
  261. package/dist/projection/card/RelationDescriptor.d.ts +0 -5
  262. package/dist/projection/card/RelationDescriptor.d.ts.map +0 -1
  263. package/dist/projection/card/RelationDescriptor.js +0 -2
  264. package/dist/projection/card/RelationDescriptor.js.map +0 -1
  265. package/dist/projection/standard.d.ts +0 -14
  266. package/dist/projection/standard.d.ts.map +0 -1
  267. package/dist/projection/standard.js +0 -2
  268. package/dist/projection/standard.js.map +0 -1
  269. package/dist/validator/AdapterFieldValidator.d.ts +0 -14
  270. package/dist/validator/AdapterFieldValidator.d.ts.map +0 -1
  271. package/dist/validator/AdapterFieldValidator.js +0 -36
  272. package/dist/validator/AdapterFieldValidator.js.map +0 -1
  273. package/src/PartialValues.ts +0 -4
  274. package/src/axis/boundary/Boundaries.ts +0 -36
  275. package/src/axis/boundary/BoundaryAxis.ts +0 -32
  276. package/src/axis/boundary/BoundaryRef.ts +0 -3
  277. package/src/axis/boundary/BoundaryRules.ts +0 -4
  278. package/src/axis/lifecycle/Clock.ts +0 -26
  279. package/src/axis/lifecycle/LifecycleAxis.ts +0 -55
  280. package/src/axis/lifecycle/LifecycleRules.ts +0 -9
  281. package/src/axis/role/RoleAxis.ts +0 -104
  282. package/src/axis/role/RoleRules.ts +0 -8
  283. package/src/lib/Checked.ts +0 -5
  284. package/src/projection/card/RelationDescriptor.ts +0 -3
  285. package/src/projection/standard.ts +0 -13
  286. package/src/validator/AdapterFieldValidator.ts +0 -40
  287. /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] ?? '.';
@@ -33,6 +33,28 @@ export class Visibility {
33
33
  return result;
34
34
  }
35
35
 
36
+ /**
37
+ * The dual of `encode`, read where a caller RECEIVES a row.
38
+ *
39
+ * `date-time` means a `Date` on both sides, and only the outgoing half was ever applied: a
40
+ * handler answered `new Date(0)`, the facade encoded it, and the caller was handed
41
+ * `"1970-01-01T00:00:00.000Z"` while its type said `Date` — locally as well as across a wire.
42
+ *
43
+ * It converts and never judges: what arrives was validated where it was produced, so a value
44
+ * the codec refuses is kept as it came rather than replaced or thrown over.
45
+ */
46
+ decode(record: Record<string, unknown>): Record<string, unknown> {
47
+ const out: Record<string, unknown> = { ...record };
48
+ for (const [key, field] of Object.entries(this.fields)) {
49
+ const value = record[key];
50
+ if (!(key in record) || value === null || value === undefined) continue;
51
+ const verdict = Boundary.of(field).decode(value);
52
+ if ('value' in verdict) out[key] = verdict.value;
53
+ }
54
+
55
+ return out;
56
+ }
57
+
36
58
  encode(record: Record<string, unknown>): Record<string, unknown> {
37
59
  const out: Record<string, unknown> = { ...record };
38
60
  for (const [key, field] of Object.entries(this.fields)) {
@@ -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
  );
@@ -1,4 +1,5 @@
1
- import { EXTENSION_AXES } from '../../axis/Axis.js';
1
+ import { Axes } from '../../axis/Axes.js';
2
+ import { CardForms } from './CardForms.js';
2
3
  import { type Resolver } from '../../axis/Resolver.js';
3
4
  import { clean, isObject } from '../../lib/utils.js';
4
5
  import { Field } from '../../field/Field.js';
@@ -8,7 +9,7 @@ import { InputValidator } from '../../validator/InputValidator.js';
8
9
  import { Schema } from '../../Schema.js';
9
10
  import { type SchemaConstructor } from '../../SchemaConstructor.js';
10
11
  import type { SchemaView } from '../../SchemaView.js';
11
- import type { Values } from '../../Values.js';
12
+ import type { Values } from '../../field/Values.js';
12
13
  import { admitPatterns, refuse } from './admission.js';
13
14
  import type { DerivedFrom } from './DerivedFrom.js';
14
15
  import type { FieldDescriptor } from './FieldDescriptor.js';
@@ -16,6 +17,7 @@ import type { FieldExtension } from './FieldExtension.js';
16
17
  import type { SchemaDescriptor } from './SchemaDescriptor.js';
17
18
  import { compare, type Diff } from './Diff.js';
18
19
  import { type DiffOptions } from './DiffOptions.js';
20
+ import { SchemaError } from '../../SchemaError.js';
19
21
 
20
22
  type FieldsOf<T> = { [K in keyof T]-?: Field<T[K]> };
21
23
 
@@ -125,11 +127,12 @@ export class Card<T = Values<Fields>> {
125
127
  */
126
128
  function describeExtension(field: Field, key: string): FieldExtension | undefined {
127
129
  const extension: Record<string, unknown> = {};
128
- for (const axis of EXTENSION_AXES) {
129
- const declared = (field as unknown as Record<string, unknown>)[axis.slot];
130
+ for (const slot of Axes.names) {
131
+ const declared = (field as unknown as Record<string, unknown>)[slot];
130
132
  if (declared === undefined) continue;
131
- const wire = axis.describe(declared, key);
132
- if (wire !== undefined) extension[axis.slot] = wire;
133
+ const form = CardForms.find(slot);
134
+ const wire = form ? form.describe(declared, key) : declared;
135
+ if (wire !== undefined) extension[slot] = wire;
133
136
  }
134
137
  clean(extension);
135
138
  return Object.keys(extension).length ? (extension as FieldExtension) : undefined;
@@ -165,7 +168,11 @@ function originOf(schema: SchemaView): DerivedFrom | undefined {
165
168
  * `{ type: 'string', maxLength: 200 }` → the same shape `text({ max: 200 })` states
166
169
  */
167
170
  function reconstructShape(property: FieldDescriptor): Field['shape'] | undefined {
168
- const types = Array.isArray(property.type) ? property.type : property.type ? [property.type] : [];
171
+ const types = Array.isArray(property.type)
172
+ ? property.type
173
+ : property.type
174
+ ? [property.type]
175
+ : [];
169
176
  if (!types.some((type) => type !== 'null')) return undefined;
170
177
 
171
178
  // `describeField` writes the shape whole, so it is read whole: a list of keywords here
@@ -188,7 +195,7 @@ function reconstructField(
188
195
  ): Field {
189
196
  const shape = reconstructShape(property);
190
197
  if (!shape) {
191
- throw new Error(
198
+ throw new SchemaError(
192
199
  `Field '${key}': the card carries no \`type\` for it, so there is no shape to rebuild. ` +
193
200
  'A field always states one.',
194
201
  );
@@ -196,9 +203,10 @@ function reconstructField(
196
203
  admitPatterns(shape, `Field '${key}'`);
197
204
  const extension = property['x-fougere'];
198
205
  const axes: Record<string, unknown> = {};
199
- for (const axis of EXTENSION_AXES) {
200
- const wire = (extension as Record<string, unknown> | undefined)?.[axis.slot];
201
- if (wire !== undefined) axes[axis.slot] = axis.reconstruct(wire, resolve);
206
+ for (const slot of Axes.names) {
207
+ const wire = (extension as Record<string, unknown> | undefined)?.[slot];
208
+ const form = CardForms.find(slot);
209
+ if (wire !== undefined) axes[slot] = form ? form.reconstruct(wire, resolve) : wire;
202
210
  }
203
211
  return new Field({
204
212
  shape,
@@ -207,7 +215,7 @@ function reconstructField(
207
215
  property.description !== undefined
208
216
  ? { description: property.description }
209
217
  : undefined,
210
- } as never);
218
+ } as never, key);
211
219
  }
212
220
 
213
221
  /**
@@ -215,7 +223,10 @@ function reconstructField(
215
223
  * FR : écrit un groupe sur CHAQUE membre — un lecteur du fil ne voit qu'un champ.
216
224
  * `carryGroup(properties.listId, ['listId', 'docId'])` → the pair lands under its `role`
217
225
  */
218
- function carryGroup(property: FieldDescriptor | undefined, group: readonly string[]): void {
226
+ function carryGroup(
227
+ property: FieldDescriptor | undefined,
228
+ group: readonly string[],
229
+ ): void {
219
230
  if (!property) return;
220
231
  const extension = (property['x-fougere'] ??= {}) as { role?: { unique?: string[][] } };
221
232
  const role = (extension.role ??= {});
@@ -0,0 +1,8 @@
1
+ import type { Resolver } from '../../axis/Resolver.js';
2
+
3
+ /** How a card writes what an axis declared, and reads it back — stated by the card, not the axis. */
4
+ export interface CardForm<Declared = unknown, Wire = unknown> {
5
+ describe(value: Declared, key: string): Wire | undefined;
6
+
7
+ reconstruct(wire: Wire, resolve?: Resolver): Declared;
8
+ }
@@ -0,0 +1,13 @@
1
+ import { Registry } from '../../lib/Registry.js';
2
+ import type { CardForm } from './CardForm.js';
3
+ import { roleOnCard } from './RoleDescriptor.js';
4
+
5
+ /**
6
+ * The axes a card writes differently from their declaration. An axis absent here travels as
7
+ * itself, which is what `lifecycle`, `boundary` and anything registered from outside do.
8
+ * FR : les axes qu'une carte écrit autrement que déclarés ; les autres voyagent tels quels.
9
+ * `CardForms.register('tenancy', tenancyOnCard)`
10
+ */
11
+ export const CardForms = new Registry<CardForm>('card form', 'call CardForms.register(slot, form)', [
12
+ ['role', roleOnCard as CardForm],
13
+ ]);
@@ -1,5 +1,5 @@
1
- import type { BoundaryRef } from '../../axis/boundary/BoundaryRef.js';
2
- import type { LifecycleRules } from '../../axis/lifecycle/LifecycleRules.js';
1
+ import type { BoundaryRef } from '../../axis/boundary/Boundary.js';
2
+ import type { LifecycleRules } from '../../axis/lifecycle/Lifecycle.js';
3
3
  import type { FieldDescriptor } from './FieldDescriptor.js';
4
4
  import type { RoleDescriptor } from './RoleDescriptor.js';
5
5
  import type { TypeSet } from './TypeSet.js';
@@ -1,5 +1,5 @@
1
1
  import { dequal } from 'dequal';
2
- import { EXTENSION_SLOTS } from '../../axis/Axis.js';
2
+ import { Axes } from '../../axis/Axes.js';
3
3
  import type { FieldDescriptor } from './FieldDescriptor.js';
4
4
  import type { FieldExtension } from './FieldExtension.js';
5
5
 
@@ -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(
@@ -93,7 +93,7 @@ function restated(
93
93
  before: FieldExtension | undefined,
94
94
  after: FieldExtension | undefined,
95
95
  ): Change[] {
96
- return EXTENSION_SLOTS.filter((axis) => !dequal(before?.[axis], after?.[axis])).map(
96
+ return Axes.names.filter((axis) => !dequal(before?.[axis], after?.[axis])).map(
97
97
  (axis) =>
98
98
  ({
99
99
  kind: 'restated',
@@ -1,9 +1,12 @@
1
- import type { BoundaryRef } from '../../axis/boundary/BoundaryRef.js';
2
- import type { LifecycleRules } from '../../axis/lifecycle/LifecycleRules.js';
1
+ import type { BoundaryRef } from '../../axis/boundary/Boundary.js';
2
+ import type { LifecycleRules } from '../../axis/lifecycle/Lifecycle.js';
3
3
  import type { RoleDescriptor } from './RoleDescriptor.js';
4
4
 
5
5
  export interface FieldExtension {
6
6
  role?: RoleDescriptor;
7
7
  lifecycle?: LifecycleRules;
8
8
  boundary?: BoundaryRef;
9
+
10
+ /** An axis registered from outside writes its own slot here, under its own name. */
11
+ [slot: string]: unknown;
9
12
  }
@@ -1,7 +1,53 @@
1
- import type { RoleRules } from '../../axis/role/RoleRules.js';
2
- import type { RelationDescriptor } from './RelationDescriptor.js';
1
+ import type { EntityConstructor } from '../../axis/role/EntityConstructor.js';
2
+ import type { Relation } from '../../axis/role/Relation.js';
3
+ import type { RoleRules } from '../../axis/role/Role.js';
4
+ import type { Resolver } from '../../axis/Resolver.js';
5
+ import { lowerFirst } from '../../lib/utils.js';
6
+ import type { CardForm } from './CardForm.js';
3
7
 
8
+ /** What a role becomes on a card: a target is a NAME there, and a unique is a group. */
4
9
  export type RoleDescriptor = Pick<RoleRules, 'primary' | 'index'> & {
5
10
  unique?: string[][];
6
11
  relation?: RelationDescriptor;
7
12
  };
13
+
14
+ export type RelationDescriptor = Pick<Relation, 'kind' | 'onDelete'> & { to: string };
15
+
16
+ type Mutable<T> = { -readonly [K in keyof T]: T[K] };
17
+
18
+ /** A card carries the target's NAME: a class cannot cross a process boundary. */
19
+ export const roleOnCard: CardForm<RoleRules, RoleDescriptor> = {
20
+ describe(role, key) {
21
+ const descriptor: Mutable<RoleDescriptor> = {};
22
+ if (role.primary) descriptor.primary = true;
23
+ if (role.unique) descriptor.unique = [[key]];
24
+ if (role.index) descriptor.index = true;
25
+ if (role.relation) {
26
+ const target = role.relation.to() as { name?: string };
27
+ descriptor.relation = {
28
+ to: lowerFirst(target.name ?? ''),
29
+ kind: role.relation.kind,
30
+ ...(role.relation.onDelete ? { onDelete: role.relation.onDelete } : {}),
31
+ };
32
+ }
33
+
34
+ return Object.keys(descriptor).length ? descriptor : undefined;
35
+ },
36
+
37
+ reconstruct(wire, resolve?: Resolver) {
38
+ const rules: Mutable<RoleRules> = {};
39
+ if (wire.primary) rules.primary = true;
40
+ if (wire.unique?.some((group) => group.length === 1)) rules.unique = true;
41
+ if (wire.index) rules.index = true;
42
+ if (wire.relation) {
43
+ const name = wire.relation.to;
44
+ rules.relation = {
45
+ to: () => (resolve?.(name) ?? ({ name } as unknown)) as EntityConstructor,
46
+ kind: wire.relation.kind,
47
+ ...(wire.relation.onDelete ? { onDelete: wire.relation.onDelete } : {}),
48
+ };
49
+ }
50
+
51
+ return rules;
52
+ },
53
+ };
@@ -1,6 +1,5 @@
1
- import type { ValidationError } from '../../lib/ValidationError.js';
2
1
  import { Shapes } from '../../axis/shape/Shape.js';
3
- import { dotted } from '../../lib/ValidationResult.js';
2
+ import { SchemaError } from '../../SchemaError.js';
4
3
 
5
4
  /**
6
5
  * Throws with what is wrong AND what to write: a card's author is in another process.
@@ -8,40 +7,9 @@ import { dotted } from '../../lib/ValidationResult.js';
8
7
  * `refuse('role.relation.kind is "maybe"', 'Expected one of one, many.')`
9
8
  */
10
9
  export function refuse(what: string, fix: string): never {
11
- throw new Error(`This card cannot be read: ${what}.\n ${fix}`);
10
+ throw new SchemaError(`This card cannot be read: ${what}.\n ${fix}`);
12
11
  }
13
12
 
14
- /**
15
- * Runs a card's axis through the validator a hand-written field goes through, so there is
16
- * one judge and not two.
17
- * FR : passe l'axe d'une carte par le juge d'un champ écrit à la main : un juge, pas deux.
18
- * `{ update: 'maybe' }` → `lifecycle is malformed — lifecycle.update: Expected 'now' or 'forbidden'`
19
- */
20
- export function admitWire(
21
- validator: (value: unknown, errors: ValidationError[]) => void,
22
- value: unknown,
23
- slot: string,
24
- ): void {
25
- const errors: ValidationError[] = [];
26
- validator(value, errors);
27
- if (errors.length) {
28
- refuse(
29
- `${slot} is malformed — ${errors.map((e) => `${dotted(e.path)}: ${e.message}`).join('; ')}`,
30
- 'A card states an axis the way a declaration does.',
31
- );
32
- }
33
- }
34
-
35
- /**
36
- * Checks a token against the list that declares it, never against a copy written here.
37
- * FR : vérifie un mot contre la liste qui le déclare, jamais contre une copie locale.
38
- * `oneOfTokens('many', RELATION_KINDS)` → `true`
39
- */
40
- export const oneOfTokens = <T extends readonly string[]>(
41
- value: unknown,
42
- tokens: T,
43
- ): value is T[number] => typeof value === 'string' && (tokens as readonly string[]).includes(value);
44
-
45
13
  /**
46
14
  * Compiles every `pattern` at the facade, so a bad one is refused here and not at the first row.
47
15
  * FR : compile chaque `pattern` à la porte : un mauvais est refusé ici, pas au premier row.
@@ -1,9 +1,10 @@
1
- import { EXTENSION_AXES } from '../axis/Axis.js';
1
+ import { Axes } from '../axis/Axes.js';
2
2
  import { Shapes } from '../axis/shape/Shape.js';
3
3
  import type { Field } from '../field/Field.js';
4
4
  import { isObject, shown } from '../lib/utils.js';
5
5
  import type { ValidationError } from '../lib/ValidationError.js';
6
6
  import type { ValidationResult } from '../lib/ValidationResult.js';
7
+ import { JsonSchemaValidator } from './JsonSchemaValidator.js';
7
8
 
8
9
  export class FieldDeclarationValidator {
9
10
  private constructor(private readonly declaration: unknown) {}
@@ -36,23 +37,10 @@ export class FieldDeclarationValidator {
36
37
  });
37
38
  }
38
39
 
39
- for (const axis of EXTENSION_AXES) {
40
- const declared = declaration[axis.slot];
41
- if (declared !== undefined) axis.validator(declared, errors);
42
- }
40
+ errors.push(...JsonSchemaValidator.of(Axes.fieldFormat).refusalsOf(declaration, []));
43
41
 
44
- if (declaration.meta !== undefined) {
45
- if (!isObject(declaration.meta)) {
46
- errors.push({
47
- path: ['meta'],
48
- message: `Expected an object — got ${shown(declaration.meta)}`,
49
- });
50
- } else if (
51
- declaration.meta.description !== undefined &&
52
- typeof declaration.meta.description !== 'string'
53
- ) {
54
- errors.push({ path: ['meta', 'description'], message: 'Expected a string' });
55
- }
42
+ for (const [slot, axis] of Axes.entries) {
43
+ errors.push(...(axis.refusals?.(declaration[slot]) ?? []));
56
44
  }
57
45
 
58
46
  return errors.length