@fougere/schema 0.8.2-alpha.0 → 0.8.4-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 (219) hide show
  1. package/dist/Schema.d.ts +3 -6
  2. package/dist/Schema.d.ts.map +1 -1
  3. package/dist/Schema.js +9 -17
  4. package/dist/Schema.js.map +1 -1
  5. package/dist/SchemaDefinition.d.ts +24 -6
  6. package/dist/SchemaDefinition.d.ts.map +1 -1
  7. package/dist/SchemaDefinition.js +76 -44
  8. package/dist/SchemaDefinition.js.map +1 -1
  9. package/dist/SchemaView.d.ts +1 -1
  10. package/dist/SchemaView.d.ts.map +1 -1
  11. package/dist/axis/Axis.d.ts +1 -1
  12. package/dist/axis/Axis.d.ts.map +1 -1
  13. package/dist/axis/boundary/Boundaries.d.ts +5 -0
  14. package/dist/axis/boundary/Boundaries.d.ts.map +1 -1
  15. package/dist/axis/boundary/Boundaries.js.map +1 -1
  16. package/dist/axis/boundary/Boundary.d.ts +5 -4
  17. package/dist/axis/boundary/Boundary.d.ts.map +1 -1
  18. package/dist/axis/boundary/Boundary.js +4 -5
  19. package/dist/axis/boundary/Boundary.js.map +1 -1
  20. package/dist/axis/boundary/BoundaryAxis.js +2 -2
  21. package/dist/axis/boundary/BoundaryAxis.js.map +1 -1
  22. package/dist/axis/lifecycle/Lifecycle.d.ts +5 -3
  23. package/dist/axis/lifecycle/Lifecycle.d.ts.map +1 -1
  24. package/dist/axis/lifecycle/Lifecycle.js +5 -1
  25. package/dist/axis/lifecycle/Lifecycle.js.map +1 -1
  26. package/dist/axis/lifecycle/LifecycleAxis.js +6 -6
  27. package/dist/axis/lifecycle/LifecycleAxis.js.map +1 -1
  28. package/dist/axis/lifecycle/apply.d.ts +6 -6
  29. package/dist/axis/lifecycle/apply.d.ts.map +1 -1
  30. package/dist/axis/lifecycle/apply.js +10 -9
  31. package/dist/axis/lifecycle/apply.js.map +1 -1
  32. package/dist/axis/role/Role.d.ts +8 -5
  33. package/dist/axis/role/Role.d.ts.map +1 -1
  34. package/dist/axis/role/Role.js +7 -0
  35. package/dist/axis/role/Role.js.map +1 -1
  36. package/dist/axis/role/RoleAxis.js +9 -9
  37. package/dist/axis/role/RoleAxis.js.map +1 -1
  38. package/dist/axis/shape/Shape.d.ts +14 -0
  39. package/dist/axis/shape/Shape.d.ts.map +1 -1
  40. package/dist/axis/shape/Shape.js +43 -34
  41. package/dist/axis/shape/Shape.js.map +1 -1
  42. package/dist/entity/EntityAdapterSet.d.ts +2 -2
  43. package/dist/entity/EntityAdapterSet.d.ts.map +1 -1
  44. package/dist/entity/EntityAdapterSet.js +2 -2
  45. package/dist/entity/EntityAdapterSet.js.map +1 -1
  46. package/dist/entity.d.ts +2 -2
  47. package/dist/entity.js +2 -2
  48. package/dist/field/Field.d.ts.map +1 -1
  49. package/dist/field/Field.js +2 -1
  50. package/dist/field/Field.js.map +1 -1
  51. package/dist/field/FieldSet.d.ts +4 -7
  52. package/dist/field/FieldSet.d.ts.map +1 -1
  53. package/dist/field/FieldSet.js +4 -15
  54. package/dist/field/FieldSet.js.map +1 -1
  55. package/dist/index.d.ts +2 -2
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +1 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/lib/validation.d.ts +32 -0
  60. package/dist/lib/validation.d.ts.map +1 -0
  61. package/dist/lib/validation.js +6 -0
  62. package/dist/lib/validation.js.map +1 -0
  63. package/dist/projection/Cases.d.ts +1 -1
  64. package/dist/projection/Cases.d.ts.map +1 -1
  65. package/dist/projection/Cases.js +11 -7
  66. package/dist/projection/Cases.js.map +1 -1
  67. package/dist/projection/Visibility.js +1 -1
  68. package/dist/projection/Visibility.js.map +1 -1
  69. package/dist/projection/card/Bundle.d.ts.map +1 -1
  70. package/dist/projection/card/Bundle.js +1 -4
  71. package/dist/projection/card/Bundle.js.map +1 -1
  72. package/dist/projection/card/Card.d.ts +7 -1
  73. package/dist/projection/card/Card.d.ts.map +1 -1
  74. package/dist/projection/card/Card.js +25 -133
  75. package/dist/projection/card/Card.js.map +1 -1
  76. package/dist/projection/card/Descriptor.d.ts +2 -2
  77. package/dist/projection/card/Descriptor.d.ts.map +1 -1
  78. package/dist/projection/card/admission.d.ts +12 -11
  79. package/dist/projection/card/admission.d.ts.map +1 -1
  80. package/dist/projection/card/admission.js +13 -11
  81. package/dist/projection/card/admission.js.map +1 -1
  82. package/dist/projection/card/diff.d.ts +7 -1
  83. package/dist/projection/card/diff.d.ts.map +1 -1
  84. package/dist/projection/card/diff.js +111 -1
  85. package/dist/projection/card/diff.js.map +1 -1
  86. package/dist/validator/FieldDeclarationValidator.d.ts +1 -1
  87. package/dist/validator/FieldDeclarationValidator.d.ts.map +1 -1
  88. package/dist/validator/FieldDeclarationValidator.js +4 -4
  89. package/dist/validator/FieldDeclarationValidator.js.map +1 -1
  90. package/dist/validator/FieldValueValidator.d.ts +6 -1
  91. package/dist/validator/FieldValueValidator.d.ts.map +1 -1
  92. package/dist/validator/FieldValueValidator.js +36 -7
  93. package/dist/validator/FieldValueValidator.js.map +1 -1
  94. package/dist/validator/InputValidator.d.ts +1 -1
  95. package/dist/validator/InputValidator.d.ts.map +1 -1
  96. package/dist/validator/InputValidator.js +8 -18
  97. package/dist/validator/InputValidator.js.map +1 -1
  98. package/dist/vocabulary/preset/created.d.ts +0 -5
  99. package/dist/vocabulary/preset/created.d.ts.map +1 -1
  100. package/dist/vocabulary/preset/created.js +0 -5
  101. package/dist/vocabulary/preset/created.js.map +1 -1
  102. package/dist/vocabulary/preset/email.d.ts +0 -5
  103. package/dist/vocabulary/preset/email.d.ts.map +1 -1
  104. package/dist/vocabulary/preset/email.js +0 -5
  105. package/dist/vocabulary/preset/email.js.map +1 -1
  106. package/dist/vocabulary/preset/updated.d.ts +0 -5
  107. package/dist/vocabulary/preset/updated.d.ts.map +1 -1
  108. package/dist/vocabulary/preset/updated.js +0 -5
  109. package/dist/vocabulary/preset/updated.js.map +1 -1
  110. package/dist/vocabulary/preset/url.d.ts +0 -5
  111. package/dist/vocabulary/preset/url.d.ts.map +1 -1
  112. package/dist/vocabulary/preset/url.js +0 -5
  113. package/dist/vocabulary/preset/url.js.map +1 -1
  114. package/dist/vocabulary/primitive/bool.d.ts +0 -5
  115. package/dist/vocabulary/primitive/bool.d.ts.map +1 -1
  116. package/dist/vocabulary/primitive/bool.js +0 -5
  117. package/dist/vocabulary/primitive/bool.js.map +1 -1
  118. package/dist/vocabulary/primitive/date.d.ts +3 -3
  119. package/dist/vocabulary/primitive/date.js +3 -3
  120. package/dist/vocabulary/primitive/json.js +3 -3
  121. package/dist/vocabulary/primitive/list.d.ts +3 -3
  122. package/dist/vocabulary/primitive/list.js +3 -3
  123. package/dist/vocabulary/primitive/many.d.ts +3 -3
  124. package/dist/vocabulary/primitive/many.js +3 -3
  125. package/dist/vocabulary/primitive/number.d.ts +0 -5
  126. package/dist/vocabulary/primitive/number.d.ts.map +1 -1
  127. package/dist/vocabulary/primitive/number.js +0 -5
  128. package/dist/vocabulary/primitive/number.js.map +1 -1
  129. package/dist/vocabulary/primitive/oneOf.js +3 -3
  130. package/dist/vocabulary/primitive/primary.js +3 -3
  131. package/dist/vocabulary/primitive/ref.d.ts +0 -5
  132. package/dist/vocabulary/primitive/ref.d.ts.map +1 -1
  133. package/dist/vocabulary/primitive/ref.js +0 -5
  134. package/dist/vocabulary/primitive/ref.js.map +1 -1
  135. package/dist/vocabulary/primitive/text.d.ts +0 -5
  136. package/dist/vocabulary/primitive/text.d.ts.map +1 -1
  137. package/dist/vocabulary/primitive/text.js +0 -5
  138. package/dist/vocabulary/primitive/text.js.map +1 -1
  139. package/dist/vocabulary/rule/immutable.d.ts +0 -5
  140. package/dist/vocabulary/rule/immutable.d.ts.map +1 -1
  141. package/dist/vocabulary/rule/immutable.js +0 -5
  142. package/dist/vocabulary/rule/immutable.js.map +1 -1
  143. package/dist/vocabulary/rule/indexed.d.ts +0 -5
  144. package/dist/vocabulary/rule/indexed.d.ts.map +1 -1
  145. package/dist/vocabulary/rule/indexed.js +0 -5
  146. package/dist/vocabulary/rule/indexed.js.map +1 -1
  147. package/dist/vocabulary/rule/nullable.d.ts +3 -3
  148. package/dist/vocabulary/rule/nullable.js +3 -3
  149. package/dist/vocabulary/rule/optional.d.ts +3 -3
  150. package/dist/vocabulary/rule/optional.js +3 -3
  151. package/dist/vocabulary/rule/readOnly.d.ts +3 -3
  152. package/dist/vocabulary/rule/readOnly.js +4 -4
  153. package/dist/vocabulary/rule/readOnly.js.map +1 -1
  154. package/dist/vocabulary/rule/unique.d.ts +0 -5
  155. package/dist/vocabulary/rule/unique.d.ts.map +1 -1
  156. package/dist/vocabulary/rule/unique.js +0 -5
  157. package/dist/vocabulary/rule/unique.js.map +1 -1
  158. package/dist/vocabulary/rule/writeOnly.d.ts +2 -2
  159. package/dist/vocabulary/rule/writeOnly.js +3 -3
  160. package/dist/vocabulary/rule/writeOnly.js.map +1 -1
  161. package/dist/vocabulary/vocabulary.d.ts +2 -2
  162. package/dist/vocabulary/vocabulary.js +5 -5
  163. package/package.json +1 -1
  164. package/src/Schema.ts +14 -20
  165. package/src/SchemaDefinition.ts +74 -48
  166. package/src/SchemaView.ts +1 -1
  167. package/src/axis/Axis.ts +1 -1
  168. package/src/axis/boundary/Boundaries.ts +5 -0
  169. package/src/axis/boundary/Boundary.ts +7 -9
  170. package/src/axis/boundary/BoundaryAxis.ts +2 -2
  171. package/src/axis/lifecycle/Lifecycle.ts +9 -4
  172. package/src/axis/lifecycle/LifecycleAxis.ts +7 -7
  173. package/src/axis/lifecycle/apply.ts +10 -9
  174. package/src/axis/role/Role.ts +14 -5
  175. package/src/axis/role/RoleAxis.ts +10 -10
  176. package/src/axis/shape/Shape.ts +61 -37
  177. package/src/entity/EntityAdapterSet.ts +2 -2
  178. package/src/entity.ts +2 -2
  179. package/src/field/Field.ts +2 -1
  180. package/src/field/FieldSet.ts +6 -20
  181. package/src/index.ts +2 -2
  182. package/src/lib/validation.ts +25 -0
  183. package/src/projection/Cases.ts +13 -8
  184. package/src/projection/Visibility.ts +1 -1
  185. package/src/projection/card/Bundle.ts +1 -4
  186. package/src/projection/card/Card.ts +26 -159
  187. package/src/projection/card/Descriptor.ts +2 -10
  188. package/src/projection/card/admission.ts +14 -12
  189. package/src/projection/card/diff.ts +142 -1
  190. package/src/validator/FieldDeclarationValidator.ts +5 -5
  191. package/src/validator/FieldValueValidator.ts +45 -12
  192. package/src/validator/InputValidator.ts +8 -18
  193. package/src/vocabulary/preset/created.ts +0 -5
  194. package/src/vocabulary/preset/email.ts +0 -5
  195. package/src/vocabulary/preset/updated.ts +0 -5
  196. package/src/vocabulary/preset/url.ts +0 -5
  197. package/src/vocabulary/primitive/bool.ts +0 -5
  198. package/src/vocabulary/primitive/date.ts +3 -3
  199. package/src/vocabulary/primitive/json.ts +3 -3
  200. package/src/vocabulary/primitive/list.ts +3 -3
  201. package/src/vocabulary/primitive/many.ts +3 -3
  202. package/src/vocabulary/primitive/number.ts +0 -5
  203. package/src/vocabulary/primitive/oneOf.ts +3 -3
  204. package/src/vocabulary/primitive/primary.ts +3 -3
  205. package/src/vocabulary/primitive/ref.ts +0 -5
  206. package/src/vocabulary/primitive/text.ts +0 -5
  207. package/src/vocabulary/rule/immutable.ts +0 -5
  208. package/src/vocabulary/rule/indexed.ts +0 -5
  209. package/src/vocabulary/rule/nullable.ts +3 -3
  210. package/src/vocabulary/rule/optional.ts +3 -3
  211. package/src/vocabulary/rule/readOnly.ts +4 -4
  212. package/src/vocabulary/rule/unique.ts +0 -5
  213. package/src/vocabulary/rule/writeOnly.ts +3 -3
  214. package/src/vocabulary/vocabulary.ts +5 -5
  215. package/dist/validation.d.ts +0 -17
  216. package/dist/validation.d.ts.map +0 -1
  217. package/dist/validation.js +0 -2
  218. package/dist/validation.js.map +0 -1
  219. package/src/validation.ts +0 -10
@@ -1 +1 @@
1
- {"version":3,"file":"unique.js","sourceRoot":"","sources":["../../../src/vocabulary/rule/unique.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C;;;;GAIG;AACH,MAAM,CAAC,MAAM,MAAM,GAAqC,UAAU,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC;IAClF,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;CACvB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"unique.js","sourceRoot":"","sources":["../../../src/vocabulary/rule/unique.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C,MAAM,CAAC,MAAM,MAAM,GAAqC,UAAU,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC;IAClF,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;CACvB,CAAC,CAAC,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import { Field } from '../../field/Field.js';
2
2
  /**
3
- * So a secret leaves no response, the dual of `readOnly` on the same axis.
4
- * FR : pour qu'un secret ne reparte dans aucune réponse, dual de `readOnly`.
3
+ * Closes the way out, the dual of `readOnly`: the value is written and never sent back.
4
+ * FR : ferme la sortie, dual de `readOnly` : la valeur s'écrit et ne repart jamais.
5
5
  * `writeOnly(text())` → `boundary.out` is `'closed'`
6
6
  */
7
7
  export declare const writeOnly: <T>(field: Field<T>) => Field<T>;
@@ -1,11 +1,11 @@
1
1
  import { vocabulary } from '../vocabulary.js';
2
2
  import { Boundary } from '../../axis/boundary/Boundary.js';
3
3
  /**
4
- * So a secret leaves no response, the dual of `readOnly` on the same axis.
5
- * FR : pour qu'un secret ne reparte dans aucune réponse, dual de `readOnly`.
4
+ * Closes the way out, the dual of `readOnly`: the value is written and never sent back.
5
+ * FR : ferme la sortie, dual de `readOnly` : la valeur s'écrit et ne repart jamais.
6
6
  * `writeOnly(text())` → `boundary.out` is `'closed'`
7
7
  */
8
8
  export const writeOnly = vocabulary('writeOnly', (field) => ({
9
- boundary: Boundary.declared(field).with({ out: 'closed' }),
9
+ boundary: Boundary.declared(field).declaring({ out: 'closed' }),
10
10
  }));
11
11
  //# sourceMappingURL=writeOnly.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"writeOnly.js","sourceRoot":"","sources":["../../../src/vocabulary/rule/writeOnly.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAG3D;;;;GAIG;AACH,MAAM,CAAC,MAAM,SAAS,GAAqC,UAAU,CACnE,WAAW,EACX,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACV,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;CAC3D,CAAC,CACH,CAAC"}
1
+ {"version":3,"file":"writeOnly.js","sourceRoot":"","sources":["../../../src/vocabulary/rule/writeOnly.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAG3D;;;;GAIG;AACH,MAAM,CAAC,MAAM,SAAS,GAAqC,UAAU,CACnE,WAAW,EACX,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACV,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;CAChE,CAAC,CACH,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import { Field } from '../field/Field.js';
2
2
  /**
3
- * So a word is one statement about a field, not a rebuilt field.
4
- * FR : pour qu'un mot soit une affirmation sur un champ, pas un champ reconstruit.
3
+ * Builds a `rule/` word, which states members on the field it receives.
4
+ * FR : fabrique un mot de `rule/`, qui énonce des membres sur le champ reçu.
5
5
  * `vocabulary('indexed', () => ({ role: { index: true } }))`
6
6
  */
7
7
  export declare function vocabulary(name: string, states: (field: Field) => Partial<Field>): FieldWord;
@@ -2,17 +2,17 @@ import { EXTENSION_SLOTS } from '../axis/Axis.js';
2
2
  import { dequal } from 'dequal';
3
3
  const MEMBER_SLOTS = [...EXTENSION_SLOTS, 'meta'];
4
4
  /**
5
- * So a word is one statement about a field, not a rebuilt field.
6
- * FR : pour qu'un mot soit une affirmation sur un champ, pas un champ reconstruit.
5
+ * Builds a `rule/` word, which states members on the field it receives.
6
+ * FR : fabrique un mot de `rule/`, qui énonce des membres sur le champ reçu.
7
7
  * `vocabulary('indexed', () => ({ role: { index: true } }))`
8
8
  */
9
9
  export function vocabulary(name, states) {
10
10
  return (field) => field.with(merge(name, field, states(field)));
11
11
  }
12
12
  /**
13
- * So two words that state the same member differently refuse instead of one winning.
14
- * FR : pour que deux mots contredisant un même membre refusent, plutôt qu'un l'emporte.
15
- * `readOnly(writeOnly(text()))` → both close a different side, so both apply
13
+ * Refuses two words stating one member differently, rather than letting the outer win.
14
+ * FR : refuse deux mots qui énoncent un même membre différemment.
15
+ * `readOnly(writeOnly(text()))` → both apply; `immutable(created())` throws
16
16
  */
17
17
  function merge(name, field, given) {
18
18
  const merged = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fougere/schema",
3
- "version": "0.8.2-alpha.0",
3
+ "version": "0.8.4-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
@@ -42,16 +42,6 @@ export class Schema {
42
42
  /** The one place a schema holds what it is. The readings below are its projections. */
43
43
  static definition: SchemaDefinition = SchemaDefinition.of({ fields: {} });
44
44
 
45
- static get fields(): Fields {
46
- return this.definition.fields;
47
- }
48
- /** An entry survives every derivation that kept its field: `Post.pick('body').adapters`. */
49
- static get adapters(): EntityAdapters<Fields> {
50
- return this.definition.adapterSet.adapters;
51
- }
52
- static get opts(): ValidateOptions {
53
- return this.definition.opts;
54
- }
55
45
  static get derivation(): SchemaDerivation | undefined {
56
46
  return this.definition.derivation;
57
47
  }
@@ -75,26 +65,29 @@ export class Schema {
75
65
  }
76
66
  }
77
67
 
78
- static getFields() {
79
- return this.fields;
68
+ static getFields(): Fields {
69
+ return this.definition.fields;
80
70
  }
81
- static getAdapters() {
82
- return this.adapters;
71
+ /** An entry survives every derivation that kept its field: `Post.pick('body').getAdapters()`. */
72
+ static getAdapters(): EntityAdapters<Fields> {
73
+ return this.definition.adapterSet.adapters;
83
74
  }
84
75
  static getUnique(): CompositeUnique<Fields> | undefined {
85
76
  return this.definition.constraints.unique;
86
77
  }
87
- static getOpts() {
88
- return this.opts;
78
+ static getOpts(): ValidateOptions {
79
+ return this.definition.opts;
89
80
  }
90
81
 
91
82
  static validate(input: unknown) {
92
- return InputValidator.of(this.fields, this.opts).validate(input);
83
+ const { fields, opts } = this.definition;
84
+
85
+ return InputValidator.of(fields, opts).validate(input);
93
86
  }
94
87
 
95
88
  static from(data: Record<string, unknown>) {
96
89
  const row: Record<string, unknown> = {};
97
- for (const [key, field] of Object.entries(this.fields)) {
90
+ for (const [key, field] of Object.entries(this.definition.fields)) {
98
91
  if (!Object.hasOwn(data, key)) continue;
99
92
  const value = data[key];
100
93
  if (value === null || value === undefined) {
@@ -108,7 +101,8 @@ export class Schema {
108
101
  }
109
102
 
110
103
  static get ['~standard'](): StandardSchemaV1.Props<Record<string, unknown>> {
111
- const { fields, opts } = this;
104
+ const { fields, opts } = this.definition;
105
+
112
106
  return {
113
107
  version: 1,
114
108
  vendor: 'fougere',
@@ -118,7 +112,7 @@ export class Schema {
118
112
  return {
119
113
  issues: verdict.errors.map((e) => ({
120
114
  message: e.message,
121
- path: e.path && e.path !== '.' ? [{ key: e.path }] : undefined,
115
+ path: e.path.length > 0 ? e.path.map((key) => ({ key })) : undefined,
122
116
  })),
123
117
  };
124
118
  },
@@ -1,5 +1,5 @@
1
- import { type Fields } from './field/Field.js';
2
- import { FieldSet, deduplicated } from './field/FieldSet.js';
1
+ import { type FieldName, type Fields } from './field/Field.js';
2
+ import { FieldSet } from './field/FieldSet.js';
3
3
  import {
4
4
  type CompositeUnique,
5
5
  type EntityDeclarations,
@@ -18,8 +18,40 @@ import type { SchemaView } from './SchemaView.js';
18
18
  * and `opts` because the next rule spanning two fields — a check, an exclusion — is a
19
19
  * member here, not an eighth positional argument in a list that has already lost one.
20
20
  */
21
- export interface SchemaConstraints<TFields extends Fields = Fields> {
21
+ export class SchemaConstraints<TFields extends Fields = Fields> {
22
22
  readonly unique?: CompositeUnique<TFields>;
23
+
24
+ private constructor(unique?: CompositeUnique<TFields>) {
25
+ if (unique) this.unique = unique;
26
+ }
27
+
28
+ static readonly none = new SchemaConstraints();
29
+
30
+ /**
31
+ * The order is part of a group, so `['a','b']` and `['b','a']` are two constraints.
32
+ * FR : l'ordre fait partie du groupe : `['a','b']` et `['b','a']` sont deux contraintes.
33
+ * `SchemaConstraints.of([['a','b'], ['a','b']])` → `unique` holds one group
34
+ */
35
+ static of<TFields extends Fields>(
36
+ groups: readonly (readonly FieldName<TFields>[])[],
37
+ ): SchemaConstraints<TFields> {
38
+ const seen = new Map<string, readonly FieldName<TFields>[]>();
39
+ for (const group of groups) seen.set(JSON.stringify(group), group);
40
+
41
+ return new SchemaConstraints(seen.size ? [...seen.values()] : undefined);
42
+ }
43
+
44
+ /** One member gone and the group is gone: it constrained a pair that no longer exists. */
45
+ renamed(transform: (key: string) => string | undefined): SchemaConstraints {
46
+ if (!this.unique) return SchemaConstraints.none;
47
+ const kept: string[][] = [];
48
+ for (const group of this.unique) {
49
+ const renamed = group.map(transform);
50
+ if (renamed.every((key): key is string => key !== undefined)) kept.push(renamed);
51
+ }
52
+
53
+ return SchemaConstraints.of(kept);
54
+ }
23
55
  }
24
56
 
25
57
  /**
@@ -72,6 +104,31 @@ export class SchemaDefinition {
72
104
  this.constraints = state.constraints;
73
105
  }
74
106
 
107
+ /**
108
+ * A schema cut from another one: it carries no previous names and holds no rows of its
109
+ * own, whatever else it keeps. `pick`, `partial`, `extend` and `compose` all land here.
110
+ */
111
+ private static derived(state: Omit<SchemaState, 'previous' | 'anchored'>): SchemaDefinition {
112
+ return new SchemaDefinition({ ...state, previous: undefined, anchored: false });
113
+ }
114
+
115
+ /**
116
+ * The same schema, one member restated — `declares` and `anchor`, which cut nothing.
117
+ * What they do not name is kept, and that is the whole difference with `derived`.
118
+ */
119
+ private restated(state: Partial<SchemaState>): SchemaDefinition {
120
+ return new SchemaDefinition({
121
+ fields: this.fields,
122
+ adapterSet: this.adapterSet,
123
+ opts: this.opts,
124
+ previous: this.previous,
125
+ derivation: this.derivation,
126
+ anchored: this.anchored,
127
+ constraints: this.constraints,
128
+ ...state,
129
+ });
130
+ }
131
+
75
132
  /** Built complete or not at all — no static assigned on the side. */
76
133
  static of(declaration: {
77
134
  fields: Fields;
@@ -89,7 +146,7 @@ export class SchemaDefinition {
89
146
  previous: declaration.previous,
90
147
  derivation: declaration.derivation,
91
148
  anchored: declaration.anchored ?? false,
92
- constraints: declaration.constraints ?? {},
149
+ constraints: declaration.constraints ?? SchemaConstraints.none,
93
150
  });
94
151
  }
95
152
 
@@ -101,31 +158,22 @@ export class SchemaDefinition {
101
158
  ]);
102
159
  const declared = FieldSet.declaring(this.fields, declarations.unique);
103
160
 
104
- return new SchemaDefinition({
161
+ return this.restated({
105
162
  fields: declared.fields,
106
163
  adapterSet: EntityAdapterSet.merged([
107
164
  this.adapterSet,
108
165
  EntityAdapterSet.of(declarations.adapters),
109
166
  ]),
110
- opts: this.opts,
111
167
  previous: declarations.previous ?? this.previous,
112
- derivation: this.derivation,
113
- anchored: this.anchored,
114
168
  constraints:
115
- declarations.unique === undefined ? this.constraints : { unique: declared.unique },
169
+ declarations.unique === undefined
170
+ ? this.constraints
171
+ : SchemaConstraints.of(declared.groups),
116
172
  });
117
173
  }
118
174
 
119
175
  anchoring(): SchemaDefinition {
120
- return new SchemaDefinition({
121
- fields: this.fields,
122
- adapterSet: this.adapterSet,
123
- opts: this.opts,
124
- previous: this.previous,
125
- derivation: this.derivation,
126
- anchored: true,
127
- constraints: this.constraints,
128
- });
176
+ return this.restated({ anchored: true });
129
177
  }
130
178
 
131
179
  pick(keys: readonly string[], root: SchemaView): SchemaDefinition {
@@ -160,52 +208,32 @@ export class SchemaDefinition {
160
208
  }
161
209
  const nameOf = (key: string) => kept.get(key);
162
210
 
163
- return new SchemaDefinition({
211
+ return SchemaDefinition.derived({
164
212
  fields,
165
213
  adapterSet: this.adapterSet.rename(nameOf),
166
214
  opts: this.opts,
167
- previous: undefined,
168
215
  derivation: this.origin(root).rename(nameOf),
169
- anchored: false,
170
- constraints: this.constraintsRenamed(nameOf),
216
+ constraints: this.constraints.renamed(nameOf),
171
217
  });
172
218
  }
173
219
 
174
- /** One member gone and the group is gone: it constrained a pair that no longer exists. */
175
- private constraintsRenamed(
176
- transform: (key: string) => string | undefined,
177
- ): SchemaConstraints {
178
- if (!this.constraints.unique) return this.constraints;
179
- const kept: string[][] = [];
180
- for (const group of this.constraints.unique) {
181
- const renamed = group.map(transform);
182
- if (renamed.every((key): key is string => key !== undefined)) kept.push(renamed);
183
- }
184
-
185
- return { unique: deduplicated(kept) as CompositeUnique<Fields> | undefined };
186
- }
187
-
188
220
  patched(root: SchemaView): SchemaDefinition {
189
- return new SchemaDefinition({
221
+ return SchemaDefinition.derived({
190
222
  fields: { ...this.fields },
191
223
  adapterSet: this.adapterSet,
192
224
  opts: { ...this.opts, patch: true },
193
- previous: undefined,
194
225
  derivation: this.origin(root),
195
- anchored: false,
196
226
  constraints: this.constraints,
197
227
  });
198
228
  }
199
229
 
200
230
  /** An added field does not pretend to come from the root, which never declared it. */
201
231
  extended(extra: Fields, root: SchemaView): SchemaDefinition {
202
- return new SchemaDefinition({
232
+ return SchemaDefinition.derived({
203
233
  fields: { ...this.fields, ...extra },
204
234
  adapterSet: this.adapterSet,
205
235
  opts: this.opts,
206
- previous: undefined,
207
236
  derivation: this.origin(root),
208
- anchored: false,
209
237
  constraints: this.constraints,
210
238
  });
211
239
  }
@@ -221,22 +249,20 @@ export class SchemaDefinition {
221
249
  groups.push(...(view.getUnique() ?? []));
222
250
  }
223
251
 
224
- return new SchemaDefinition({
252
+ return SchemaDefinition.derived({
225
253
  fields,
226
254
  adapterSet: EntityAdapterSet.merged(
227
255
  views.map((view) => EntityAdapterSet.of(view.getAdapters())),
228
256
  ),
229
257
  opts,
230
- previous: undefined,
231
258
  derivation: undefined,
232
- anchored: false,
233
- constraints: { unique: deduplicated(groups) as CompositeUnique<Fields> | undefined },
259
+ constraints: SchemaConstraints.of(groups),
234
260
  });
235
261
  }
236
262
 
237
263
  /**
238
- * So a gesture naming a field that does not exist says so, and lists what there is.
239
- * FR : pour qu'un geste nommant un champ inexistant le dise, et énumère ce qui existe.
264
+ * Refuses a gesture naming a field the schema does not carry, and lists what it does.
265
+ * FR : refuse un geste nommant un champ absent, et énumère ceux qui existent.
240
266
  * `pick('titel')` → `pick(): unknown field \`titel\`. This schema carries id, title, body.`
241
267
  */
242
268
  private assertKnown(operation: string, keys: readonly string[]): void {
package/src/SchemaView.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Field, Fields } from './field/Field.js';
2
2
  import type { CompositeUnique, PreviousNames } from './entity/EntityDeclarations.js';
3
3
  import type { EntityAdapters } from './entity/EntityAdapters.js';
4
- import type { ValidationResult } from './validation.js';
4
+ import type { ValidationResult } from './lib/validation.js';
5
5
  import type { ValidateOptions } from './validator/InputValidator.js';
6
6
  import type { SchemaDerivation } from './SchemaDerivation.js';
7
7
 
package/src/axis/Axis.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { roleAxis } from './role/RoleAxis.js';
2
2
  import { lifecycleAxis } from './lifecycle/LifecycleAxis.js';
3
3
  import { boundaryAxis } from './boundary/BoundaryAxis.js';
4
- import type { ValidationError } from '../validation.js';
4
+ import type { ValidationError } from '../lib/validation.js';
5
5
 
6
6
  export interface Axis<Declared = unknown, Wire = unknown> {
7
7
  readonly slot: 'role' | 'lifecycle' | 'boundary';
@@ -1,6 +1,11 @@
1
1
  import type { BoundaryRules } from './Boundary.js';
2
2
  import { Registry } from '../../lib/Registry.js';
3
3
 
4
+ /**
5
+ * Wire to domain, and it must ANSWER a value it already produced: two doors decode — the
6
+ * client one on what arrives, `StorageGuard` on what a handler writes — so a decoder that
7
+ * halves cents halves them twice and stores a hundredth.
8
+ */
4
9
  export type Decoder = (value: unknown) => { value: unknown } | { error: string };
5
10
  export type Encoder = (value: unknown) => unknown;
6
11
 
@@ -13,9 +13,9 @@ export type BoundaryRef = 'isoDate' | (string & {}) | BoundaryRules;
13
13
  const identityDecoder: Decoder = (value) => ({ value });
14
14
  const identityEncoder: Encoder = (value) => value;
15
15
 
16
- export class Boundary implements BoundaryRules {
17
- readonly in?: BoundaryRules['in'];
18
- readonly out?: BoundaryRules['out'];
16
+ export class Boundary {
17
+ private readonly in?: BoundaryRules['in'];
18
+ private readonly out?: BoundaryRules['out'];
19
19
  readonly decode: Decoder;
20
20
  readonly encode: Encoder;
21
21
 
@@ -61,15 +61,13 @@ export class Boundary implements BoundaryRules {
61
61
 
62
62
  /** `date-time` means a `Date` on both sides, without a word in the entity. */
63
63
  static forShape(shape: Shape | undefined): Boundary {
64
- const base = Shapes.of(shape).base;
65
- if (base?.type === 'string' && base.format === 'date-time') {
66
- return new Boundary(Boundaries.aliases.find('isoDate')!);
67
- }
64
+ if (Shapes.typeOf(shape) === 'date') return new Boundary(Boundaries.aliases.find('isoDate')!);
68
65
  return new Boundary();
69
66
  }
70
67
 
71
- with(overrides: BoundaryRules): Boundary {
72
- return new Boundary({ in: overrides.in ?? this.in, out: overrides.out ?? this.out });
68
+ /** The dual of `declared`: what `readOnly()` writes back on the field, not a judge. */
69
+ declaring(overrides: BoundaryRules): BoundaryRules {
70
+ return { in: overrides.in ?? this.in, out: overrides.out ?? this.out };
73
71
  }
74
72
 
75
73
  get readOnly(): boolean {
@@ -10,7 +10,7 @@ export const boundaryAxis: Axis<BoundaryRef, BoundaryRef> = {
10
10
  if (typeof value === 'string') return;
11
11
  if (!isObject(value)) {
12
12
  errors.push({
13
- path: 'boundary',
13
+ path: ['boundary'],
14
14
  message: `Expected an alias name or { in, out } — got ${shown(value)}`,
15
15
  });
16
16
  return;
@@ -19,7 +19,7 @@ export const boundaryAxis: Axis<BoundaryRef, BoundaryRef> = {
19
19
  const rule = value[side];
20
20
  if (rule === undefined || rule === 'closed') continue;
21
21
  if (!isObject(rule) || typeof rule[verb] !== 'string') {
22
- errors.push({ path: `boundary.${side}`, message: `Expected 'closed' or { ${verb}: <name> }` });
22
+ errors.push({ path: ['boundary', side], message: `Expected 'closed' or { ${verb}: <name> }` });
23
23
  }
24
24
  }
25
25
  },
@@ -8,9 +8,9 @@ export interface LifecycleRules {
8
8
  update?: (typeof UPDATE_TOKENS)[number];
9
9
  }
10
10
 
11
- export class Lifecycle implements LifecycleRules {
12
- readonly create?: LifecycleRules['create'];
13
- readonly update?: LifecycleRules['update'];
11
+ export class Lifecycle {
12
+ private readonly create?: LifecycleRules['create'];
13
+ private readonly update?: LifecycleRules['update'];
14
14
 
15
15
  private constructor(rules: LifecycleRules = {}) {
16
16
  this.create = rules.create;
@@ -25,8 +25,13 @@ export class Lifecycle implements LifecycleRules {
25
25
  return this.create === undefined;
26
26
  }
27
27
 
28
+ /** The server fills it at create, so a client form never carries it. */
29
+ get stampedAtCreate(): boolean {
30
+ return this.create === 'now';
31
+ }
32
+
28
33
  get stampedOnce(): boolean {
29
- return this.create === 'now' && this.update !== 'now';
34
+ return this.stampedAtCreate && !this.stampedOnUpdate;
30
35
  }
31
36
 
32
37
  get immutable(): boolean {
@@ -1,5 +1,5 @@
1
1
  import type { Axis } from '../Axis.js';
2
- import type { ValidationError } from '../../validation.js';
2
+ import type { ValidationError } from '../../lib/validation.js';
3
3
  import { admitWire, oneOfTokens } from '../../projection/card/admission.js';
4
4
  import { isObject, shown } from '../../lib/utils.js';
5
5
  import { CREATE_TOKENS, UPDATE_TOKENS, type LifecycleRules } from './Lifecycle.js';
@@ -9,13 +9,13 @@ export const lifecycleAxis: Axis<LifecycleRules, LifecycleRules> = {
9
9
 
10
10
  validator(value, errors) {
11
11
  if (!isObject(value)) {
12
- errors.push({ path: 'lifecycle', message: `Expected an object — got ${shown(value)}` });
12
+ errors.push({ path: ['lifecycle'], message: `Expected an object — got ${shown(value)}` });
13
13
  return;
14
14
  }
15
15
  if (value.create !== undefined) validateCreate(value.create, errors);
16
16
  if (value.update !== undefined && !oneOfTokens(value.update, UPDATE_TOKENS)) {
17
17
  errors.push({
18
- path: 'lifecycle.update',
18
+ path: ['lifecycle', 'update'],
19
19
  message: `Expected 'now' or 'forbidden' — got ${shown(value.update)}`,
20
20
  });
21
21
  }
@@ -29,8 +29,8 @@ export const lifecycleAxis: Axis<LifecycleRules, LifecycleRules> = {
29
29
  };
30
30
 
31
31
  /**
32
- * So the four legal ways to declare a creation are stated once, and anything else names them.
33
- * FR : pour que les quatre façons de déclarer une création soient dites une fois.
32
+ * Judges the create rule, whose four legal forms are stated here and nowhere else.
33
+ * FR : juge la règle de création, dont les quatre formes légales sont énoncées ici seulement.
34
34
  * `create: 3` → `Expected 'now', 'optional', { value } or { generate } — got 3`
35
35
  */
36
36
  function validateCreate(rule: unknown, errors: ValidationError[]): void {
@@ -40,7 +40,7 @@ function validateCreate(rule: unknown, errors: ValidationError[]): void {
40
40
  if ('generate' in rule) {
41
41
  if (typeof rule.generate !== 'string') {
42
42
  errors.push({
43
- path: 'lifecycle.create.generate',
43
+ path: ['lifecycle', 'create', 'generate'],
44
44
  message: `Expected a generator name — got ${shown(rule.generate)}`,
45
45
  });
46
46
  }
@@ -48,7 +48,7 @@ function validateCreate(rule: unknown, errors: ValidationError[]): void {
48
48
  }
49
49
  }
50
50
  errors.push({
51
- path: 'lifecycle.create',
51
+ path: ['lifecycle', 'create'],
52
52
  message: `Expected 'now', 'optional', { value } or { generate } — got ${shown(rule)}`,
53
53
  });
54
54
  }
@@ -4,10 +4,9 @@ import { Clock } from './Clock.js';
4
4
  import { type Field, type Fields } from '../../field/Field.js';
5
5
 
6
6
  /**
7
- * So every storage realizes a create the same way.
8
- * FR : pour que chaque storage réalise une création de la même façon.
9
- * `applyCreate({ id: primary(), createdAt: created() }, { title: 'a' })`
10
- * → `id` generated, `createdAt` stamped, `title` untouched
7
+ * Fills what the declaration leaves to the storage, so no handler stamps a date itself.
8
+ * FR : remplit ce que la déclaration laisse au storage : aucun handler n'estampe lui-même.
9
+ * `applyCreate({ id: primary(), createdAt: created() }, { title: 'a' })` → `id`, `createdAt`
11
10
  */
12
11
  export function applyCreate(fields: Fields, input: Record<string, unknown>): Record<string, unknown> {
13
12
  const values: Record<string, unknown> = { ...input };
@@ -17,7 +16,7 @@ export function applyCreate(fields: Fields, input: Record<string, unknown>): Rec
17
16
  if (name in values) continue;
18
17
  const rule = Lifecycle.of(field);
19
18
 
20
- if (rule.create === 'now') values[name] = new Date(instant);
19
+ if (rule.stampedAtCreate) values[name] = new Date(instant);
21
20
  else if (rule.literal) values[name] = freshValue(rule.literal.value);
22
21
  else if (rule.generator) values[name] = Generators.resolve(rule.generator)();
23
22
  }
@@ -26,8 +25,9 @@ export function applyCreate(fields: Fields, input: Record<string, unknown>): Rec
26
25
  }
27
26
 
28
27
  /**
29
- * So two rows created from one declared default never end up sharing the same object.
30
- * FR : pour que deux lignes nées d'un même défaut ne partagent pas l'objet.
28
+ * Clones a declared default, so two rows born of `create: { value: [] }` hold two arrays.
29
+ * FR : clone un défaut déclaré, pour que deux lignes nées de `create: { value: [] }`
30
+ * tiennent deux tableaux.
31
31
  * `create: { value: [] }` → each instance gets its own array
32
32
  */
33
33
  function freshValue(value: unknown): unknown {
@@ -36,8 +36,9 @@ function freshValue(value: unknown): unknown {
36
36
  }
37
37
 
38
38
  /**
39
- * So `updated()` is stamped by the storage, and no handler has to remember it.
40
- * FR : pour qu'`updated()` soit estampé par le storage, pas par le handler.
39
+ * The dual of `applyCreate` on a patch: only `update: 'now'` fields are touched.
40
+ * FR : le dual d'`applyCreate` sur une modification : seuls les champs `update: 'now'`
41
+ * sont touchés.
41
42
  * `applyUpdate(fields, { title: 'b' })` → `updatedAt` added, nothing else
42
43
  */
43
44
  export function applyUpdate(fields: Fields, patch: Record<string, unknown>): Record<string, unknown> {
@@ -7,11 +7,11 @@ export interface RoleRules {
7
7
  relation?: Relation;
8
8
  }
9
9
 
10
- export class Role implements RoleRules {
11
- readonly primary?: boolean;
12
- readonly index?: boolean;
13
- readonly unique?: boolean;
14
- readonly relation?: Relation;
10
+ export class Role {
11
+ private readonly primary?: boolean;
12
+ private readonly index?: boolean;
13
+ private readonly unique?: boolean;
14
+ private readonly relation?: Relation;
15
15
 
16
16
  private constructor(rules: RoleRules = {}) {
17
17
  this.primary = rules.primary;
@@ -45,8 +45,17 @@ export class Role implements RoleRules {
45
45
  return this.relation?.kind === 'one';
46
46
  }
47
47
 
48
+ /** Either kind — what a CLI flag and a GraphQL selection both leave out. */
49
+ get isRelation(): boolean {
50
+ return this.relation !== undefined;
51
+ }
52
+
48
53
  /** Calls `() => Post` so no caller has to. */
49
54
  get target(): EntityConstructor | undefined {
50
55
  return this.relation?.to();
51
56
  }
57
+
58
+ get onDelete(): Relation['onDelete'] {
59
+ return this.relation?.onDelete;
60
+ }
52
61
  }