@fougere/schema 0.2.0-alpha.0 → 0.2.0-alpha.2

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 (188) hide show
  1. package/dist/entity.d.ts +9 -106
  2. package/dist/entity.d.ts.map +1 -1
  3. package/dist/entity.js +16 -266
  4. package/dist/entity.js.map +1 -1
  5. package/dist/field/boundary.d.ts +26 -43
  6. package/dist/field/boundary.d.ts.map +1 -1
  7. package/dist/field/boundary.js +30 -42
  8. package/dist/field/boundary.js.map +1 -1
  9. package/dist/field/field.d.ts +13 -56
  10. package/dist/field/field.d.ts.map +1 -1
  11. package/dist/field/field.js +22 -26
  12. package/dist/field/field.js.map +1 -1
  13. package/dist/field/format.d.ts +29 -0
  14. package/dist/field/format.d.ts.map +1 -0
  15. package/dist/field/format.js +24 -0
  16. package/dist/field/format.js.map +1 -0
  17. package/dist/field/index.d.ts +7 -6
  18. package/dist/field/index.d.ts.map +1 -1
  19. package/dist/field/index.js +6 -5
  20. package/dist/field/index.js.map +1 -1
  21. package/dist/field/lifecycle.d.ts +15 -24
  22. package/dist/field/lifecycle.d.ts.map +1 -1
  23. package/dist/field/lifecycle.js +10 -17
  24. package/dist/field/lifecycle.js.map +1 -1
  25. package/dist/field/meta.d.ts +1 -5
  26. package/dist/field/meta.d.ts.map +1 -1
  27. package/dist/field/meta.js +2 -4
  28. package/dist/field/meta.js.map +1 -1
  29. package/dist/field/role.d.ts +15 -22
  30. package/dist/field/role.d.ts.map +1 -1
  31. package/dist/field/role.js +11 -31
  32. package/dist/field/role.js.map +1 -1
  33. package/dist/field/shape.d.ts +24 -12
  34. package/dist/field/shape.d.ts.map +1 -1
  35. package/dist/field/shape.js +45 -27
  36. package/dist/field/shape.js.map +1 -1
  37. package/dist/hints.d.ts +10 -15
  38. package/dist/hints.d.ts.map +1 -1
  39. package/dist/hints.js +22 -1
  40. package/dist/hints.js.map +1 -1
  41. package/dist/index.d.ts +37 -36
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +37 -36
  44. package/dist/index.js.map +1 -1
  45. package/dist/name.d.ts +4 -12
  46. package/dist/name.d.ts.map +1 -1
  47. package/dist/name.js +4 -12
  48. package/dist/name.js.map +1 -1
  49. package/dist/projections/card.d.ts +39 -58
  50. package/dist/projections/card.d.ts.map +1 -1
  51. package/dist/projections/card.js.map +1 -1
  52. package/dist/projections/describe.d.ts +4 -4
  53. package/dist/projections/describe.d.ts.map +1 -1
  54. package/dist/projections/describe.js +4 -9
  55. package/dist/projections/describe.js.map +1 -1
  56. package/dist/projections/io.d.ts +5 -11
  57. package/dist/projections/io.d.ts.map +1 -1
  58. package/dist/projections/io.js +5 -11
  59. package/dist/projections/io.js.map +1 -1
  60. package/dist/projections/lifecycle.d.ts +5 -19
  61. package/dist/projections/lifecycle.d.ts.map +1 -1
  62. package/dist/projections/lifecycle.js +7 -29
  63. package/dist/projections/lifecycle.js.map +1 -1
  64. package/dist/projections/reconstruct.d.ts +11 -16
  65. package/dist/projections/reconstruct.d.ts.map +1 -1
  66. package/dist/projections/reconstruct.js +29 -32
  67. package/dist/projections/reconstruct.js.map +1 -1
  68. package/dist/projections/source.d.ts +11 -34
  69. package/dist/projections/source.d.ts.map +1 -1
  70. package/dist/projections/source.js +4 -23
  71. package/dist/projections/source.js.map +1 -1
  72. package/dist/projections/typescript.d.ts +6 -22
  73. package/dist/projections/typescript.d.ts.map +1 -1
  74. package/dist/projections/typescript.js +16 -43
  75. package/dist/projections/typescript.js.map +1 -1
  76. package/dist/schema/index.d.ts +3 -0
  77. package/dist/schema/index.d.ts.map +1 -0
  78. package/dist/schema/index.js +2 -0
  79. package/dist/schema/index.js.map +1 -0
  80. package/dist/schema/schema.d.ts +74 -0
  81. package/dist/schema/schema.d.ts.map +1 -0
  82. package/dist/schema/schema.js +162 -0
  83. package/dist/schema/schema.js.map +1 -0
  84. package/dist/schema/view.d.ts +26 -0
  85. package/dist/schema/view.d.ts.map +1 -0
  86. package/dist/schema/view.js +2 -0
  87. package/dist/schema/view.js.map +1 -0
  88. package/dist/unique.d.ts +12 -37
  89. package/dist/unique.d.ts.map +1 -1
  90. package/dist/unique.js +11 -34
  91. package/dist/unique.js.map +1 -1
  92. package/dist/validation/index.d.ts +4 -0
  93. package/dist/validation/index.d.ts.map +1 -0
  94. package/dist/validation/index.js +2 -0
  95. package/dist/validation/index.js.map +1 -0
  96. package/dist/validation/judge.d.ts +55 -0
  97. package/dist/validation/judge.d.ts.map +1 -0
  98. package/dist/validation/judge.js +351 -0
  99. package/dist/validation/judge.js.map +1 -0
  100. package/dist/validation/options.d.ts +6 -0
  101. package/dist/validation/options.d.ts.map +1 -0
  102. package/dist/validation/options.js +3 -0
  103. package/dist/validation/options.js.map +1 -0
  104. package/dist/validation/result.d.ts +20 -0
  105. package/dist/validation/result.d.ts.map +1 -0
  106. package/dist/validation/result.js +3 -0
  107. package/dist/validation/result.js.map +1 -0
  108. package/dist/vocabulary/bool.d.ts +1 -1
  109. package/dist/vocabulary/bool.d.ts.map +1 -1
  110. package/dist/vocabulary/bool.js +2 -2
  111. package/dist/vocabulary/bool.js.map +1 -1
  112. package/dist/vocabulary/created.d.ts +4 -10
  113. package/dist/vocabulary/created.d.ts.map +1 -1
  114. package/dist/vocabulary/created.js +5 -12
  115. package/dist/vocabulary/created.js.map +1 -1
  116. package/dist/vocabulary/date.d.ts +1 -1
  117. package/dist/vocabulary/date.d.ts.map +1 -1
  118. package/dist/vocabulary/date.js +2 -2
  119. package/dist/vocabulary/date.js.map +1 -1
  120. package/dist/vocabulary/immutable.d.ts +4 -6
  121. package/dist/vocabulary/immutable.d.ts.map +1 -1
  122. package/dist/vocabulary/immutable.js +3 -6
  123. package/dist/vocabulary/immutable.js.map +1 -1
  124. package/dist/vocabulary/indexed.d.ts +5 -16
  125. package/dist/vocabulary/indexed.d.ts.map +1 -1
  126. package/dist/vocabulary/indexed.js +4 -16
  127. package/dist/vocabulary/indexed.js.map +1 -1
  128. package/dist/vocabulary/json.d.ts +8 -13
  129. package/dist/vocabulary/json.d.ts.map +1 -1
  130. package/dist/vocabulary/json.js +3 -3
  131. package/dist/vocabulary/json.js.map +1 -1
  132. package/dist/vocabulary/list.d.ts +3 -6
  133. package/dist/vocabulary/list.d.ts.map +1 -1
  134. package/dist/vocabulary/list.js +6 -5
  135. package/dist/vocabulary/list.js.map +1 -1
  136. package/dist/vocabulary/many.d.ts +3 -4
  137. package/dist/vocabulary/many.d.ts.map +1 -1
  138. package/dist/vocabulary/many.js +5 -5
  139. package/dist/vocabulary/many.js.map +1 -1
  140. package/dist/vocabulary/nullable.d.ts +2 -4
  141. package/dist/vocabulary/nullable.d.ts.map +1 -1
  142. package/dist/vocabulary/nullable.js +4 -8
  143. package/dist/vocabulary/nullable.js.map +1 -1
  144. package/dist/vocabulary/number.d.ts +1 -1
  145. package/dist/vocabulary/number.d.ts.map +1 -1
  146. package/dist/vocabulary/number.js +2 -2
  147. package/dist/vocabulary/number.js.map +1 -1
  148. package/dist/vocabulary/oneOf.d.ts +1 -1
  149. package/dist/vocabulary/oneOf.d.ts.map +1 -1
  150. package/dist/vocabulary/oneOf.js +2 -2
  151. package/dist/vocabulary/oneOf.js.map +1 -1
  152. package/dist/vocabulary/optional.d.ts +1 -1
  153. package/dist/vocabulary/optional.d.ts.map +1 -1
  154. package/dist/vocabulary/optional.js +3 -3
  155. package/dist/vocabulary/optional.js.map +1 -1
  156. package/dist/vocabulary/primary.d.ts +5 -5
  157. package/dist/vocabulary/primary.d.ts.map +1 -1
  158. package/dist/vocabulary/primary.js +14 -11
  159. package/dist/vocabulary/primary.js.map +1 -1
  160. package/dist/vocabulary/readOnly.d.ts +4 -7
  161. package/dist/vocabulary/readOnly.d.ts.map +1 -1
  162. package/dist/vocabulary/readOnly.js +4 -7
  163. package/dist/vocabulary/readOnly.js.map +1 -1
  164. package/dist/vocabulary/ref.d.ts +1 -1
  165. package/dist/vocabulary/ref.d.ts.map +1 -1
  166. package/dist/vocabulary/ref.js +2 -2
  167. package/dist/vocabulary/ref.js.map +1 -1
  168. package/dist/vocabulary/text.d.ts +1 -1
  169. package/dist/vocabulary/text.d.ts.map +1 -1
  170. package/dist/vocabulary/text.js +2 -2
  171. package/dist/vocabulary/text.js.map +1 -1
  172. package/dist/vocabulary/unique.d.ts +8 -18
  173. package/dist/vocabulary/unique.d.ts.map +1 -1
  174. package/dist/vocabulary/unique.js +7 -18
  175. package/dist/vocabulary/unique.js.map +1 -1
  176. package/dist/vocabulary/updated.d.ts +2 -2
  177. package/dist/vocabulary/updated.d.ts.map +1 -1
  178. package/dist/vocabulary/updated.js +1 -2
  179. package/dist/vocabulary/updated.js.map +1 -1
  180. package/dist/vocabulary/writeOnly.d.ts +4 -6
  181. package/dist/vocabulary/writeOnly.d.ts.map +1 -1
  182. package/dist/vocabulary/writeOnly.js +4 -6
  183. package/dist/vocabulary/writeOnly.js.map +1 -1
  184. package/package.json +2 -2
  185. package/dist/projections/validation.d.ts +0 -57
  186. package/dist/projections/validation.d.ts.map +0 -1
  187. package/dist/projections/validation.js +0 -148
  188. package/dist/projections/validation.js.map +0 -1
@@ -0,0 +1,26 @@
1
+ import type { Field, Fields } from "../field/index.js";
2
+ import type { Hints } from "../hints.js";
3
+ import type { CompositeUnique } from "../unique.js";
4
+ import type { ValidationResult, ValidateOptions } from '../validation/index.js';
5
+ /** The row an entity carries — its fields' value types, all present. */
6
+ export type Row<TFields extends Fields> = {
7
+ [K in keyof TFields]: TFields[K] extends Field<infer T> ? T : never;
8
+ };
9
+ /** A row as handed to the constructor — every key omissible. */
10
+ export type PartialRow<TFields extends Fields> = Partial<Row<TFields>>;
11
+ /**
12
+ * What a schema ANSWERS. No construct signature, so a class carrying a body satisfies it —
13
+ * and `Fields` by default, for a reader that does not care which map it holds.
14
+ */
15
+ export interface SchemaView<TFields extends Fields = Fields> {
16
+ readonly name: string;
17
+ getFields(): TFields;
18
+ /** Per-consumer hints from the 2nd arg of `entity()`. Derivations carry them. */
19
+ getHints(): Hints<TFields> | undefined;
20
+ /** Field groups unique together, from the 2nd arg of `entity()`. */
21
+ getUnique(): CompositeUnique<TFields> | undefined;
22
+ /** This view's validation mode — `patch` is set by `partial()`. */
23
+ getOpts(): ValidateOptions;
24
+ validate(input: unknown): ValidationResult<Row<TFields>>;
25
+ }
26
+ //# sourceMappingURL=view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../src/schema/view.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGhF,wEAAwE;AACxE,MAAM,MAAM,GAAG,CAAC,OAAO,SAAS,MAAM,IAAI;KACvC,CAAC,IAAI,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK;CACpE,CAAC;AAEF,gEAAgE;AAChE,MAAM,MAAM,UAAU,CAAC,OAAO,SAAS,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;AAEvE;;;GAGG;AACH,MAAM,WAAW,UAAU,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,SAAS,IAAI,OAAO,CAAC;IACrB,iFAAiF;IACjF,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;IACvC,oEAAoE;IACpE,SAAS,IAAI,eAAe,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;IAClD,mEAAmE;IACnE,OAAO,IAAI,eAAe,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;CAC1D"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view.js","sourceRoot":"","sources":["../../src/schema/view.ts"],"names":[],"mappings":""}
package/dist/unique.d.ts CHANGED
@@ -1,16 +1,9 @@
1
1
  import type { Fields } from './field/index.js';
2
2
  import type { Hints } from './hints.js';
3
3
  /**
4
- * Field names that identify at most one row when taken together.
5
- *
6
- * `unique(field)` states a fact about one column; some facts are about a pair.
7
- * "A book appears once in a list" is true of `(listId, docId)` and of neither
8
- * alone, and there is no shape that can express it — judging one value never
9
- * sees the other rows. So it is declared on the entity and realized by storage.
10
- *
11
- * Held here rather than in a handler because a handler cannot hold it: a check
12
- * followed by a write is two round trips, and a concurrent request fits between
13
- * them. The database is the only place where the promise is keepable.
4
+ * Field names that identify at most one row when taken together. No shape can express it —
5
+ * judging one value never sees the other rows — and no handler either: a check then a write
6
+ * is two round trips with room for a concurrent one between them. Only the database keeps it.
14
7
  */
15
8
  export type CompositeUnique<TFields extends Fields> = ReadonlyArray<ReadonlyArray<Extract<keyof TFields, string>>>;
16
9
  /**
@@ -21,45 +14,27 @@ export type CompositeUnique<TFields extends Fields> = ReadonlyArray<ReadonlyArra
21
14
  * adds a key here, never a positional argument.
22
15
  */
23
16
  export interface EntityDeclarations<TFields extends Fields> {
24
- /** Field groups that must be unique together. */
25
17
  unique?: CompositeUnique<TFields>;
26
18
  /** Per-consumer hints, keyed by registered adapter. See {@link Hints}. */
27
19
  hints?: Hints<TFields>;
28
20
  }
29
21
  /**
30
- * Carry composite groups across a field-key transform the entity-level twin of
31
- * `deriveHints`.
32
- *
33
- * A group whose members did not all survive is dropped rather than narrowed: the
34
- * pair `(listId, docId)` says nothing about `listId` alone, and keeping the
35
- * remnant would silently state a stronger fact than the author ever wrote.
22
+ * Carry composite groups across a key transform. A group that lost a member is DROPPED,
23
+ * not narrowed: `(listId, docId)` says nothing about `listId` alone.
36
24
  */
37
25
  export declare function deriveUnique(groups: CompositeUnique<Fields> | undefined, mapKey: (key: string) => string | undefined): CompositeUnique<Fields> | undefined;
38
26
  /**
39
- * Carry the projected groups across the same key transform the field-level twin of
40
- * {@link deriveUnique}, and it has to run with it: the group lives in TWO places once
41
- * projected (the declaration and each member's role), so remapping one alone left
42
- * `pick('id','listId')` with a role still claiming a pair whose other member was gone.
43
- *
44
- * Same rule as the declaration — a group that lost a member is dropped, not narrowed.
45
- * The empty self-reference needs no remapping: it names no key, which is exactly why it
46
- * survives a `rename()` for free.
27
+ * The same, on each member's role it must run WITH {@link deriveUnique}: once projected
28
+ * the group lives in two places, so remapping one alone left a role claiming a pair whose
29
+ * other member was gone. The empty self-reference names no key, so it needs no remapping.
47
30
  */
48
31
  export declare function deriveUniqueRoles<TFields extends Fields>(fields: TFields, mapKey: (key: string) => string | undefined): TFields;
49
32
  /**
50
- * Project the entity's composite groups onto the role of every member — the single
51
- * place the two ways of stating uniqueness meet.
52
- *
53
- * The author writes each fact where it lives: `unique(slug)` on the field it is about,
54
- * `entity(fields, { unique: [['listId','docId']] })` on the entity, because a fact about
55
- * a pair is held by neither field alone. Both then read as ONE normal form on the role
56
- * axis, so every consumer — the DDL, the card, a foreign adapter — has a single shape to
57
- * handle, and a field belonging to two constraints simply carries two member lists.
33
+ * Project the entity's composite groups onto every member's role — the single place the
34
+ * two ways of stating uniqueness meet, so every consumer has one shape to handle.
58
35
  *
59
- * The entity declaration stays the source (`getUnique()` keeps answering it); this is a
60
- * projection of it, never a second place to edit. A named member group is spelled out
61
- * here, while a lone `unique()` keeps its `[]` self-reference — resolved by whoever reads
62
- * it, which is what makes `rename()` free.
36
+ * The entity declaration stays the source (`getUnique()` answers it); this is a projection,
37
+ * never a second place to edit.
63
38
  */
64
39
  export declare function projectUniqueOntoFields<TFields extends Fields>(fields: TFields, groups: CompositeUnique<TFields> | undefined): TFields;
65
40
  //# sourceMappingURL=unique.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"unique.d.ts","sourceRoot":"","sources":["../src/unique.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,eAAe,CAAC,OAAO,SAAS,MAAM,IAAI,aAAa,CACjE,aAAa,CAAC,OAAO,CAAC,MAAM,OAAO,EAAE,MAAM,CAAC,CAAC,CAC9C,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB,CAAC,OAAO,SAAS,MAAM;IACxD,iDAAiD;IACjD,MAAM,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;IAClC,0EAA0E;IAC1E,KAAK,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,SAAS,EAC3C,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAC1C,eAAe,CAAC,MAAM,CAAC,GAAG,SAAS,CAQrC;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,SAAS,MAAM,EACtD,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAC1C,OAAO,CAkBT;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,SAAS,MAAM,EAC5D,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,GAAG,SAAS,GAC3C,OAAO,CAiBT"}
1
+ {"version":3,"file":"unique.d.ts","sourceRoot":"","sources":["../src/unique.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC;;;;GAIG;AACH,MAAM,MAAM,eAAe,CAAC,OAAO,SAAS,MAAM,IAAI,aAAa,CACjE,aAAa,CAAC,OAAO,CAAC,MAAM,OAAO,EAAE,MAAM,CAAC,CAAC,CAC9C,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB,CAAC,OAAO,SAAS,MAAM;IACxD,MAAM,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;IAClC,0EAA0E;IAC1E,KAAK,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,SAAS,EAC3C,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAC1C,eAAe,CAAC,MAAM,CAAC,GAAG,SAAS,CAQrC;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,SAAS,MAAM,EACtD,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAC1C,OAAO,CAeT;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,SAAS,MAAM,EAC5D,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,GAAG,SAAS,GAC3C,OAAO,CAcT"}
package/dist/unique.js CHANGED
@@ -1,10 +1,6 @@
1
1
  /**
2
- * Carry composite groups across a field-key transform the entity-level twin of
3
- * `deriveHints`.
4
- *
5
- * A group whose members did not all survive is dropped rather than narrowed: the
6
- * pair `(listId, docId)` says nothing about `listId` alone, and keeping the
7
- * remnant would silently state a stronger fact than the author ever wrote.
2
+ * Carry composite groups across a key transform. A group that lost a member is DROPPED,
3
+ * not narrowed: `(listId, docId)` says nothing about `listId` alone.
8
4
  */
9
5
  export function deriveUnique(groups, mapKey) {
10
6
  if (!groups)
@@ -15,14 +11,9 @@ export function deriveUnique(groups, mapKey) {
15
11
  return carried.length > 0 ? carried : undefined;
16
12
  }
17
13
  /**
18
- * Carry the projected groups across the same key transform the field-level twin of
19
- * {@link deriveUnique}, and it has to run with it: the group lives in TWO places once
20
- * projected (the declaration and each member's role), so remapping one alone left
21
- * `pick('id','listId')` with a role still claiming a pair whose other member was gone.
22
- *
23
- * Same rule as the declaration — a group that lost a member is dropped, not narrowed.
24
- * The empty self-reference needs no remapping: it names no key, which is exactly why it
25
- * survives a `rename()` for free.
14
+ * The same, on each member's role it must run WITH {@link deriveUnique}: once projected
15
+ * the group lives in two places, so remapping one alone left a role claiming a pair whose
16
+ * other member was gone. The empty self-reference names no key, so it needs no remapping.
26
17
  */
27
18
  export function deriveUniqueRoles(fields, mapKey) {
28
19
  const out = { ...fields };
@@ -36,27 +27,16 @@ export function deriveUniqueRoles(fields, mapKey) {
36
27
  if (carried.length === groups.length)
37
28
  continue; // nothing lost, keep the field as is
38
29
  const { unique: _dropped, ...rest } = field.role;
39
- out[key] = {
40
- ...field,
41
- role: carried.length ? { ...rest, unique: carried } : rest,
42
- };
30
+ out[key] = field.with({ role: carried.length ? { ...rest, unique: carried } : rest });
43
31
  }
44
32
  return out;
45
33
  }
46
34
  /**
47
- * Project the entity's composite groups onto the role of every member — the single
48
- * place the two ways of stating uniqueness meet.
49
- *
50
- * The author writes each fact where it lives: `unique(slug)` on the field it is about,
51
- * `entity(fields, { unique: [['listId','docId']] })` on the entity, because a fact about
52
- * a pair is held by neither field alone. Both then read as ONE normal form on the role
53
- * axis, so every consumer — the DDL, the card, a foreign adapter — has a single shape to
54
- * handle, and a field belonging to two constraints simply carries two member lists.
35
+ * Project the entity's composite groups onto every member's role — the single place the
36
+ * two ways of stating uniqueness meet, so every consumer has one shape to handle.
55
37
  *
56
- * The entity declaration stays the source (`getUnique()` keeps answering it); this is a
57
- * projection of it, never a second place to edit. A named member group is spelled out
58
- * here, while a lone `unique()` keeps its `[]` self-reference — resolved by whoever reads
59
- * it, which is what makes `rename()` free.
38
+ * The entity declaration stays the source (`getUnique()` answers it); this is a projection,
39
+ * never a second place to edit.
60
40
  */
61
41
  export function projectUniqueOntoFields(fields, groups) {
62
42
  if (!groups || groups.length === 0)
@@ -69,10 +49,7 @@ export function projectUniqueOntoFields(fields, groups) {
69
49
  if (!field)
70
50
  continue; // a group naming an absent field states nothing here
71
51
  const already = field.role?.unique ?? [];
72
- projected[key] = {
73
- ...field,
74
- role: { ...field.role, unique: [...already, members] },
75
- };
52
+ projected[key] = field.with({ role: { ...field.role, unique: [...already, members] } });
76
53
  }
77
54
  }
78
55
  return projected;
@@ -1 +1 @@
1
- {"version":3,"file":"unique.js","sourceRoot":"","sources":["../src/unique.ts"],"names":[],"mappings":"AAiCA;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,MAA2C,EAC3C,MAA2C;IAE3C,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAE9B,MAAM,OAAO,GAAG,MAAM;SACnB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;SACjC,MAAM,CAAC,CAAC,KAAK,EAAqB,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC;IAEjF,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AAClD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAe,EACf,MAA2C;IAE3C,MAAM,GAAG,GAAG,EAAE,GAAG,MAAM,EAAY,CAAC;IACpC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/C,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC;QAClC,IAAI,CAAC,MAAM,EAAE,MAAM;YAAE,SAAS;QAE9B,MAAM,OAAO,GAAG,MAAM;aACnB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;aAC7D,MAAM,CAAC,CAAC,KAAK,EAAqB,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC;QAEvF,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM;YAAE,SAAS,CAAG,qCAAqC;QACvF,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC,IAAK,CAAC;QAClD,GAAG,CAAC,GAAG,CAAC,GAAG;YACT,GAAG,KAAK;YACR,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI;SAC3D,CAAC;IACJ,CAAC;IACD,OAAO,GAAc,CAAC;AACxB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAe,EACf,MAA4C;IAE5C,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC;IAElD,MAAM,SAAS,GAAG,EAAE,GAAG,MAAM,EAAY,CAAC;IAC1C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;QAC3B,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YAC1B,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;YAC7B,IAAI,CAAC,KAAK;gBAAE,SAAS,CAAG,qDAAqD;YAC7E,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,MAAM,IAAI,EAAE,CAAC;YACzC,SAAS,CAAC,GAAG,CAAC,GAAG;gBACf,GAAG,KAAK;gBACR,IAAI,EAAE,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,EAAE,OAAO,CAAC,EAAE;aACvD,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,SAAoB,CAAC;AAC9B,CAAC"}
1
+ {"version":3,"file":"unique.js","sourceRoot":"","sources":["../src/unique.ts"],"names":[],"mappings":"AAyBA;;;GAGG;AACH,MAAM,UAAU,YAAY,CAC1B,MAA2C,EAC3C,MAA2C;IAE3C,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAE9B,MAAM,OAAO,GAAG,MAAM;SACnB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;SACjC,MAAM,CAAC,CAAC,KAAK,EAAqB,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC;IAEjF,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AAClD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAe,EACf,MAA2C;IAE3C,MAAM,GAAG,GAAG,EAAE,GAAG,MAAM,EAAY,CAAC;IACpC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/C,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC;QAClC,IAAI,CAAC,MAAM,EAAE,MAAM;YAAE,SAAS;QAE9B,MAAM,OAAO,GAAG,MAAM;aACnB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;aAC7D,MAAM,CAAC,CAAC,KAAK,EAAqB,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC;QAEvF,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM;YAAE,SAAS,CAAG,qCAAqC;QACvF,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC,IAAK,CAAC;QAClD,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACxF,CAAC;IACD,OAAO,GAAc,CAAC;AACxB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAe,EACf,MAA4C;IAE5C,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC;IAElD,MAAM,SAAS,GAAG,EAAE,GAAG,MAAM,EAAY,CAAC;IAC1C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;QAC3B,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YAC1B,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;YAC7B,IAAI,CAAC,KAAK;gBAAE,SAAS,CAAG,qDAAqD;YAC7E,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,MAAM,IAAI,EAAE,CAAC;YACzC,SAAS,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QAC1F,CAAC;IACH,CAAC;IACD,OAAO,SAAoB,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,4 @@
1
+ export type { ValidationError, ValidationResult, Checked } from "./result.js";
2
+ export type { ValidateOptions } from "./options.js";
3
+ export { Judge } from "./judge.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/validation/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC9E,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { Judge } from "./judge.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/validation/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,55 @@
1
+ import type { Field, Fields } from '../field/index.js';
2
+ import type { Checked, ValidationResult } from './result.js';
3
+ import type { ValidateOptions } from './options.js';
4
+ /**
5
+ * The one judge, at the three levels anything is judged: a field's DECLARATION, one VALUE
6
+ * against its shape, a ROW against a field map. Same answer shape at all three.
7
+ *
8
+ * A class and not three functions because it holds state: the per-shape plan cache. The
9
+ * closed vocabularies belong to the axes that close them — this file imports them.
10
+ */
11
+ export declare class Judge {
12
+ /**
13
+ * One plan per shape — the shape object IS the source JSON Schema (the closed
14
+ * grammar the helpers emit), handed straight to the engine. Keyed by the shape
15
+ * reference (stable: derivations copy field refs, `partial()` spreads the field but
16
+ * keeps the same `shape`), built once and reused so the parse cost is amortised —
17
+ * and so an unresolvable format is refused once per shape, never per value.
18
+ */
19
+ private static readonly plans;
20
+ private static planFor;
21
+ /**
22
+ * Resolve a declared format, and REFUSE a name neither the engine nor the registry
23
+ * knows: the engine ignores an unknown format in silence, so a typo would let every
24
+ * value through while the card claims the field is constrained.
25
+ *
26
+ * SCOPE: the field's own shape — a format nested inside `json(Entity)` is not reached.
27
+ */
28
+ private static customFormatOf;
29
+ /**
30
+ * Validate a present value (null included) against its field — pure, never mutates.
31
+ *
32
+ * Reads `shape` and nothing else, so it answers "is this a legal value?" without ever
33
+ * asking who is speaking. That is what makes it usable on the way OUT of the domain,
34
+ * where the client-only axes (`boundary`, `lifecycle`) do not apply.
35
+ */
36
+ static value(field: Field, value: unknown): Checked;
37
+ /** `'now' | 'optional' | { value } | { generate: <name> }` — the four ways absence is answered. */
38
+ private static checkCreate;
39
+ private static checkLifecycle;
40
+ private static checkRelation;
41
+ private static checkRole;
42
+ /**
43
+ * The registry is OPEN, so a name cannot be checked here — `declaredBoundary` resolves it
44
+ * and throws `Unknown boundary alias` at the one place that can know. Only the FORM is
45
+ * judged: a name, or a pair of directional rules.
46
+ */
47
+ private static checkBoundary;
48
+ /** Judge a field's declaration — the five axes against their closed vocabularies. */
49
+ static field(value: unknown): ValidationResult<Field>;
50
+ /** Judge an input object against a field map — membership, absence, and who may speak. */
51
+ static row(fields: Fields, input: unknown, opts?: ValidateOptions): ValidationResult<Record<string, unknown>>;
52
+ /** The same judgment, as a boolean. */
53
+ static isField(value: unknown): value is Field;
54
+ }
55
+ //# sourceMappingURL=judge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"judge.d.ts","sourceRoot":"","sources":["../../src/validation/judge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAA0B,MAAM,mBAAmB,CAAC;AAK/E,OAAO,KAAK,EAAE,OAAO,EAAmB,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAgBpD;;;;;;GAMG;AACH,qBAAa,KAAK;IAChB;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAoC;IAEjE,OAAO,CAAC,MAAM,CAAC,OAAO;IAetB;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IAW7B;;;;;;OAMG;IAEH,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAiClD;IAED,mGAAmG;IACnG,OAAO,CAAC,MAAM,CAAC,WAAW;IAoB1B,OAAO,CAAC,MAAM,CAAC,cAAc;IAoB7B,OAAO,CAAC,MAAM,CAAC,aAAa;IAgC5B,OAAO,CAAC,MAAM,CAAC,SAAS;IAiCxB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IAwB5B,qFAAqF;IAErF,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAsCpD;IAED,0FAA0F;IAC1F,MAAM,CAAC,GAAG,CACR,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,EACd,IAAI,GAAE,eAAoB,GACvB,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CA8E7C;IAED,uCAAuC;IACvC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAE7C;CACF"}
@@ -0,0 +1,351 @@
1
+ import { CREATE_TOKENS, UPDATE_TOKENS } from '../field/lifecycle.js';
2
+ import { ON_DELETE, RELATION_KINDS } from '../field/role.js';
3
+ import { Anatomy, Formats, boundaryOf, isShape, resolveBoundary, } from '../field/index.js';
4
+ import { Validator, format as engineFormats } from '@cfworker/json-schema';
5
+ const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
6
+ const oneOfTokens = (v, tokens) => typeof v === 'string' && tokens.includes(v);
7
+ /**
8
+ * The one judge, at the three levels anything is judged: a field's DECLARATION, one VALUE
9
+ * against its shape, a ROW against a field map. Same answer shape at all three.
10
+ *
11
+ * A class and not three functions because it holds state: the per-shape plan cache. The
12
+ * closed vocabularies belong to the axes that close them — this file imports them.
13
+ */
14
+ export class Judge {
15
+ /**
16
+ * One plan per shape — the shape object IS the source JSON Schema (the closed
17
+ * grammar the helpers emit), handed straight to the engine. Keyed by the shape
18
+ * reference (stable: derivations copy field refs, `partial()` spreads the field but
19
+ * keeps the same `shape`), built once and reused so the parse cost is amortised —
20
+ * and so an unresolvable format is refused once per shape, never per value.
21
+ */
22
+ static plans = new WeakMap();
23
+ static planFor(shape) {
24
+ let p = this.plans.get(shape);
25
+ if (!p) {
26
+ const base = Anatomy.of(shape).base;
27
+ const formatName = base?.type === 'string' ? base.format : undefined;
28
+ p = {
29
+ validator: new Validator(shape, '2020-12', true),
30
+ custom: formatName === undefined ? undefined : this.customFormatOf(formatName),
31
+ formatName,
32
+ };
33
+ this.plans.set(shape, p);
34
+ }
35
+ return p;
36
+ }
37
+ /**
38
+ * Resolve a declared format, and REFUSE a name neither the engine nor the registry
39
+ * knows: the engine ignores an unknown format in silence, so a typo would let every
40
+ * value through while the card claims the field is constrained.
41
+ *
42
+ * SCOPE: the field's own shape — a format nested inside `json(Entity)` is not reached.
43
+ */
44
+ static customFormatOf(name) {
45
+ const custom = Formats.resolve(name);
46
+ if (!custom && !(name in engineFormats)) {
47
+ throw new Error(`Unknown format: '${name}'. Register it with Formats.register('${name}', …) — ` +
48
+ `the engine judges ${Object.keys(engineFormats).length} formats natively and this is not one of them.`);
49
+ }
50
+ return custom;
51
+ }
52
+ /**
53
+ * Validate a present value (null included) against its field — pure, never mutates.
54
+ *
55
+ * Reads `shape` and nothing else, so it answers "is this a legal value?" without ever
56
+ * asking who is speaking. That is what makes it usable on the way OUT of the domain,
57
+ * where the client-only axes (`boundary`, `lifecycle`) do not apply.
58
+ */
59
+ static value(field, value) {
60
+ const shape = field.shape;
61
+ // The pre-engine guards dispatch on the BASE type — `shape.type` itself may be
62
+ // the nullable union. They only short-circuit NON-null values: null always goes
63
+ // to the engine, whose union judges it (that is the whole nullability model).
64
+ const base = Anatomy.of(shape).base;
65
+ if (value !== null) {
66
+ // Opaque JSON (`json()`): no nested shape → passes through unchecked.
67
+ // `json(Entity)` carries `properties` and falls through to the engine,
68
+ // which validates the nesting natively (it's plain JSON Schema).
69
+ if (base?.type === 'object' && !base.properties)
70
+ return { value };
71
+ // A date-time field also accepts a live Date — the domain value, not its JSON wire
72
+ // string — which the boundary then passes through. The engine validates JSON, so a
73
+ // Date (a non-JSON value) skips the schema check here; a date STRING is validated below.
74
+ if (base?.type === 'string' && base.format === 'date-time' && value instanceof Date) {
75
+ return Number.isNaN(value.getTime()) ? { error: 'Invalid date' } : { value };
76
+ }
77
+ // NaN is `typeof number` yet has no JSON form, so the JSON-centric engine accepts it —
78
+ // guard it here (same nature as the Date case: a JS value JSON can't represent).
79
+ if ((base?.type === 'number' || base?.type === 'integer') && typeof value === 'number' && Number.isNaN(value)) {
80
+ return { error: 'Expected a number' };
81
+ }
82
+ }
83
+ const plan = this.planFor(shape);
84
+ const result = plan.validator.validate(value);
85
+ if (!result.valid)
86
+ return { error: result.errors[0]?.error ?? 'Invalid value' };
87
+ // A registered format judges AFTER the engine passed — so a custom rule composes
88
+ // with the built-ins instead of replacing them, and the message is the engine's
89
+ // own wording: from the outside, `siret` fails exactly like `email` does.
90
+ if (plan.custom && typeof value === 'string' && !plan.custom(value)) {
91
+ return { error: `String does not match format "${plan.formatName}".` };
92
+ }
93
+ return { value };
94
+ }
95
+ /** `'now' | 'optional' | { value } | { generate: <name> }` — the four ways absence is answered. */
96
+ static checkCreate(rule, errors) {
97
+ if (oneOfTokens(rule, CREATE_TOKENS))
98
+ return;
99
+ if (isObject(rule)) {
100
+ if ("value" in rule)
101
+ return; // any value — the shape judges it (`assertDefaultsAreValid`)
102
+ if ("generate" in rule) {
103
+ if (typeof rule.generate !== "string") {
104
+ errors.push({
105
+ path: "lifecycle.create.generate",
106
+ message: "Expected a generator name",
107
+ });
108
+ }
109
+ return;
110
+ }
111
+ }
112
+ errors.push({
113
+ path: "lifecycle.create",
114
+ message: `Expected 'now', 'optional', { value } or { generate } — got ${JSON.stringify(rule)}`,
115
+ });
116
+ }
117
+ static checkLifecycle(lifecycle, errors) {
118
+ if (!isObject(lifecycle)) {
119
+ errors.push({
120
+ path: "lifecycle",
121
+ message: `Expected an object — got ${JSON.stringify(lifecycle)}`,
122
+ });
123
+ return;
124
+ }
125
+ if (lifecycle.create !== undefined)
126
+ this.checkCreate(lifecycle.create, errors);
127
+ if (lifecycle.update !== undefined &&
128
+ !oneOfTokens(lifecycle.update, UPDATE_TOKENS)) {
129
+ errors.push({
130
+ path: "lifecycle.update",
131
+ message: `Expected 'now' or 'forbidden' — got ${JSON.stringify(lifecycle.update)}`,
132
+ });
133
+ }
134
+ }
135
+ static checkRelation(relation, errors) {
136
+ if (!isObject(relation)) {
137
+ errors.push({
138
+ path: "role.relation",
139
+ message: `Expected an object — got ${JSON.stringify(relation)}`,
140
+ });
141
+ return;
142
+ }
143
+ if (!oneOfTokens(relation.kind, RELATION_KINDS)) {
144
+ errors.push({
145
+ path: "role.relation.kind",
146
+ message: `Expected 'one' or 'many' — got ${JSON.stringify(relation.kind)}`,
147
+ });
148
+ }
149
+ // A thunk, never the class: it is what lets a circular relation resolve lazily.
150
+ if (typeof relation.to !== "function") {
151
+ errors.push({
152
+ path: "role.relation.to",
153
+ message: "Expected a thunk returning the target entity",
154
+ });
155
+ }
156
+ if (relation.onDelete !== undefined &&
157
+ !oneOfTokens(relation.onDelete, ON_DELETE)) {
158
+ errors.push({
159
+ path: "role.relation.onDelete",
160
+ message: `Expected 'cascade', 'restrict' or 'set null' — got ${JSON.stringify(relation.onDelete)}`,
161
+ });
162
+ }
163
+ }
164
+ static checkRole(role, errors) {
165
+ if (!isObject(role)) {
166
+ errors.push({
167
+ path: "role",
168
+ message: `Expected an object — got ${JSON.stringify(role)}`,
169
+ });
170
+ return;
171
+ }
172
+ for (const flag of ["primary", "index"]) {
173
+ if (role[flag] !== undefined && typeof role[flag] !== "boolean") {
174
+ errors.push({
175
+ path: `role.${flag}`,
176
+ message: `Expected a boolean — got ${JSON.stringify(role[flag])}`,
177
+ });
178
+ }
179
+ }
180
+ if (role.unique !== undefined) {
181
+ const groups = role.unique;
182
+ const legal = Array.isArray(groups) &&
183
+ groups.every((g) => Array.isArray(g) && g.every((m) => typeof m === "string"));
184
+ if (!legal) {
185
+ errors.push({
186
+ path: "role.unique",
187
+ message: "Expected groups of field names — string[][]",
188
+ });
189
+ }
190
+ }
191
+ if (role.relation !== undefined)
192
+ this.checkRelation(role.relation, errors);
193
+ }
194
+ /**
195
+ * The registry is OPEN, so a name cannot be checked here — `declaredBoundary` resolves it
196
+ * and throws `Unknown boundary alias` at the one place that can know. Only the FORM is
197
+ * judged: a name, or a pair of directional rules.
198
+ */
199
+ static checkBoundary(boundary, errors) {
200
+ if (typeof boundary === "string")
201
+ return;
202
+ if (!isObject(boundary)) {
203
+ errors.push({
204
+ path: "boundary",
205
+ message: `Expected an alias name or { in, out } — got ${JSON.stringify(boundary)}`,
206
+ });
207
+ return;
208
+ }
209
+ for (const [side, verb] of [
210
+ ["in", "decode"],
211
+ ["out", "encode"],
212
+ ]) {
213
+ const rule = boundary[side];
214
+ if (rule === undefined || rule === "closed")
215
+ continue;
216
+ if (!isObject(rule) || typeof rule[verb] !== "string") {
217
+ errors.push({
218
+ path: `boundary.${side}`,
219
+ message: `Expected 'closed' or { ${verb}: <name> }`,
220
+ });
221
+ }
222
+ }
223
+ }
224
+ /** Judge a field's declaration — the five axes against their closed vocabularies. */
225
+ static field(value) {
226
+ if (!isObject(value)) {
227
+ return {
228
+ success: false,
229
+ errors: [
230
+ {
231
+ path: ".",
232
+ message: `Expected an object — got ${JSON.stringify(value)}`,
233
+ },
234
+ ],
235
+ };
236
+ }
237
+ const errors = [];
238
+ if (!isShape(value.shape)) {
239
+ errors.push({
240
+ path: "shape",
241
+ message: `Every field states a shape — got ${JSON.stringify(value.shape)}`,
242
+ });
243
+ }
244
+ if (value.lifecycle !== undefined)
245
+ this.checkLifecycle(value.lifecycle, errors);
246
+ if (value.role !== undefined)
247
+ this.checkRole(value.role, errors);
248
+ if (value.boundary !== undefined)
249
+ this.checkBoundary(value.boundary, errors);
250
+ if (value.meta !== undefined) {
251
+ if (!isObject(value.meta)) {
252
+ errors.push({
253
+ path: "meta",
254
+ message: `Expected an object — got ${JSON.stringify(value.meta)}`,
255
+ });
256
+ }
257
+ else if (value.meta.description !== undefined &&
258
+ typeof value.meta.description !== "string") {
259
+ errors.push({ path: "meta.description", message: "Expected a string" });
260
+ }
261
+ }
262
+ return errors.length ? { success: false, errors } : { success: true, data: value };
263
+ }
264
+ /** Judge an input object against a field map — membership, absence, and who may speak. */
265
+ static row(fields, input, opts = {}) {
266
+ if (typeof input !== 'object' || input === null) {
267
+ return { success: false, errors: [{ path: '.', message: 'Expected an object' }] };
268
+ }
269
+ const data = input;
270
+ const errors = [];
271
+ const out = {};
272
+ // Refuse, never strip: a key outside the contract is an error, not a silent
273
+ // drop. The client derives from the same contract (a form cannot emit a
274
+ // foreign key), so a stranger is a bug or an attack — both deserve the 400.
275
+ // Stripping would also let the envelope diverge from GraphQL, which refuses
276
+ // unknown input keys by construction.
277
+ //
278
+ // A FACT is judged by this same rule, deliberately — the one place it was tempting to
279
+ // relax it. A subscriber's copy of a fact can be older than the sender's, so tolerating
280
+ // a stranger key would let a rolling deployment through; it would also mean a reader
281
+ // silently ignoring a field it was supposed to handle. If the judge refuses, that is
282
+ // the end of it: the sender re-syncs its readers before it ships.
283
+ for (const key of Object.keys(data)) {
284
+ if (!(key in fields)) {
285
+ errors.push({ path: key, message: 'Unknown field' });
286
+ }
287
+ }
288
+ for (const [key, field] of Object.entries(fields)) {
289
+ const path = key;
290
+ const value = data[key];
291
+ if (value === undefined) {
292
+ if (opts.patch)
293
+ continue; // patch: an unsent field is left untouched
294
+ // A read-only field is server-owned: its absence from a client input is
295
+ // never "Required" (same stance as OpenAPI readOnly+required).
296
+ if (boundaryOf(field).in === 'closed')
297
+ continue;
298
+ // Absence is answered by `lifecycle.create` — a key access on the normal
299
+ // form. The judge only asks "is there a rule?": any rule ('now',
300
+ // 'optional', { value }, { generate }) makes absence legal, and the field
301
+ // is omitted from the result. Realisation is the storage adapter's role.
302
+ if (field.lifecycle?.create !== undefined)
303
+ continue;
304
+ // No create rule: a `many` relation defaults to the empty collection
305
+ // (graph semantics, read on `role` — never a shared `{ value: [] }`).
306
+ if (field.role?.relation?.kind === 'many') {
307
+ out[key] = [];
308
+ continue;
309
+ }
310
+ errors.push({ path, message: 'Required' });
311
+ continue;
312
+ }
313
+ // A PRESENT value can be illegal by an axis other than shape:
314
+ // boundary `in: 'closed'` — a read-only field never crosses inbound;
315
+ // lifecycle `update: 'forbidden'` — re-supplying an immutable field in a patch.
316
+ if (boundaryOf(field).in === 'closed') {
317
+ errors.push({ path, message: 'Read-only' });
318
+ continue;
319
+ }
320
+ if (opts.patch && field.lifecycle?.update === 'forbidden') {
321
+ errors.push({ path, message: 'Immutable' });
322
+ continue;
323
+ }
324
+ // The shape predicate judges the value — null included, via the `[T,'null']`
325
+ // union. Then the boundary's decode converts wire→domain; null skips decode
326
+ // (a legal null is already the domain value, there is nothing to convert).
327
+ const checked = this.value(field, value);
328
+ if ('error' in checked) {
329
+ errors.push({ path, message: checked.error });
330
+ continue;
331
+ }
332
+ if (checked.value === null) {
333
+ out[key] = null;
334
+ continue;
335
+ }
336
+ const decoded = resolveBoundary(field).decode(checked.value);
337
+ if ('error' in decoded)
338
+ errors.push({ path, message: decoded.error });
339
+ else
340
+ out[key] = decoded.value;
341
+ }
342
+ if (errors.length > 0)
343
+ return { success: false, errors };
344
+ return { success: true, data: out };
345
+ }
346
+ /** The same judgment, as a boolean. */
347
+ static isField(value) {
348
+ return this.field(value).success;
349
+ }
350
+ }
351
+ //# sourceMappingURL=judge.js.map