@fougere/schema 0.12.1-alpha.0 → 0.12.2-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 (246) hide show
  1. package/dist/FougereFieldAxes.d.ts.map +1 -0
  2. package/dist/FougereFieldAxes.js.map +1 -0
  3. package/dist/Schema.d.ts +4 -3
  4. package/dist/Schema.d.ts.map +1 -1
  5. package/dist/Schema.js +3 -0
  6. package/dist/Schema.js.map +1 -1
  7. package/dist/SchemaConstraints.d.ts +13 -10
  8. package/dist/SchemaConstraints.d.ts.map +1 -1
  9. package/dist/SchemaConstraints.js +32 -20
  10. package/dist/SchemaConstraints.js.map +1 -1
  11. package/dist/SchemaDefinition.d.ts.map +1 -1
  12. package/dist/SchemaDefinition.js +6 -5
  13. package/dist/SchemaDefinition.js.map +1 -1
  14. package/dist/SchemaError.d.ts +4 -1
  15. package/dist/SchemaError.d.ts.map +1 -1
  16. package/dist/SchemaError.js +5 -2
  17. package/dist/SchemaError.js.map +1 -1
  18. package/dist/SchemaView.d.ts +3 -2
  19. package/dist/SchemaView.d.ts.map +1 -1
  20. package/dist/axis/Axes.d.ts +1 -1
  21. package/dist/axis/Axes.js +3 -3
  22. package/dist/axis/Axis.d.ts +12 -2
  23. package/dist/axis/Axis.d.ts.map +1 -1
  24. package/dist/axis/boundary/Boundary.d.ts +9 -2
  25. package/dist/axis/boundary/Boundary.d.ts.map +1 -1
  26. package/dist/axis/boundary/Boundary.js +6 -2
  27. package/dist/axis/boundary/Boundary.js.map +1 -1
  28. package/dist/axis/boundary/Decoder.js.map +1 -1
  29. package/dist/axis/lifecycle/Lifecycle.d.ts +13 -4
  30. package/dist/axis/lifecycle/Lifecycle.d.ts.map +1 -1
  31. package/dist/axis/lifecycle/Lifecycle.js +8 -4
  32. package/dist/axis/lifecycle/Lifecycle.js.map +1 -1
  33. package/dist/axis/lifecycle/apply.js +1 -1
  34. package/dist/axis/lifecycle/apply.js.map +1 -1
  35. package/dist/axis/role/Role.d.ts +17 -10
  36. package/dist/axis/role/Role.d.ts.map +1 -1
  37. package/dist/axis/role/Role.js +30 -10
  38. package/dist/axis/role/Role.js.map +1 -1
  39. package/dist/axis/shape/Shape.d.ts +32 -9
  40. package/dist/axis/shape/Shape.d.ts.map +1 -1
  41. package/dist/axis/shape/Shape.js +43 -12
  42. package/dist/axis/shape/Shape.js.map +1 -1
  43. package/dist/axis/shape/ShapeFormat.d.ts +10 -0
  44. package/dist/axis/shape/ShapeFormat.d.ts.map +1 -0
  45. package/dist/axis/shape/ShapeFormat.js +32 -0
  46. package/dist/axis/shape/ShapeFormat.js.map +1 -0
  47. package/dist/entity/EntityDeclarations.d.ts +3 -2
  48. package/dist/entity/EntityDeclarations.d.ts.map +1 -1
  49. package/dist/entity/FieldGroups.d.ts +9 -0
  50. package/dist/entity/FieldGroups.d.ts.map +1 -0
  51. package/dist/entity/FieldGroups.js +2 -0
  52. package/dist/entity/FieldGroups.js.map +1 -0
  53. package/dist/field/Field.d.ts +35 -16
  54. package/dist/field/Field.d.ts.map +1 -1
  55. package/dist/field/Field.js +53 -8
  56. package/dist/field/Field.js.map +1 -1
  57. package/dist/field/FieldFormat.d.ts +1 -1
  58. package/dist/field/FieldFormat.js +4 -4
  59. package/dist/field/FieldFormat.js.map +1 -1
  60. package/dist/field/FieldSet.d.ts +12 -3
  61. package/dist/field/FieldSet.d.ts.map +1 -1
  62. package/dist/field/FieldSet.js +22 -15
  63. package/dist/field/FieldSet.js.map +1 -1
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js.map +1 -1
  67. package/dist/lib/Format.d.ts +21 -2
  68. package/dist/lib/Format.d.ts.map +1 -1
  69. package/dist/lib/Format.js +31 -3
  70. package/dist/lib/Format.js.map +1 -1
  71. package/dist/lib/UnionToIntersection.d.ts +7 -0
  72. package/dist/lib/UnionToIntersection.d.ts.map +1 -0
  73. package/dist/lib/UnionToIntersection.js +2 -0
  74. package/dist/lib/UnionToIntersection.js.map +1 -0
  75. package/dist/lib/utils.d.ts +1 -1
  76. package/dist/lib/utils.d.ts.map +1 -1
  77. package/dist/lib/utils.js +1 -1
  78. package/dist/lib/utils.js.map +1 -1
  79. package/dist/projection/Cases.d.ts +1 -1
  80. package/dist/projection/Cases.d.ts.map +1 -1
  81. package/dist/projection/Cases.js +20 -8
  82. package/dist/projection/Cases.js.map +1 -1
  83. package/dist/projection/Visibility.d.ts.map +1 -1
  84. package/dist/projection/Visibility.js +6 -6
  85. package/dist/projection/Visibility.js.map +1 -1
  86. package/dist/projection/card/Bundle.d.ts.map +1 -1
  87. package/dist/projection/card/Bundle.js.map +1 -1
  88. package/dist/projection/card/Card.d.ts.map +1 -1
  89. package/dist/projection/card/Card.js +21 -30
  90. package/dist/projection/card/Card.js.map +1 -1
  91. package/dist/projection/card/CardForms.js +1 -1
  92. package/dist/projection/card/Diff.js.map +1 -1
  93. package/dist/projection/card/FieldExtension.d.ts +2 -2
  94. package/dist/projection/card/FieldExtension.d.ts.map +1 -1
  95. package/dist/projection/card/RoleDescriptor.d.ts +7 -2
  96. package/dist/projection/card/RoleDescriptor.d.ts.map +1 -1
  97. package/dist/projection/card/RoleDescriptor.js +2 -2
  98. package/dist/projection/card/RoleDescriptor.js.map +1 -1
  99. package/dist/projection/card/ShapeKeywords.d.ts +1 -1
  100. package/dist/projection/card/ShapeKeywords.d.ts.map +1 -1
  101. package/dist/validator/FieldDeclarationValidator.d.ts.map +1 -1
  102. package/dist/validator/FieldDeclarationValidator.js +7 -10
  103. package/dist/validator/FieldDeclarationValidator.js.map +1 -1
  104. package/dist/validator/FieldValueValidator.d.ts.map +1 -1
  105. package/dist/validator/FieldValueValidator.js +0 -2
  106. package/dist/validator/FieldValueValidator.js.map +1 -1
  107. package/dist/validator/InputValidator.d.ts +7 -1
  108. package/dist/validator/InputValidator.d.ts.map +1 -1
  109. package/dist/validator/InputValidator.js +15 -14
  110. package/dist/validator/InputValidator.js.map +1 -1
  111. package/dist/vocabulary/preset/created.d.ts +2 -2
  112. package/dist/vocabulary/preset/created.d.ts.map +1 -1
  113. package/dist/vocabulary/preset/created.js +2 -2
  114. package/dist/vocabulary/preset/created.js.map +1 -1
  115. package/dist/vocabulary/preset/updated.d.ts +2 -2
  116. package/dist/vocabulary/preset/updated.d.ts.map +1 -1
  117. package/dist/vocabulary/preset/updated.js +2 -2
  118. package/dist/vocabulary/preset/updated.js.map +1 -1
  119. package/dist/vocabulary/primitive/bool.d.ts +2 -5
  120. package/dist/vocabulary/primitive/bool.d.ts.map +1 -1
  121. package/dist/vocabulary/primitive/bool.js +1 -5
  122. package/dist/vocabulary/primitive/bool.js.map +1 -1
  123. package/dist/vocabulary/primitive/date.d.ts +2 -3
  124. package/dist/vocabulary/primitive/date.d.ts.map +1 -1
  125. package/dist/vocabulary/primitive/date.js +2 -3
  126. package/dist/vocabulary/primitive/date.js.map +1 -1
  127. package/dist/vocabulary/primitive/json.d.ts +3 -3
  128. package/dist/vocabulary/primitive/json.d.ts.map +1 -1
  129. package/dist/vocabulary/primitive/json.js +11 -11
  130. package/dist/vocabulary/primitive/json.js.map +1 -1
  131. package/dist/vocabulary/primitive/list.d.ts +6 -5
  132. package/dist/vocabulary/primitive/list.d.ts.map +1 -1
  133. package/dist/vocabulary/primitive/list.js +13 -7
  134. package/dist/vocabulary/primitive/list.js.map +1 -1
  135. package/dist/vocabulary/primitive/many.d.ts +2 -4
  136. package/dist/vocabulary/primitive/many.d.ts.map +1 -1
  137. package/dist/vocabulary/primitive/many.js +2 -4
  138. package/dist/vocabulary/primitive/many.js.map +1 -1
  139. package/dist/vocabulary/primitive/number.d.ts +2 -4
  140. package/dist/vocabulary/primitive/number.d.ts.map +1 -1
  141. package/dist/vocabulary/primitive/number.js +1 -3
  142. package/dist/vocabulary/primitive/number.js.map +1 -1
  143. package/dist/vocabulary/primitive/oneOf.d.ts +4 -7
  144. package/dist/vocabulary/primitive/oneOf.d.ts.map +1 -1
  145. package/dist/vocabulary/primitive/oneOf.js +19 -11
  146. package/dist/vocabulary/primitive/oneOf.js.map +1 -1
  147. package/dist/vocabulary/primitive/primary.d.ts +2 -2
  148. package/dist/vocabulary/primitive/primary.d.ts.map +1 -1
  149. package/dist/vocabulary/primitive/primary.js +2 -3
  150. package/dist/vocabulary/primitive/primary.js.map +1 -1
  151. package/dist/vocabulary/primitive/ref.d.ts +2 -6
  152. package/dist/vocabulary/primitive/ref.d.ts.map +1 -1
  153. package/dist/vocabulary/primitive/ref.js +1 -1
  154. package/dist/vocabulary/primitive/ref.js.map +1 -1
  155. package/dist/vocabulary/primitive/text.d.ts +2 -4
  156. package/dist/vocabulary/primitive/text.d.ts.map +1 -1
  157. package/dist/vocabulary/primitive/text.js +1 -3
  158. package/dist/vocabulary/primitive/text.js.map +1 -1
  159. package/dist/vocabulary/rule/nullable.d.ts +0 -1
  160. package/dist/vocabulary/rule/nullable.d.ts.map +1 -1
  161. package/dist/vocabulary/rule/nullable.js +0 -1
  162. package/dist/vocabulary/rule/nullable.js.map +1 -1
  163. package/dist/vocabulary/rule/optional.d.ts +0 -1
  164. package/dist/vocabulary/rule/optional.d.ts.map +1 -1
  165. package/dist/vocabulary/rule/optional.js +0 -1
  166. package/dist/vocabulary/rule/optional.js.map +1 -1
  167. package/dist/vocabulary/rule/readOnly.d.ts +0 -2
  168. package/dist/vocabulary/rule/readOnly.d.ts.map +1 -1
  169. package/dist/vocabulary/rule/readOnly.js +0 -2
  170. package/dist/vocabulary/rule/readOnly.js.map +1 -1
  171. package/dist/vocabulary/rule/writeOnly.d.ts +0 -2
  172. package/dist/vocabulary/rule/writeOnly.d.ts.map +1 -1
  173. package/dist/vocabulary/rule/writeOnly.js +0 -2
  174. package/dist/vocabulary/rule/writeOnly.js.map +1 -1
  175. package/dist/vocabulary/vocabulary.d.ts +2 -3
  176. package/dist/vocabulary/vocabulary.d.ts.map +1 -1
  177. package/dist/vocabulary/vocabulary.js +17 -21
  178. package/dist/vocabulary/vocabulary.js.map +1 -1
  179. package/package.json +1 -1
  180. package/src/Schema.ts +11 -8
  181. package/src/SchemaConstraints.ts +40 -22
  182. package/src/SchemaDefinition.ts +7 -5
  183. package/src/SchemaError.ts +8 -2
  184. package/src/SchemaView.ts +3 -2
  185. package/src/axis/Axes.ts +3 -3
  186. package/src/axis/Axis.ts +13 -2
  187. package/src/axis/boundary/Boundary.ts +16 -2
  188. package/src/axis/boundary/Decoder.ts +2 -0
  189. package/src/axis/lifecycle/Lifecycle.ts +15 -4
  190. package/src/axis/lifecycle/apply.ts +1 -1
  191. package/src/axis/role/Role.ts +41 -9
  192. package/src/axis/shape/Shape.ts +89 -23
  193. package/src/axis/shape/ShapeFormat.ts +37 -0
  194. package/src/entity/EntityDeclarations.ts +3 -2
  195. package/src/entity/FieldGroups.ts +9 -0
  196. package/src/field/Field.ts +84 -21
  197. package/src/field/FieldFormat.ts +5 -5
  198. package/src/field/FieldSet.ts +27 -19
  199. package/src/index.ts +1 -0
  200. package/src/lib/Format.ts +36 -3
  201. package/src/lib/UnionToIntersection.ts +10 -0
  202. package/src/lib/utils.ts +5 -1
  203. package/src/projection/Cases.ts +25 -8
  204. package/src/projection/Visibility.ts +9 -6
  205. package/src/projection/card/Bundle.ts +2 -0
  206. package/src/projection/card/Card.ts +26 -27
  207. package/src/projection/card/CardForms.ts +1 -1
  208. package/src/projection/card/Diff.ts +4 -0
  209. package/src/projection/card/FieldExtension.ts +2 -2
  210. package/src/projection/card/RoleDescriptor.ts +9 -4
  211. package/src/projection/card/ShapeKeywords.ts +1 -1
  212. package/src/validator/FieldDeclarationValidator.ts +7 -10
  213. package/src/validator/FieldValueValidator.ts +3 -1
  214. package/src/validator/InputValidator.ts +17 -12
  215. package/src/vocabulary/preset/created.ts +3 -3
  216. package/src/vocabulary/preset/updated.ts +4 -3
  217. package/src/vocabulary/primitive/bool.ts +3 -8
  218. package/src/vocabulary/primitive/date.ts +5 -4
  219. package/src/vocabulary/primitive/json.ts +27 -12
  220. package/src/vocabulary/primitive/list.ts +16 -9
  221. package/src/vocabulary/primitive/many.ts +3 -4
  222. package/src/vocabulary/primitive/number.ts +3 -8
  223. package/src/vocabulary/primitive/oneOf.ts +40 -24
  224. package/src/vocabulary/primitive/primary.ts +9 -5
  225. package/src/vocabulary/primitive/ref.ts +3 -7
  226. package/src/vocabulary/primitive/text.ts +3 -8
  227. package/src/vocabulary/rule/nullable.ts +0 -1
  228. package/src/vocabulary/rule/optional.ts +0 -1
  229. package/src/vocabulary/rule/readOnly.ts +0 -2
  230. package/src/vocabulary/rule/writeOnly.ts +0 -2
  231. package/src/vocabulary/vocabulary.ts +18 -22
  232. package/dist/entity/CompositeUnique.d.ts +0 -4
  233. package/dist/entity/CompositeUnique.d.ts.map +0 -1
  234. package/dist/entity/CompositeUnique.js +0 -2
  235. package/dist/entity/CompositeUnique.js.map +0 -1
  236. package/dist/field/FougereFieldAxes.d.ts.map +0 -1
  237. package/dist/field/FougereFieldAxes.js.map +0 -1
  238. package/dist/field/Meta.d.ts +0 -6
  239. package/dist/field/Meta.d.ts.map +0 -1
  240. package/dist/field/Meta.js +0 -3
  241. package/dist/field/Meta.js.map +0 -1
  242. package/src/entity/CompositeUnique.ts +0 -6
  243. package/src/field/Meta.ts +0 -5
  244. /package/dist/{field/FougereFieldAxes.d.ts → FougereFieldAxes.d.ts} +0 -0
  245. /package/dist/{field/FougereFieldAxes.js → FougereFieldAxes.js} +0 -0
  246. /package/src/{field/FougereFieldAxes.ts → FougereFieldAxes.ts} +0 -0
@@ -15,21 +15,23 @@ export class Visibility {
15
15
  const result: Fields = {};
16
16
  for (const [name, field] of Object.entries(this.fields)) {
17
17
  const role = Role.of(field);
18
- if (role.isPrimary) continue;
19
- if (Lifecycle.of(field).stampedAtCreate) continue;
20
- if (role.isCollection) continue;
21
- if (Boundary.of(field).readOnly) continue;
18
+ if (role.isPrimary()) continue;
19
+ if (Lifecycle.of(field).stampedAtCreate()) continue;
20
+ if (role.isCollection()) continue;
21
+ if (Boundary.of(field).readOnly()) continue;
22
22
  result[name] = field;
23
23
  }
24
+
24
25
  return result;
25
26
  }
26
27
 
27
28
  get output(): Fields {
28
29
  const result: Fields = {};
29
30
  for (const [name, field] of Object.entries(this.fields)) {
30
- if (Boundary.of(field).writeOnly) continue;
31
+ if (Boundary.of(field).writeOnly()) continue;
31
32
  result[name] = field;
32
33
  }
34
+
33
35
  return result;
34
36
  }
35
37
 
@@ -60,7 +62,7 @@ export class Visibility {
60
62
  for (const [key, field] of Object.entries(this.fields)) {
61
63
  if (!(key in record)) continue;
62
64
  const boundary = Boundary.of(field);
63
- if (boundary.writeOnly) {
65
+ if (boundary.writeOnly()) {
64
66
  delete out[key];
65
67
  continue;
66
68
  }
@@ -68,6 +70,7 @@ export class Visibility {
68
70
  if (value === null || value === undefined) continue;
69
71
  out[key] = boundary.encode(value);
70
72
  }
73
+
71
74
  return out;
72
75
  }
73
76
  }
@@ -56,6 +56,7 @@ export class Bundle {
56
56
  byName[name.toLowerCase()] = schema as unknown as EntityConstructor;
57
57
  schemas[name] = schema;
58
58
  }
59
+
59
60
  return schemas;
60
61
  }
61
62
 
@@ -72,6 +73,7 @@ export class Bundle {
72
73
  );
73
74
  if (diff.changes.length > 0 || diff.ambiguous.length > 0) entities[name] = diff;
74
75
  }
76
+
75
77
  return {
76
78
  entitiesAdded: Object.keys(after).filter((name) => !(name in before)),
77
79
  entitiesRemoved: Object.keys(before).filter((name) => !(name in after)),
@@ -39,10 +39,11 @@ export class Card<T = Values<Fields>> {
39
39
  const required: string[] = [];
40
40
  for (const [key, field] of Object.entries(fields)) {
41
41
  properties[key] = describeField(field, key);
42
- if (validator.onAbsent(field) === null) required.push(key);
42
+ if (validator.requires(field)) required.push(key);
43
43
  }
44
- for (const group of schema.getUnique() ?? [])
45
- for (const member of group) carryGroup(properties[member], group);
44
+ for (const kind of ['unique', 'index'] as const)
45
+ for (const group of (kind === 'unique' ? schema.getUnique() : schema.getIndex()) ?? [])
46
+ for (const member of group) carryGroup(properties[member], kind, group);
46
47
 
47
48
  const descriptor: SchemaDescriptor = {
48
49
  type: 'object',
@@ -56,6 +57,7 @@ export class Card<T = Values<Fields>> {
56
57
 
57
58
  const origin = originOf(schema);
58
59
  if (origin) descriptor['x-fougere-derived'] = origin;
60
+
59
61
  return new Card<Values<TFields>>(descriptor);
60
62
  }
61
63
 
@@ -95,7 +97,7 @@ export class Card<T = Values<Fields>> {
95
97
  }
96
98
 
97
99
  const fields: Fields = {};
98
- const groups: string[][] = [];
100
+ const groups: { unique: string[][]; index: string[][] } = { unique: [], index: [] };
99
101
  for (const [key, property] of Object.entries(descriptor.properties)) {
100
102
  if (!isObject(property)) {
101
103
  refuse(
@@ -104,13 +106,15 @@ export class Card<T = Values<Fields>> {
104
106
  );
105
107
  }
106
108
  fields[key] = reconstructField(property, key, resolve);
107
- for (const group of property['x-fougere']?.role?.unique ?? [])
108
- if (group.length > 1) groups.push([...group]);
109
+ for (const kind of ['unique', 'index'] as const)
110
+ for (const group of property['x-fougere']?.role?.[kind] ?? [])
111
+ if (group.length > 1) groups[kind].push([...group]);
109
112
  }
110
113
  const schema = Schema.of({ fields, constraints: SchemaConstraints.of(groups) });
111
114
  const title = name ?? descriptor.title;
112
115
  if (title)
113
116
  Object.defineProperty(schema, 'name', { value: title, configurable: true });
117
+
114
118
  return schema as unknown as SchemaConstructor<FieldsOf<T>>;
115
119
  }
116
120
 
@@ -127,14 +131,13 @@ export class Card<T = Values<Fields>> {
127
131
  */
128
132
  function describeExtension(field: Field, key: string): FieldExtension | undefined {
129
133
  const extension: Record<string, unknown> = {};
130
- for (const slot of Axes.names) {
131
- const declared = (field as unknown as Record<string, unknown>)[slot];
132
- if (declared === undefined) continue;
133
- const form = CardForms.find(slot);
134
+ for (const [name, declared] of Object.entries(field.axes)) {
135
+ const form = CardForms.find(name);
134
136
  const wire = form ? form.describe(declared, key) : declared;
135
- if (wire !== undefined) extension[slot] = wire;
137
+ if (wire !== undefined) extension[name] = wire;
136
138
  }
137
139
  clean(extension);
140
+
138
141
  return Object.keys(extension).length ? (extension as FieldExtension) : undefined;
139
142
  }
140
143
 
@@ -142,9 +145,9 @@ function describeField(field: Field, key: string): FieldDescriptor {
142
145
  // The shape IS JSON Schema, so it lands whole rather than key by key — which is what let
143
146
  // it be copied under a computed key into a type that names its own.
144
147
  const descriptor: FieldDescriptor = clean({ ...field.shape }) as FieldDescriptor;
145
- if (field.meta?.description) descriptor.description = field.meta.description;
146
148
  const extension = describeExtension(field, key);
147
149
  if (extension) descriptor['x-fougere'] = extension;
150
+
148
151
  return descriptor;
149
152
  }
150
153
 
@@ -159,6 +162,7 @@ function originOf(schema: SchemaView): DerivedFrom | undefined {
159
162
  const nameOf = Object.fromEntries(
160
163
  Object.entries(derivation.nameOf).map(([key, value]) => [key, value ?? null]),
161
164
  );
165
+
162
166
  return { from: derivation.sourceName, nameOf };
163
167
  }
164
168
 
@@ -177,7 +181,7 @@ function reconstructShape(property: FieldDescriptor): Field['shape'] | undefined
177
181
 
178
182
  // `describeField` writes the shape whole, so it is read whole: a list of keywords here
179
183
  // would be a second inventory, and the one that forgets a keyword loses it in silence.
180
- const { 'x-fougere': _extension, description: _description, ...shape } = property;
184
+ const { 'x-fougere': _extension, ...shape } = property;
181
185
  if (shape.items) shape.items = reconstructShape(shape.items) as FieldDescriptor;
182
186
 
183
187
  return clean(shape) as Field['shape'];
@@ -203,19 +207,13 @@ function reconstructField(
203
207
  admitPatterns(shape, `Field '${key}'`);
204
208
  const extension = property['x-fougere'];
205
209
  const axes: Record<string, unknown> = {};
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;
210
+ for (const name of Axes.names) {
211
+ const wire = (extension as Record<string, unknown> | undefined)?.[name];
212
+ const form = CardForms.find(name);
213
+ if (wire !== undefined) axes[name] = form ? form.reconstruct(wire, resolve) : wire;
210
214
  }
211
- return new Field({
212
- shape,
213
- ...axes,
214
- meta:
215
- property.description !== undefined
216
- ? { description: property.description }
217
- : undefined,
218
- } as never, key);
215
+
216
+ return new Field({ shape, ...axes } as never, key);
219
217
  }
220
218
 
221
219
  /**
@@ -225,10 +223,11 @@ function reconstructField(
225
223
  */
226
224
  function carryGroup(
227
225
  property: FieldDescriptor | undefined,
226
+ kind: 'unique' | 'index',
228
227
  group: readonly string[],
229
228
  ): void {
230
229
  if (!property) return;
231
- const extension = (property['x-fougere'] ??= {}) as { role?: { unique?: string[][] } };
230
+ const extension = (property['x-fougere'] ??= {}) as { role?: Record<string, string[][]> };
232
231
  const role = (extension.role ??= {});
233
- (role.unique ??= []).push([...group]);
232
+ (role[kind] ??= []).push([...group]);
234
233
  }
@@ -8,6 +8,6 @@ import { roleOnCard } from './RoleDescriptor.js';
8
8
  * FR : les axes qu'une carte écrit autrement que déclarés ; les autres voyagent tels quels.
9
9
  * `CardForms.register('tenancy', tenancyOnCard)`
10
10
  */
11
- export const CardForms = new Registry<CardForm>('card form', 'call CardForms.register(slot, form)', [
11
+ export const CardForms = new Registry<CardForm>('card form', 'call CardForms.register(name, form)', [
12
12
  ['role', roleOnCard as CardForm],
13
13
  ]);
@@ -107,6 +107,7 @@ function restated(
107
107
 
108
108
  function shapeOf(descriptor: FieldDescriptor): Record<string, unknown> {
109
109
  const { 'x-fougere': _extension, description: _description, ...shape } = descriptor;
110
+
110
111
  return shape as Record<string, unknown>;
111
112
  }
112
113
 
@@ -118,6 +119,7 @@ function shapeOf(descriptor: FieldDescriptor): Record<string, unknown> {
118
119
  function typesOf(descriptor: FieldDescriptor): TypeSet {
119
120
  const type = descriptor.type;
120
121
  if (type === undefined) return [];
122
+
121
123
  return (Array.isArray(type) ? [...type] : [type]).sort();
122
124
  }
123
125
 
@@ -128,6 +130,7 @@ function typesOf(descriptor: FieldDescriptor): TypeSet {
128
130
  */
129
131
  function boundsOf(descriptor: FieldDescriptor): Record<string, unknown> {
130
132
  const { type: _type, ...rest } = shapeOf(descriptor);
133
+
131
134
  return rest;
132
135
  }
133
136
 
@@ -153,5 +156,6 @@ function candidates(
153
156
  const now = Object.keys(after);
154
157
  const apart = ({ removed: gone, added: appeared }: RenameCandidate): number =>
155
158
  Math.abs(now.indexOf(appeared) - was.indexOf(gone));
159
+
156
160
  return found.sort((a, b) => apart(a) - apart(b));
157
161
  }
@@ -7,6 +7,6 @@ export interface FieldExtension {
7
7
  lifecycle?: LifecycleRules;
8
8
  boundary?: BoundaryRef;
9
9
 
10
- /** An axis registered from outside writes its own slot here, under its own name. */
11
- [slot: string]: unknown;
10
+ /** An axis registered from outside writes here, under its own name. */
11
+ [axis: string]: unknown;
12
12
  }
@@ -5,9 +5,14 @@ import type { Resolver } from '../../axis/Resolver.js';
5
5
  import { lowerFirst } from '../../lib/utils.js';
6
6
  import type { CardForm } from './CardForm.js';
7
7
 
8
- /** What a role becomes on a card: a target is a NAME there, and a unique is a group. */
9
- export type RoleDescriptor = Pick<RoleRules, 'primary' | 'index'> & {
8
+ /**
9
+ * What a role becomes on a card: a target is a NAME there, and `unique` and `index` are both
10
+ * GROUPS — the field says `true`, the card says which names go together, since a group of
11
+ * several belongs to no single field.
12
+ */
13
+ export type RoleDescriptor = Pick<RoleRules, 'primary'> & {
10
14
  unique?: string[][];
15
+ index?: string[][];
11
16
  relation?: RelationDescriptor;
12
17
  };
13
18
 
@@ -21,7 +26,7 @@ export const roleOnCard: CardForm<RoleRules, RoleDescriptor> = {
21
26
  const descriptor: Mutable<RoleDescriptor> = {};
22
27
  if (role.primary) descriptor.primary = true;
23
28
  if (role.unique) descriptor.unique = [[key]];
24
- if (role.index) descriptor.index = true;
29
+ if (role.index) descriptor.index = [[key]];
25
30
  if (role.relation) {
26
31
  const target = role.relation.to() as { name?: string };
27
32
  descriptor.relation = {
@@ -38,7 +43,7 @@ export const roleOnCard: CardForm<RoleRules, RoleDescriptor> = {
38
43
  const rules: Mutable<RoleRules> = {};
39
44
  if (wire.primary) rules.primary = true;
40
45
  if (wire.unique?.some((group) => group.length === 1)) rules.unique = true;
41
- if (wire.index) rules.index = true;
46
+ if (wire.index?.some((group) => group.length === 1)) rules.index = true;
42
47
  if (wire.relation) {
43
48
  const name = wire.relation.to;
44
49
  rules.relation = {
@@ -1,7 +1,7 @@
1
1
  import type { Shape } from '../../axis/shape/Shape.js';
2
+ import type { UnionToIntersection } from '../../lib/UnionToIntersection.js';
2
3
 
3
4
  type KeywordsOf<S> = S extends unknown ? Partial<Omit<S, 'type' | 'items' | 'properties'>> : never;
4
- type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
5
5
 
6
6
  /** Every keyword any shape may carry, flattened — what a descriptor records of one. */
7
7
  export type ShapeKeywords = UnionToIntersection<KeywordsOf<Shape>>;
@@ -1,7 +1,7 @@
1
1
  import { Axes } from '../axis/Axes.js';
2
- import { Shapes } from '../axis/shape/Shape.js';
3
2
  import type { Field } from '../field/Field.js';
4
- import { isObject, shown } from '../lib/utils.js';
3
+ import { isObject } from '../lib/utils.js';
4
+ import { SchemaError } from '../SchemaError.js';
5
5
  import type { ValidationError } from '../lib/ValidationError.js';
6
6
  import type { ValidationResult } from '../lib/ValidationResult.js';
7
7
  import { JsonSchemaValidator } from './JsonSchemaValidator.js';
@@ -22,7 +22,7 @@ export class FieldDeclarationValidator {
22
22
  errors: [
23
23
  {
24
24
  path: [],
25
- message: `Expected an object — got ${shown(declaration)}`,
25
+ message: `Expected an object — got ${SchemaError.inspect(declaration)}`,
26
26
  },
27
27
  ],
28
28
  };
@@ -30,17 +30,14 @@ export class FieldDeclarationValidator {
30
30
 
31
31
  const errors: ValidationError[] = [];
32
32
 
33
- if (!Shapes.is(declaration.shape)) {
34
- errors.push({
35
- path: ['shape'],
36
- message: `Every field states a shape — got ${shown(declaration.shape)}`,
37
- });
33
+ if (declaration.shape === undefined) {
34
+ errors.push({ path: ['shape'], message: 'Every field states a shape' });
38
35
  }
39
36
 
40
37
  errors.push(...JsonSchemaValidator.of(Axes.fieldFormat).refusalsOf(declaration, []));
41
38
 
42
- for (const [slot, axis] of Axes.entries) {
43
- errors.push(...(axis.refusals?.(declaration[slot]) ?? []));
39
+ for (const [name, axis] of Axes.entries) {
40
+ errors.push(...(axis.refusals?.(declaration[name], declaration) ?? []));
44
41
  }
45
42
 
46
43
  return errors.length
@@ -30,7 +30,6 @@ export class FieldValueValidator {
30
30
  const type = Shapes.typeOf(shape);
31
31
  const base = Shapes.of(shape).base;
32
32
  if (value !== null) {
33
- if (base?.type === 'object' && !base.properties) return { value };
34
33
  if (type === 'date' && value instanceof Date) {
35
34
  return Number.isNaN(value.getTime()) ? { message: 'Invalid date' } : { value };
36
35
  }
@@ -46,6 +45,7 @@ export class FieldValueValidator {
46
45
  if (plan.custom && typeof value === 'string' && !plan.custom(value)) {
47
46
  return { message: `String does not match format "${plan.formatName}".` };
48
47
  }
48
+
49
49
  return { value };
50
50
  }
51
51
 
@@ -72,6 +72,7 @@ export class FieldValueValidator {
72
72
  };
73
73
  this.plans.set(shape, plan);
74
74
  }
75
+
75
76
  return plan;
76
77
  }
77
78
 
@@ -83,6 +84,7 @@ export class FieldValueValidator {
83
84
  `the engine validates ${Object.keys(engineFormats).length} formats natively and this is not one of them.`,
84
85
  );
85
86
  }
87
+
86
88
  return custom;
87
89
  }
88
90
  }
@@ -1,3 +1,4 @@
1
+ import { Axes } from '../axis/Axes.js';
1
2
  import { Boundary } from '../axis/boundary/Boundary.js';
2
3
  import { Lifecycle } from '../axis/lifecycle/Lifecycle.js';
3
4
  import { Role } from '../axis/role/Role.js';
@@ -20,11 +21,14 @@ export class InputValidator {
20
21
  return new InputValidator(fields, options);
21
22
  }
22
23
 
23
- onAbsent(field: Field): 'skip' | 'empty-list' | null {
24
- if (Boundary.of(field).readOnly) return 'skip';
25
- if (!Lifecycle.of(field).requiredAtCreate) return 'skip';
26
- if (Role.of(field).isCollection) return 'empty-list';
27
- return null;
24
+ /**
25
+ * The three REGISTERED axes decide, and the shape is not one of them — it could not answer
26
+ * anyway: `nullable(text())` and `optional(text())` both shape `['string', 'null']`, and only
27
+ * the second dispenses. JSON Schema draws the same line, where `required` is a list on the
28
+ * OBJECT and never a keyword on the property.
29
+ */
30
+ requires(field: Field): boolean {
31
+ return !Axes.all.some((axis) => axis.admitsAbsence?.(field));
28
32
  }
29
33
 
30
34
  /** Hands on the value it PARSED, so a handler never re-checks a row. */
@@ -43,7 +47,8 @@ export class InputValidator {
43
47
  for (const [key, field] of Object.entries(this.fields)) {
44
48
  const verdict = this.admit(field, data[key]);
45
49
  if (verdict === undefined) continue;
46
- if ('message' in verdict) errors.push({ path: [key, ...(verdict.path ?? [])], message: verdict.message });
50
+ if ('message' in verdict)
51
+ errors.push({ path: [key, ...(verdict.path ?? [])], message: verdict.message });
47
52
  else row[key] = verdict.value;
48
53
  }
49
54
 
@@ -61,8 +66,11 @@ export class InputValidator {
61
66
  private admit(field: Field, value: unknown): Verdict | undefined {
62
67
  if (value === undefined) return this.whenAbsent(field);
63
68
 
64
- if (Boundary.of(field).readOnly) return { message: InputRefusal.readOnly };
65
- if (this.options.patch && Lifecycle.of(field).immutable) return { message: InputRefusal.immutable };
69
+ if (Boundary.of(field).readOnly()) return { message: InputRefusal.readOnly };
70
+ // The other side of the relation carries the key: there is nothing to write here.
71
+ if (Role.of(field).isCollection()) return { message: InputRefusal.readOnly };
72
+ if (this.options.patch && Lifecycle.of(field).immutable())
73
+ return { message: InputRefusal.immutable };
66
74
 
67
75
  return FieldValueValidator.of(field).parse(value);
68
76
  }
@@ -70,9 +78,6 @@ export class InputValidator {
70
78
  private whenAbsent(field: Field): Verdict | undefined {
71
79
  if (this.options.patch) return undefined;
72
80
 
73
- const absence = this.onAbsent(field);
74
- if (absence === null) return { message: InputRefusal.required };
75
-
76
- return absence === 'empty-list' ? { value: [] } : undefined;
81
+ return this.requires(field) ? { message: InputRefusal.required } : undefined;
77
82
  }
78
83
  }
@@ -1,8 +1,8 @@
1
- import { Field } from '../../field/Field.js';
1
+ import { Field, type Described } from '../../field/Field.js';
2
2
  import { date } from '../primitive/date.js';
3
3
 
4
- export function created(): Field<Date> {
5
- return date().with({
4
+ export function created(opts?: Described): Field<Date> {
5
+ return date(opts).with({
6
6
  lifecycle: { create: 'now', update: 'forbidden' },
7
7
  });
8
8
  }
@@ -1,7 +1,8 @@
1
+ import { Field, type Described } from '../../field/Field.js';
1
2
  import { created } from './created.js';
2
- import { Field } from '../../field/Field.js';
3
3
 
4
- export function updated(): Field<Date> {
5
- const base = created();
4
+ export function updated(opts?: Described): Field<Date> {
5
+ const base = created(opts);
6
+
6
7
  return base.with({ lifecycle: { ...base.lifecycle, update: 'now' } });
7
8
  }
@@ -1,10 +1,5 @@
1
- import { Field } from '../../field/Field.js';
1
+ import { Field, type Shared } from '../../field/Field.js';
2
2
 
3
- export function bool(opts?: { default?: boolean; description?: string }): Field<boolean> {
4
- return new Field<boolean>({
5
- shape: { type: 'boolean' },
6
- lifecycle:
7
- opts?.default !== undefined ? { create: { value: opts.default } } : undefined,
8
- meta: opts?.description !== undefined ? { description: opts.description } : undefined,
9
- });
3
+ export function bool(opts?: Shared<boolean>): Field<boolean> {
4
+ return new Field<boolean>({ shape: { type: 'boolean' } }).setShared(opts);
10
5
  }
@@ -1,10 +1,11 @@
1
- import { Field } from '../../field/Field.js';
1
+ import { Field, type Shared } from '../../field/Field.js';
2
2
 
3
3
  /**
4
4
  * A `Date` in a handler, an ISO string on the wire — the `isoDate` boundary converts.
5
- * FR : un `Date` dans un handler, une chaîne ISO sur le fil — `isoDate` convertit.
6
5
  * `date()` → `{ type: 'string', format: 'date-time' }`
7
6
  */
8
- export function date(): Field<Date> {
9
- return new Field<Date>({ shape: { type: 'string', format: 'date-time' } });
7
+ export function date(opts?: Shared<Date>): Field<Date> {
8
+ return new Field<Date>({ shape: { type: 'string', format: 'date-time' } }).setShared(
9
+ opts,
10
+ );
10
11
  }
@@ -1,29 +1,44 @@
1
- import { Field } from '../../field/Field.js';
1
+ import { Field, type Shared } from '../../field/Field.js';
2
2
  import type { SchemaView } from '../../SchemaView.js';
3
3
  import { InputValidator } from '../../validator/InputValidator.js';
4
+ import { SchemaError } from '../../SchemaError.js';
4
5
 
5
- export function json<T = unknown>(): Field<T>;
6
+ type Entity = SchemaView & (new (...args: never[]) => unknown);
7
+
8
+ export function json<T = unknown>(opts?: Shared<T>): Field<T>;
6
9
  export function json<E extends SchemaView & (new (...args: any[]) => any)>(
7
10
  of: E,
11
+ opts?: Shared<InstanceType<E>>,
8
12
  ): Field<InstanceType<E>>;
13
+
9
14
  /**
10
- * `json(Address)` where the object has a shape; `json()` alone admits any shape forever.
11
- * FR : `json(Address)` quand l'objet a une forme ; `json()` seul admet tout, à jamais.
12
15
  * `json(Address)` → the entity's properties, and its required keys
13
16
  */
14
- export function json(of?: SchemaView): Field<unknown> {
15
- if (!of) return new Field({ shape: { type: 'object' } });
16
- const fields = of.getFields();
17
+ export function json(
18
+ of?: Entity | Shared<unknown>,
19
+ opts?: Shared<unknown>,
20
+ ): Field<unknown> {
21
+ const [schema, shared] = typeof of === 'function' ? [of, opts] : [undefined, of];
22
+
23
+ if (schema && typeof schema.getFields !== 'function')
24
+ throw new SchemaError(
25
+ 'json() takes an entity, such as json(Address) — got a function that is not one',
26
+ );
27
+
28
+ if (!schema) return new Field({ shape: { type: 'object' } }).setShared(shared);
29
+
30
+ const fields = schema.getFields();
17
31
  const validator = InputValidator.of(fields);
18
32
  const properties: Record<string, unknown> = {};
19
33
  const required: string[] = [];
34
+
20
35
  for (const [key, field] of Object.entries(fields)) {
21
- properties[key] = field.meta?.description
22
- ? { ...field.shape, description: field.meta.description }
23
- : field.shape;
24
- if (validator.onAbsent(field) === null) required.push(key);
36
+ properties[key] = field.shape;
37
+
38
+ if (validator.requires(field)) required.push(key);
25
39
  }
40
+
26
41
  return new Field({
27
42
  shape: { type: 'object', properties, ...(required.length ? { required } : {}) },
28
- });
43
+ }).setShared(shared);
29
44
  }
@@ -1,22 +1,29 @@
1
- import { Field } from '../../field/Field.js';
1
+ import { Field, type Shared } from '../../field/Field.js';
2
2
  import { SchemaError } from '../../SchemaError.js';
3
3
 
4
- export interface ListOptions {
4
+ export interface ListOptions<T = unknown> extends Shared<T[]> {
5
5
  min?: number;
6
6
  max?: number;
7
7
  }
8
8
 
9
9
  /**
10
- * A list of VALUES. The other side of a relation is `many()`, and passing one here throws.
11
- * FR : une liste de VALEURS. L'autre côté d'une relation est `many()`, et en passer une ici lève.
10
+ * A list of values. It keeps the shape of the item and nothing else: `nullable()` and a
11
+ * `description` write in the shape, so they work. A rule writes beside it, and an item of a
12
+ * list has no key, no column and no name to keep it on.
12
13
  * `list(text(), { max: 5 })` → `{ type: 'array', items: { type: 'string' }, maxItems: 5 }`
13
14
  */
14
- export function list<T>(item: Field<T>, opts?: ListOptions): Field<T[]> {
15
- if (item.role?.relation)
15
+ export function list<T>(field: Field<T>, opts?: ListOptions<T>): Field<T[]> {
16
+ if (field.hasAxes())
16
17
  throw new SchemaError(
17
- 'list() takes a value field (text(), number()…) — a relation has no value shape',
18
+ 'list() takes a value, not a rule. Put the rule on the list: unique(list(text())).',
18
19
  );
20
+
19
21
  return new Field<T[]>({
20
- shape: { type: 'array', items: item.shape, minItems: opts?.min, maxItems: opts?.max },
21
- });
22
+ shape: {
23
+ type: 'array',
24
+ items: field.shape,
25
+ minItems: opts?.min,
26
+ maxItems: opts?.max,
27
+ },
28
+ }).setShared(opts);
22
29
  }
@@ -1,17 +1,16 @@
1
1
  import { type EntityConstructor } from '../../axis/role/EntityConstructor.js';
2
2
  import { Relation } from '../../axis/role/Relation.js';
3
- import { Field } from '../../field/Field.js';
3
+ import { Field, type Described } from '../../field/Field.js';
4
4
 
5
5
  /**
6
- * The far side of a `ref()`: no column of its own, and absent from what a client sends.
7
- * FR : l'autre côté d'un `ref()` : aucune colonne à lui, et absent de ce qu'un client envoie.
8
6
  * `many(() => Post)` → `{ type: 'array' }` with `role.relation.kind` `'many'`
9
7
  */
10
8
  export function many<E extends EntityConstructor>(
11
9
  target: E | (() => E),
10
+ opts?: Described,
12
11
  ): Field<InstanceType<E>[]> {
13
12
  return new Field<InstanceType<E>[]>({
14
13
  shape: { type: 'array' },
15
14
  role: { relation: Relation.many(target) },
16
- });
15
+ }).setShared(opts);
17
16
  }
@@ -1,11 +1,9 @@
1
- import { Field } from '../../field/Field.js';
1
+ import { Field, type Shared } from '../../field/Field.js';
2
2
 
3
- export interface NumberOptions {
3
+ export interface NumberOptions extends Shared<number> {
4
4
  min?: number;
5
5
  max?: number;
6
6
  integer?: boolean;
7
- default?: number;
8
- description?: string;
9
7
  }
10
8
 
11
9
  export function number(opts?: NumberOptions): Field<number> {
@@ -15,8 +13,5 @@ export function number(opts?: NumberOptions): Field<number> {
15
13
  minimum: opts?.min,
16
14
  maximum: opts?.max,
17
15
  },
18
- lifecycle:
19
- opts?.default !== undefined ? { create: { value: opts.default } } : undefined,
20
- meta: opts?.description !== undefined ? { description: opts.description } : undefined,
21
- });
16
+ }).setShared(opts);
22
17
  }