@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
@@ -1,43 +1,39 @@
1
1
  import { Axes } from '../axis/Axes.js';
2
2
  import { dequal } from 'dequal';
3
3
  import { SchemaError } from '../SchemaError.js';
4
- const memberSlots = () => [...Axes.names, 'meta'];
5
4
  /**
6
- * Builds a `rule/` word, which states members on the field it receives.
7
- * FR : fabrique un mot de `rule/`, qui énonce des membres sur le champ reçu.
5
+ * Builds a `rule/` word
8
6
  * `vocabulary('indexed', () => ({ role: { index: true } }))`
9
7
  */
10
- export function vocabulary(name, states) {
11
- return (field) => field.with(merge(name, field, states(field)));
8
+ export function vocabulary(word, states) {
9
+ return (field) => field.with(merge(word, field, states(field)));
12
10
  }
13
11
  /**
14
- * Refuses two words stating one member differently, rather than letting the outer win.
15
- * FR : refuse deux mots qui énoncent un même membre différemment.
16
- * `readOnly(writeOnly(text()))` → both apply; `immutable(created())` → throws
12
+ * Refuses two words stating one member DIFFERENTLY, rather than letting the outer win.
13
+ * `readOnly(writeOnly(text()))` → both apply, they write two members; `immutable(created())`
14
+ * too, since both say `update: 'forbidden'`; `immutable(updated())` → throws.
17
15
  */
18
- function merge(name, field, given) {
16
+ function merge(word, field, changes) {
19
17
  const merged = {};
20
- const stated = given;
21
- const held = field;
22
- if ('shape' in given)
23
- merged.shape = given.shape;
24
- for (const slot of memberSlots()) {
25
- const members = stated[slot];
18
+ if ('shape' in changes)
19
+ merged.shape = changes.shape;
20
+ for (const name of Axes.names) {
21
+ const members = changes[name];
26
22
  if (members === undefined)
27
23
  continue;
28
- const already = held[slot];
29
- if (typeof members !== 'object' || members === null || typeof already !== 'object') {
30
- merged[slot] = members;
24
+ const already = field.axis(name);
25
+ if (typeof members !== 'object' || members === null || typeof already !== 'object' || already === null) {
26
+ merged[name] = members;
31
27
  continue;
32
28
  }
33
29
  for (const [member, value] of Object.entries(members)) {
34
- const previous = already?.[member];
30
+ const previous = already[member];
35
31
  if (previous === undefined || dequal(previous, value))
36
32
  continue;
37
- throw new SchemaError(`vocabulary: \`${name}\` states ${slot}.${member} = ${JSON.stringify(value)}, but the ` +
33
+ throw new SchemaError(`vocabulary: \`${word}\` states ${name}.${member} = ${JSON.stringify(value)}, but the ` +
38
34
  `field already states ${JSON.stringify(previous)}. Apply one or the other, not both.`);
39
35
  }
40
- merged[slot] = { ...already, ...members };
36
+ merged[name] = { ...already, ...members };
41
37
  }
42
38
  return merged;
43
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"vocabulary.js","sourceRoot":"","sources":["../../src/vocabulary/vocabulary.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,WAAW,GAAG,GAAa,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AAE5D;;;;GAIG;AACH,MAAM,UAAU,UAAU,CACxB,IAAY,EACZ,MAAwC;IAExC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC;AAID;;;;GAIG;AACH,SAAS,KAAK,CAAC,IAAY,EAAE,KAAY,EAAE,KAAqB;IAC9D,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,MAAM,IAAI,GAAG,KAA2C,CAAC;IACzD,IAAI,OAAO,IAAI,KAAK;QAAE,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAEjD,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,EAAE,CAAC;QACjC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,OAAO,KAAK,SAAS;YAAE,SAAS;QACpC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAwC,CAAC;QAClE,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YACnF,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC;YACvB,SAAS;QACX,CAAC;QACD,KAAK,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YACtD,MAAM,QAAQ,GAAG,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC;YACnC,IAAI,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC;gBAAE,SAAS;YAChE,MAAM,IAAI,WAAW,CACnB,iBAAiB,IAAI,aAAa,IAAI,IAAI,MAAM,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,YAAY;gBACrF,wBAAwB,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,qCAAqC,CACxF,CAAC;QACJ,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC;IAC5C,CAAC;IACD,OAAO,MAAwB,CAAC;AAClC,CAAC"}
1
+ {"version":3,"file":"vocabulary.js","sourceRoot":"","sources":["../../src/vocabulary/vocabulary.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD;;;GAGG;AACH,MAAM,UAAU,UAAU,CACxB,IAAY,EACZ,MAAwC;IAExC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC;AAID;;;;GAIG;AACH,SAAS,KAAK,CAAC,IAAY,EAAE,KAAY,EAAE,OAAkC;IAC3E,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,IAAI,OAAO,IAAI,OAAO;QAAE,MAAM,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAErD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAmC,EAAE,CAAC;QAC5D,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,OAAO,KAAK,SAAS;YAAE,SAAS;QACpC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACvG,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC;YACvB,SAAS;QACX,CAAC;QACD,KAAK,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YACtD,MAAM,QAAQ,GAAI,OAAmC,CAAC,MAAM,CAAC,CAAC;YAC9D,IAAI,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC;gBAAE,SAAS;YAChE,MAAM,IAAI,WAAW,CACnB,iBAAiB,IAAI,aAAa,IAAI,IAAI,MAAM,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,YAAY;gBACrF,wBAAwB,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,qCAAqC,CACxF,CAAC;QACJ,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC;IAC5C,CAAC;IAED,OAAO,MAAwB,CAAC;AAClC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fougere/schema",
3
- "version": "0.12.1-alpha.0",
3
+ "version": "0.12.2-alpha.0",
4
4
  "description": "The entity and its 4 axes — shape, role, lifecycle, boundary. Subject-owned judgments, edge-safe.",
5
5
  "keywords": [
6
6
  "fougere",
package/src/Schema.ts CHANGED
@@ -2,7 +2,7 @@ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
2
 
3
3
  import { Boundary } from './axis/boundary/Boundary.js';
4
4
  import { type Fields } from './field/Fields.js';
5
- import { type CompositeUnique } from './entity/CompositeUnique.js';
5
+ import { type FieldGroups } from './entity/FieldGroups.js';
6
6
  import { type EntityDeclarations } from './entity/EntityDeclarations.js';
7
7
  import { type PreviousNames } from './entity/PreviousNames.js';
8
8
  import { type EntityAdapters } from './entity/EntityAdapters.js';
@@ -16,6 +16,7 @@ import type { SchemaView } from './SchemaView.js';
16
16
 
17
17
  import type { SchemaConstructor } from './SchemaConstructor.js';
18
18
  import { SchemaError } from './SchemaError.js';
19
+ import type { UnionToIntersection } from './lib/UnionToIntersection.js';
19
20
 
20
21
  export const ANONYMOUS_SCHEMA_NAME = 'Schema';
21
22
 
@@ -53,9 +54,13 @@ export class Schema {
53
54
  static getAdapters(): EntityAdapters<Fields> {
54
55
  return this.definition.adapterSet.adapters;
55
56
  }
56
- static getUnique(): CompositeUnique<Fields> | undefined {
57
+ static getUnique(): FieldGroups<Fields> | undefined {
57
58
  return this.definition.constraints.unique;
58
59
  }
60
+
61
+ static getIndex(): FieldGroups<Fields> | undefined {
62
+ return this.definition.constraints.index;
63
+ }
59
64
  static getOpts(): ValidateOptions {
60
65
  return this.definition.opts;
61
66
  }
@@ -78,6 +83,7 @@ export class Schema {
78
83
  const decoded = Boundary.of(field).decode(value);
79
84
  row[key] = 'message' in decoded ? value : decoded.value;
80
85
  }
86
+
81
87
  return row;
82
88
  }
83
89
 
@@ -90,6 +96,7 @@ export class Schema {
90
96
  validate(value: unknown) {
91
97
  const verdict = InputValidator.of(fields, opts).validate(value);
92
98
  if (verdict.success) return { value: verdict.data };
99
+
93
100
  return {
94
101
  issues: verdict.errors.map((e) => ({
95
102
  message: e.message,
@@ -138,6 +145,7 @@ export class Schema {
138
145
  );
139
146
  }
140
147
  Object.defineProperty(this, 'name', { value: name, configurable: true });
148
+
141
149
  return this;
142
150
  }
143
151
 
@@ -168,16 +176,11 @@ export class Schema {
168
176
  value: ANONYMOUS_SCHEMA_NAME,
169
177
  configurable: true,
170
178
  });
179
+
171
180
  return Derived as unknown as SchemaConstructor<Fields>;
172
181
  }
173
182
  }
174
183
 
175
- type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (
176
- k: infer I,
177
- ) => void
178
- ? I
179
- : never;
180
-
181
184
  type Merged<T extends SchemaView[]> = UnionToIntersection<
182
185
  T[number] extends { getFields(): infer F } ? F : Fields
183
186
  > &
@@ -1,19 +1,21 @@
1
1
  import { type FieldName } from './field/FieldName.js';
2
2
  import { type Fields } from './field/Fields.js';
3
- import { type CompositeUnique } from './entity/CompositeUnique.js';
3
+ import { type FieldGroups } from './entity/FieldGroups.js';
4
4
 
5
5
  /**
6
- * What a schema constrains beyond any single field.
7
- *
8
- * `unique` is the one member today. It sits under an owner rather than beside `fields`
9
- * and `opts` because the next rule spanning two fields — a check, an exclusion — is a
10
- * member here, not an eighth positional argument in a list that has already lost one.
6
+ * What a schema states beyond any single field: groups of names. `unique` constrains the
7
+ * rows, `index` says how they are reached — two decisions, and neither implies the other.
8
+ * They sit under an owner rather than beside `fields` and `opts` because the next rule
9
+ * spanning two fields — a check, an exclusion — is a member here, not an eighth positional
10
+ * argument in a list that has already lost one.
11
11
  */
12
12
  export class SchemaConstraints<TFields extends Fields = Fields> {
13
- readonly unique?: CompositeUnique<TFields>;
13
+ readonly unique?: FieldGroups<TFields>;
14
+ readonly index?: FieldGroups<TFields>;
14
15
 
15
- private constructor(unique?: CompositeUnique<TFields>) {
16
+ private constructor(unique?: FieldGroups<TFields>, index?: FieldGroups<TFields>) {
16
17
  if (unique) this.unique = unique;
18
+ if (index) this.index = index;
17
19
  }
18
20
 
19
21
  static readonly none = new SchemaConstraints();
@@ -24,23 +26,39 @@ export class SchemaConstraints<TFields extends Fields = Fields> {
24
26
  * `SchemaConstraints.of([['a','b'], ['a','b']])` → `unique` holds one group
25
27
  */
26
28
  static of<TFields extends Fields>(
27
- groups: readonly (readonly FieldName<TFields>[])[],
29
+ groups: { unique?: FieldGroups<TFields>; index?: FieldGroups<TFields> },
28
30
  ): SchemaConstraints<TFields> {
29
- const seen = new Map<string, readonly FieldName<TFields>[]>();
30
- for (const group of groups) seen.set(JSON.stringify(group), group);
31
-
32
- return new SchemaConstraints(seen.size ? [...seen.values()] : undefined);
31
+ return new SchemaConstraints(deduped(groups.unique), deduped(groups.index));
33
32
  }
34
33
 
35
- /** One member gone and the group is gone: it constrained a pair that no longer exists. */
34
+ /** One member gone and the group is gone: it named a pair that no longer exists. */
36
35
  renamed(transform: (key: string) => string | undefined): SchemaConstraints {
37
- if (!this.unique) return SchemaConstraints.none;
38
- const kept: string[][] = [];
39
- for (const group of this.unique) {
40
- const renamed = group.map(transform);
41
- if (renamed.every((key): key is string => key !== undefined)) kept.push(renamed);
42
- }
43
-
44
- return SchemaConstraints.of(kept);
36
+ return SchemaConstraints.of({
37
+ unique: survivors(this.unique, transform),
38
+ index: survivors(this.index, transform),
39
+ });
45
40
  }
46
41
  }
42
+
43
+ const deduped = <TFields extends Fields>(
44
+ groups?: FieldGroups<TFields>,
45
+ ): FieldGroups<TFields> | undefined => {
46
+ if (!groups) return undefined;
47
+ const seen = new Map<string, readonly FieldName<TFields>[]>();
48
+ for (const group of groups) seen.set(JSON.stringify(group), group);
49
+
50
+ return seen.size ? [...seen.values()] : undefined;
51
+ };
52
+
53
+ const survivors = (
54
+ groups: FieldGroups<Fields> | undefined,
55
+ transform: (key: string) => string | undefined,
56
+ ): string[][] => {
57
+ const kept: string[][] = [];
58
+ for (const group of groups ?? []) {
59
+ const renamed = group.map(transform);
60
+ if (renamed.every((key): key is string => key !== undefined)) kept.push(renamed);
61
+ }
62
+
63
+ return kept;
64
+ };
@@ -113,7 +113,7 @@ export class SchemaDefinition {
113
113
  ...addressed,
114
114
  ...Object.keys(declarations.previous ?? {}),
115
115
  ]);
116
- const declared = FieldSet.declaring(this.fields, declarations.unique);
116
+ const declared = FieldSet.declaring(this.fields, declarations);
117
117
 
118
118
  return this.restated({
119
119
  fields: declared.fields,
@@ -123,9 +123,9 @@ export class SchemaDefinition {
123
123
  ]),
124
124
  previous: declarations.previous ?? this.previous,
125
125
  constraints:
126
- declarations.unique === undefined
126
+ declarations.unique === undefined && declarations.index === undefined
127
127
  ? this.constraints
128
- : SchemaConstraints.of(declared.groups),
128
+ : SchemaConstraints.of(declared),
129
129
  });
130
130
  }
131
131
 
@@ -199,11 +199,13 @@ export class SchemaDefinition {
199
199
  static merged(views: readonly SchemaView[]): SchemaDefinition {
200
200
  const fields: Fields = {};
201
201
  let opts: ValidateOptions = {};
202
- const groups: (readonly string[])[] = [];
202
+ const groups: { unique: (readonly string[])[]; index: (readonly string[])[] } =
203
+ { unique: [], index: [] };
203
204
  for (const view of views) {
204
205
  Object.assign(fields, view.getFields());
205
206
  opts = { ...opts, ...view.getOpts() };
206
- groups.push(...(view.getUnique() ?? []));
207
+ groups.unique.push(...(view.getUnique() ?? []));
208
+ groups.index.push(...(view.getIndex() ?? []));
207
209
  }
208
210
 
209
211
  return SchemaDefinition.derived({
@@ -1,7 +1,13 @@
1
1
  export class SchemaError extends Error {
2
- constructor(message: string) {
3
- super(message);
2
+ constructor(message: string, options?: { received: unknown }) {
3
+ super(
4
+ options ? `${message} — got ${SchemaError.inspect(options.received)}` : message,
5
+ );
4
6
 
5
7
  this.name = 'SchemaError';
6
8
  }
9
+
10
+ static inspect(value: unknown): string {
11
+ return JSON.stringify(value) ?? typeof value;
12
+ }
7
13
  }
package/src/SchemaView.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Fields } from './field/Fields.js';
2
- import type { CompositeUnique } from './entity/CompositeUnique.js';
2
+ import type { FieldGroups } from './entity/FieldGroups.js';
3
3
  import type { PreviousNames } from './entity/PreviousNames.js';
4
4
  import type { EntityAdapters } from './entity/EntityAdapters.js';
5
5
  import type { ValidationResult } from './lib/ValidationResult.js';
@@ -17,7 +17,8 @@ export interface SchemaView<TFields extends Fields = Fields> {
17
17
  readonly anchored?: boolean;
18
18
  getFields(): TFields;
19
19
  getAdapters(): EntityAdapters<TFields>;
20
- getUnique(): CompositeUnique<TFields> | undefined;
20
+ getUnique(): FieldGroups<TFields> | undefined;
21
+ getIndex(): FieldGroups<TFields> | undefined;
21
22
  getOpts(): ValidateOptions;
22
23
  validate(input: unknown): ValidationResult<Values<TFields>>;
23
24
  }
package/src/axis/Axes.ts CHANGED
@@ -9,10 +9,10 @@ import { Role } from './role/Role.js';
9
9
  class AxisRegistry extends Registry<Axis> {
10
10
  private composed?: JsonSchema;
11
11
 
12
- register(slot: string, axis: Axis): Axis {
12
+ register(name: string, axis: Axis): Axis {
13
13
  this.composed = undefined;
14
14
 
15
- return super.register(slot, axis);
15
+ return super.register(name, axis);
16
16
  }
17
17
 
18
18
  /** The document a field declaration is judged against, rebuilt the next time one arrives. */
@@ -27,7 +27,7 @@ class AxisRegistry extends Registry<Axis> {
27
27
  * The axes this process reads, and the door an axis declared elsewhere comes through.
28
28
  * `Axes.register('tenancy', Tenancy)` — from a `vocabulary/` file, read before `entities/`
29
29
  */
30
- export const Axes = new AxisRegistry('axis', 'call Axes.register(slot, axis)', [
30
+ export const Axes = new AxisRegistry('axis', 'call Axes.register(name, axis)', [
31
31
  ['role', Role],
32
32
  ['lifecycle', Lifecycle],
33
33
  ['boundary', Boundary],
package/src/axis/Axis.ts CHANGED
@@ -1,9 +1,20 @@
1
1
  import type { Format } from '../lib/Format.js';
2
+ import type { Field } from '../field/Field.js';
2
3
  import type { ValidationError } from '../lib/ValidationError.js';
3
4
 
4
5
  export interface Axis {
5
6
  readonly format: Format;
6
7
 
7
- /** What a format cannot state: `role.relation.to` is a function, and JSON holds none. */
8
- refusals?(value: unknown): ValidationError[];
8
+ /**
9
+ * What a format cannot state: `role.relation.to` is a function, and JSON holds none.
10
+ * The whole declaration comes with the name, because a contradiction is a PAIR — a
11
+ * primary key that admits null states one legal `role` beside one legal `shape`.
12
+ */
13
+ refusals?(value: unknown, declaration: Record<string, unknown>): ValidationError[];
14
+
15
+ /**
16
+ * Whether this axis alone lets the field be absent from what a caller sends. Asked of every
17
+ * REGISTERED axis, so a fourth one weighs on it without the validator naming it.
18
+ */
19
+ admitsAbsence?(field: Field): boolean;
9
20
  }
@@ -47,9 +47,15 @@ export class Boundary {
47
47
 
48
48
  const alias = Boundaries.aliases.find(ref);
49
49
  if (!alias) throw new SchemaError(`Unknown boundary alias: '${ref}'`);
50
+
50
51
  return new Boundary(alias);
51
52
  }
52
53
 
54
+ /** The server fills it, so a caller never sends it. */
55
+ static admitsAbsence(field: Field): boolean {
56
+ return this.of(field).readOnly();
57
+ }
58
+
53
59
  static of(field: Field): Boundary {
54
60
  const declared = Boundary.declared(field);
55
61
  const derived = Boundary.forShape(field.shape);
@@ -57,6 +63,7 @@ export class Boundary {
57
63
  in: declared.rules.in ?? derived.rules.in,
58
64
  out: declared.rules.out ?? derived.rules.out,
59
65
  };
66
+
60
67
  return new Boundary(rules, {
61
68
  decode:
62
69
  typeof rules.in === 'object'
@@ -72,6 +79,7 @@ export class Boundary {
72
79
  /** `date-time` means a `Date` on both sides, without a word in the entity. */
73
80
  static forShape(shape: Shape | undefined): Boundary {
74
81
  if (Shapes.typeOf(shape) === 'date') return new Boundary(Boundaries.aliases.resolve('isoDate'));
82
+
75
83
  return new Boundary();
76
84
  }
77
85
 
@@ -80,11 +88,17 @@ export class Boundary {
80
88
  return { in: overrides.in ?? this.rules.in, out: overrides.out ?? this.rules.out };
81
89
  }
82
90
 
83
- get readOnly(): boolean {
91
+ readOnly(): boolean {
84
92
  return this.rules.in === 'closed';
85
93
  }
86
94
 
87
- get writeOnly(): boolean {
95
+ writeOnly(): boolean {
88
96
  return this.rules.out === 'closed';
89
97
  }
90
98
  }
99
+
100
+ declare module '../../FougereFieldAxes.js' {
101
+ interface FougereFieldAxes {
102
+ readonly boundary?: BoundaryRef;
103
+ }
104
+ }
@@ -33,8 +33,10 @@ Boundaries.decoders.register('isoDate', (value) => {
33
33
  if (value instanceof Date) return { value };
34
34
  if (typeof value === 'string') {
35
35
  const date = new Date(value);
36
+
36
37
  return Number.isNaN(date.getTime()) ? { message: 'Invalid date' } : { value: date };
37
38
  }
39
+
38
40
  return { message: 'Expected a date' };
39
41
  });
40
42
  Boundaries.encoders.register('isoDate', (value) =>
@@ -23,6 +23,11 @@ export class Lifecycle {
23
23
 
24
24
  private constructor(private readonly rules: LifecycleRules = {}) {}
25
25
 
26
+ /** `optional()`, a default, a generator: the rule says the value need not arrive. */
27
+ static admitsAbsence(field: { lifecycle?: LifecycleRules }): boolean {
28
+ return !this.of(field).requiredAtCreate();
29
+ }
30
+
26
31
  static of(field: { lifecycle?: LifecycleRules }): Lifecycle {
27
32
  return new Lifecycle(field.lifecycle);
28
33
  }
@@ -47,19 +52,19 @@ export class Lifecycle {
47
52
  return { value: Generators.resolve(rule.generate)() };
48
53
  }
49
54
 
50
- get requiredAtCreate(): boolean {
55
+ requiredAtCreate(): boolean {
51
56
  return this.create === undefined;
52
57
  }
53
58
 
54
- get stampedAtCreate(): boolean {
59
+ stampedAtCreate(): boolean {
55
60
  return this.create === 'now';
56
61
  }
57
62
 
58
- get immutable(): boolean {
63
+ immutable(): boolean {
59
64
  return this.update === 'forbidden';
60
65
  }
61
66
 
62
- get stampedOnUpdate(): boolean {
67
+ stampedOnUpdate(): boolean {
63
68
  return this.update === 'now';
64
69
  }
65
70
 
@@ -71,3 +76,9 @@ export class Lifecycle {
71
76
  : undefined;
72
77
  }
73
78
  }
79
+
80
+ declare module '../../FougereFieldAxes.js' {
81
+ interface FougereFieldAxes {
82
+ readonly lifecycle?: LifecycleRules;
83
+ }
84
+ }
@@ -39,7 +39,7 @@ export function applyUpdate(
39
39
  const instant = Date.now();
40
40
 
41
41
  for (const [name, field] of Object.entries(fields) as [string, Field][]) {
42
- if (Lifecycle.of(field).stampedOnUpdate && !(name in values))
42
+ if (Lifecycle.of(field).stampedOnUpdate() && !(name in values))
43
43
  values[name] = new Date(instant);
44
44
  }
45
45
 
@@ -1,6 +1,8 @@
1
1
  import type { Relation } from './Relation.js';
2
2
  import type { EntityConstructor } from './EntityConstructor.js';
3
3
  import { isObject } from '../../lib/utils.js';
4
+ import { Shapes, type Shape } from '../shape/Shape.js';
5
+ import type { ValidationError } from '../../lib/ValidationError.js';
4
6
  import { Format, type Accepted } from '../../lib/Format.js';
5
7
  import { ON_DELETE, RELATION_KINDS } from './Relation.js';
6
8
 
@@ -20,42 +22,66 @@ export class Role {
20
22
  )
21
23
  .closed();
22
24
 
23
- static refusals(value: unknown) {
25
+ static refusals(value: unknown, declaration: Record<string, unknown> = {}): ValidationError[] {
26
+ const role = Role.of({ role: isObject(value) ? (value as RoleRules) : undefined });
27
+ const errors: ValidationError[] = [];
24
28
  const relation = isObject(value) ? value.relation : undefined;
25
- if (!isObject(relation) || !('to' in relation) || typeof relation.to === 'function') return [];
26
29
 
27
- return [{ path: ['role', 'relation', 'to'], message: 'Expected a function returning the target entity, such as () => Post' }];
30
+ if (isObject(relation) && 'to' in relation && typeof relation.to !== 'function')
31
+ errors.push({
32
+ path: ['role', 'relation', 'to'],
33
+ message: 'Expected a function returning the target entity, such as () => Post',
34
+ });
35
+
36
+ if (role.isPrimary() && Shapes.isNullable(declaration.shape as Shape))
37
+ errors.push({
38
+ path: ['role', 'primary'],
39
+ message: 'A primary key admits no null — a row is addressed by it',
40
+ });
41
+
42
+ if (role.isCollection() && (role.isUnique() || role.isIndexed()))
43
+ errors.push({
44
+ path: ['role', role.isUnique() ? 'unique' : 'index'],
45
+ message: 'A collection has no column of its own, so there is nothing to index',
46
+ });
47
+
48
+ return errors;
28
49
  }
29
50
 
30
51
  private constructor(private readonly rules: RoleRules = {}) {}
31
52
 
53
+ /** A collection has no column: the far side of the relation carries the key. */
54
+ static admitsAbsence(field: { role?: RoleRules }): boolean {
55
+ return this.of(field).isCollection();
56
+ }
57
+
32
58
  static of(field: { role?: RoleRules }): Role {
33
59
  return new Role(field.role);
34
60
  }
35
61
 
36
- get isPrimary(): boolean {
62
+ isPrimary(): boolean {
37
63
  return this.rules.primary === true;
38
64
  }
39
65
 
40
- get isIndexed(): boolean {
66
+ isIndexed(): boolean {
41
67
  return this.rules.index === true;
42
68
  }
43
69
 
44
70
  /** A field unique on its own. A group spanning several belongs to the schema. */
45
- get isUnique(): boolean {
71
+ isUnique(): boolean {
46
72
  return this.rules.unique === true;
47
73
  }
48
74
 
49
- get isCollection(): boolean {
75
+ isCollection(): boolean {
50
76
  return this.rules.relation?.kind === 'many';
51
77
  }
52
78
 
53
- get isReference(): boolean {
79
+ isReference(): boolean {
54
80
  return this.rules.relation?.kind === 'one';
55
81
  }
56
82
 
57
83
  /** Either kind — what a CLI flag and a GraphQL selection both leave out. */
58
- get isRelation(): boolean {
84
+ isRelation(): boolean {
59
85
  return this.rules.relation !== undefined;
60
86
  }
61
87
 
@@ -70,3 +96,9 @@ export class Role {
70
96
  }
71
97
 
72
98
  export type RoleRules = Accepted<typeof Role.format>;
99
+
100
+ declare module '../../FougereFieldAxes.js' {
101
+ interface FougereFieldAxes {
102
+ readonly role?: RoleRules;
103
+ }
104
+ }