@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,7 +1,7 @@
1
1
  import type { Axis, Resolver } from '../Axis.js';
2
2
  import { refuse, oneOfTokens } from '../../projection/card/admission.js';
3
3
  import { isObject, lowerFirst, shown } from '../../lib/utils.js';
4
- import type { ValidationError } from '../../validation.js';
4
+ import type { ValidationError } from '../../lib/validation.js';
5
5
  import { ON_DELETE, RELATION_KINDS, type EntityConstructor, type Relation } from './Relation.js';
6
6
  import { type RoleRules } from './Role.js';
7
7
  import type { RoleDescriptor } from '../../projection/card/Descriptor.js';
@@ -11,13 +11,13 @@ export const roleAxis: Axis<RoleRules, RoleDescriptor> = {
11
11
 
12
12
  validator(value, errors) {
13
13
  if (!isObject(value)) {
14
- errors.push({ path: 'role', message: `Expected an object — got ${shown(value)}` });
14
+ errors.push({ path: ['role'], message: `Expected an object — got ${shown(value)}` });
15
15
  return;
16
16
  }
17
17
  for (const flag of ['primary', 'index', 'unique'] as const) {
18
18
  if (value[flag] !== undefined && typeof value[flag] !== 'boolean') {
19
19
  errors.push({
20
- path: `role.${flag}`,
20
+ path: ['role', flag],
21
21
  message: `Expected a boolean — got ${shown(value[flag])}`,
22
22
  });
23
23
  }
@@ -73,27 +73,27 @@ export const roleAxis: Axis<RoleRules, RoleDescriptor> = {
73
73
  };
74
74
 
75
75
  /**
76
- * So a relation is validated where `() => Post` is required a card carries a name.
77
- * FR : pour qu'une relation soit jugée `() => Post` est exigé, contrairement à une carte.
78
- * `{ to: User, kind: 'one' }` → error `Expected a function returning the target entity, such as () => Post`
76
+ * Judges a relation where `to` is a thunk; a card states a NAME, resolved by `toSchema` first.
77
+ * FR : juge une relation dont `to` est une fonction ; une carte y met un NOM, résolu avant.
78
+ * `{ to: User, kind: 'one' }` → `Expected a function returning the target entity, () => Post`
79
79
  */
80
80
  function validateRelation(relation: unknown, errors: ValidationError[]): void {
81
81
  if (!isObject(relation)) {
82
- errors.push({ path: 'role.relation', message: `Expected an object — got ${shown(relation)}` });
82
+ errors.push({ path: ['role', 'relation'], message: `Expected an object — got ${shown(relation)}` });
83
83
  return;
84
84
  }
85
85
  if (!oneOfTokens(relation.kind, RELATION_KINDS)) {
86
86
  errors.push({
87
- path: 'role.relation.kind',
87
+ path: ['role', 'relation', 'kind'],
88
88
  message: `Expected 'one' or 'many' — got ${shown(relation.kind)}`,
89
89
  });
90
90
  }
91
91
  if (typeof relation.to !== 'function') {
92
- errors.push({ path: 'role.relation.to', message: 'Expected a function returning the target entity, such as () => Post' });
92
+ errors.push({ path: ['role', 'relation', 'to'], message: 'Expected a function returning the target entity, such as () => Post' });
93
93
  }
94
94
  if (relation.onDelete !== undefined && !oneOfTokens(relation.onDelete, ON_DELETE)) {
95
95
  errors.push({
96
- path: 'role.relation.onDelete',
96
+ path: ['role', 'relation', 'onDelete'],
97
97
  message: `Expected 'cascade', 'restrict' or 'set null' — got ${shown(relation.onDelete)}`,
98
98
  });
99
99
  }
@@ -1,6 +1,17 @@
1
- import type { JSONSchema7 } from 'json-schema';
1
+ import type { JSONSchema7, JSONSchema7TypeName } from 'json-schema';
2
2
  import type { StringFormat } from './Formats.js';
3
3
 
4
+ /**
5
+ * The standard's own list, less `null` — a shape states that as the `[T,'null']` union —
6
+ * and with `string` in the three forms every projection here tells apart: a `date()`, a
7
+ * bounded set, and everything else. Those three are the whole of what this package adds.
8
+ */
9
+ export type ShapeType =
10
+ | Exclude<JSONSchema7TypeName, 'null' | 'string'>
11
+ | 'text'
12
+ | 'date'
13
+ | 'choice';
14
+
4
15
  type Nullably<T extends string> = T | readonly [T, 'null'];
5
16
 
6
17
  interface StringConstraints { minLength?: number; maxLength?: number; pattern?: string; enum?: readonly (string | null)[]; format?: StringFormat }
@@ -17,16 +28,6 @@ export type Shape =
17
28
 
18
29
  const SHAPE_TYPES = ['string', 'number', 'integer', 'boolean', 'array', 'object'] as const;
19
30
 
20
- function isShapeImpl(value: unknown): value is Shape {
21
- if (typeof value !== 'object' || value === null) return false;
22
- const type = (value as Shape).type;
23
- const names = Array.isArray(type) ? type : [type];
24
- return (
25
- names.some((name) => (SHAPE_TYPES as readonly unknown[]).includes(name)) &&
26
- names.every((name) => name === 'null' || (SHAPE_TYPES as readonly unknown[]).includes(name))
27
- );
28
- }
29
-
30
31
  type BaseShape =
31
32
  | ({ type: 'string' } & StringConstraints)
32
33
  | ({ type: 'number' | 'integer' } & NumericConstraints)
@@ -34,37 +35,21 @@ type BaseShape =
34
35
  | ({ type: 'array' } & ArrayConstraints)
35
36
  | ({ type: 'object' } & ObjectConstraints);
36
37
 
37
- function nullableShapeImpl(shape: Shape): Shape {
38
- if (Array.isArray(shape.type)) return shape;
39
- const nullable = { ...shape, type: [shape.type, 'null'] } as unknown as Shape;
40
- if ('enum' in nullable && nullable.enum && !nullable.enum.includes(null)) {
41
- (nullable as { enum: readonly (string | null)[] }).enum = [...nullable.enum, null];
42
- }
43
- return nullable;
44
- }
45
-
46
38
  interface ShapeParts {
47
39
  base?: BaseShape;
48
40
  nullable: boolean;
49
41
  }
50
42
 
51
- function collectPatterns(value: unknown, found: string[]): void {
52
- if (Array.isArray(value)) {
53
- for (const member of value) collectPatterns(member, found);
54
-
55
- return;
56
- }
57
- if (typeof value !== 'object' || value === null) return;
58
-
59
- const { pattern } = value as { pattern?: unknown };
60
- if (typeof pattern === 'string') found.push(pattern);
61
-
62
- for (const member of Object.values(value)) collectPatterns(member, found);
63
- }
64
-
65
43
  export class Shapes {
66
44
  static is(value: unknown): value is Shape {
67
- return isShapeImpl(value);
45
+ if (typeof value !== 'object' || value === null) return false;
46
+ const type = (value as Shape).type;
47
+ const names = Array.isArray(type) ? type : [type];
48
+
49
+ return (
50
+ names.some((name) => (SHAPE_TYPES as readonly unknown[]).includes(name)) &&
51
+ names.every((name) => name === 'null' || (SHAPE_TYPES as readonly unknown[]).includes(name))
52
+ );
68
53
  }
69
54
 
70
55
  /**
@@ -75,13 +60,33 @@ export class Shapes {
75
60
  */
76
61
  static patterns(shape: unknown): string[] {
77
62
  const found: string[] = [];
78
- collectPatterns(shape, found);
63
+ Shapes.collectPatterns(shape, found);
79
64
 
80
65
  return found;
81
66
  }
82
67
 
68
+ private static collectPatterns(value: unknown, found: string[]): void {
69
+ if (Array.isArray(value)) {
70
+ for (const member of value) Shapes.collectPatterns(member, found);
71
+
72
+ return;
73
+ }
74
+ if (typeof value !== 'object' || value === null) return;
75
+
76
+ const { pattern } = value as { pattern?: unknown };
77
+ if (typeof pattern === 'string') found.push(pattern);
78
+
79
+ for (const member of Object.values(value)) Shapes.collectPatterns(member, found);
80
+ }
81
+
83
82
  static nullable(shape: Shape): Shape {
84
- return nullableShapeImpl(shape);
83
+ if (Array.isArray(shape.type)) return shape;
84
+ const nullable = { ...shape, type: [shape.type, 'null'] } as unknown as Shape;
85
+ if ('enum' in nullable && nullable.enum && !nullable.enum.includes(null)) {
86
+ (nullable as { enum: readonly (string | null)[] }).enum = [...nullable.enum, null];
87
+ }
88
+
89
+ return nullable;
85
90
  }
86
91
 
87
92
  private static readonly cache = new WeakMap<object, ShapeParts>();
@@ -109,6 +114,20 @@ export class Shapes {
109
114
  static isNullable(shape?: Shape): boolean {
110
115
  return this.of(shape).nullable;
111
116
  }
117
+
118
+ /**
119
+ * `shape.type` read off the base, except that a `string` answers `date` or `choice`.
120
+ * FR : `shape.type` lu sur la base, sauf qu'une chaîne répond `date` ou `choice`.
121
+ * `typeOf({ type: ['string', 'null'], format: 'date-time' })` → `'date'`
122
+ */
123
+ static typeOf(shape?: Shape): ShapeType | undefined {
124
+ const base = this.of(shape).base;
125
+ if (!base) return undefined;
126
+ if (base.type !== 'string') return base.type;
127
+ if (base.format === 'date-time') return 'date';
128
+
129
+ return base.enum?.length ? 'choice' : 'text';
130
+ }
112
131
  }
113
132
 
114
133
  type Assert<T extends true> = T;
@@ -116,3 +135,8 @@ type ShapeKeys<T> = T extends unknown ? keyof T : never;
116
135
  type _ShapeConformsToJsonSchema = Assert<
117
136
  [Exclude<ShapeKeys<Shape>, keyof JSONSchema7>] extends [never] ? true : false
118
137
  >;
138
+ type _ShapeTypesAreTheStandardsLessNull = Assert<
139
+ [Exclude<Exclude<JSONSchema7TypeName, 'null'>, (typeof SHAPE_TYPES)[number]>] extends [never]
140
+ ? true
141
+ : false
142
+ >;
@@ -36,7 +36,7 @@ export class EntityAdapterSet {
36
36
  return new EntityAdapterSet(adapters);
37
37
  }
38
38
 
39
- /** Folds several into one, a later field entry replacing the earlier. Takes sets only, so */
39
+ /** Folds several into one, a later field entry replacing the earlier. */
40
40
  static merged(adapterSets: readonly EntityAdapterSet[]): EntityAdapterSet {
41
41
  const configurations: AdapterConfigurations = {};
42
42
 
@@ -58,7 +58,7 @@ export class EntityAdapterSet {
58
58
  ];
59
59
  }
60
60
 
61
- /** Follows a derivation — `pick`, `omit` and `rename` are one gesture: what remains, and */
61
+ /** Follows a derivation — `pick`, `omit` and `rename` are one gesture. */
62
62
  rename(transform: (key: string) => string | undefined): EntityAdapterSet {
63
63
  const renamed: AdapterConfigurations = {};
64
64
 
package/src/entity.ts CHANGED
@@ -3,8 +3,8 @@ import { type EntityDeclarations } from './entity/EntityDeclarations.js';
3
3
  import { Schema, type SchemaConstructor } from './Schema.js';
4
4
 
5
5
  /**
6
- * So the whole framework starts from one call: fields, and what they say about themselves.
7
- * FR : pour que tout parte d'un appel : les champs, et ce qu'ils disent d'eux-mêmes.
6
+ * The one call everything derives from: the fields, and what the entity states about them.
7
+ * FR : l'appel dont tout dérive : les champs, et ce que l'entité en dit.
8
8
  * `class Post extends entity({ id: primary(), title: text() }, { unique: [['title']] }) {}`
9
9
  */
10
10
  export function entity<TFields extends Fields>(
@@ -6,6 +6,7 @@ import type { Meta } from './Meta.js';
6
6
  import type { Axis } from '../axis/Axis.js';
7
7
  import { FieldDeclarationValidator } from '../validator/FieldDeclarationValidator.js';
8
8
  import { FieldValueValidator } from '../validator/FieldValueValidator.js';
9
+ import { dotted } from '../lib/validation.js';
9
10
 
10
11
  export type Fields = Record<string, Field>;
11
12
 
@@ -28,7 +29,7 @@ export class Field<T = unknown> {
28
29
  if (!verdict.success) {
29
30
  throw new Error(
30
31
  `${key ? `Field '${key}': ` : ''}` +
31
- verdict.errors.map((e) => `${e.path}: ${e.message}`).join('; '),
32
+ verdict.errors.map((e) => `${dotted(e.path)}: ${e.message}`).join('; '),
32
33
  );
33
34
  }
34
35
 
@@ -10,20 +10,19 @@ export class FieldSet<TFields extends Fields = Fields> {
10
10
  }
11
11
 
12
12
  /**
13
- * So a declared unique lands where it belongs: on the field, or on the schema.
14
- * FR : pour qu'une unicité déclarée atterrisse sur le champ ou sur le schéma.
15
- * `declaring(fields, [['email'], ['listId', 'docId']])`
16
- * → `email` carries `role.unique`, the pair becomes a schema constraint
13
+ * Splits a declared `unique`: a group of one lands on the field, several stay on the schema.
14
+ * FR : partage un `unique` déclaré : un groupe d'un va au champ, plusieurs restent au schéma.
15
+ * `declaring(fields, [['email'], ['listId', 'docId']])` → `email` carries it, the pair is a group
17
16
  */
18
17
  static declaring<TFields extends Fields>(
19
18
  declared: TFields,
20
19
  unique?: CompositeUnique<TFields>,
21
- ): { fields: TFields; unique: CompositeUnique<TFields> | undefined } {
20
+ ): { fields: TFields; groups: CompositeUnique<TFields> } {
22
21
  const fields: Fields = {};
23
22
  for (const [key, field] of Object.entries(declared))
24
23
  fields[key] = new Field(field, key);
25
24
 
26
- const composite: string[][] = [];
25
+ const composite: FieldName<TFields>[][] = [];
27
26
  for (const group of unique ?? []) {
28
27
  const missing = group.filter((key) => !Object.hasOwn(fields, key));
29
28
  if (missing.length)
@@ -40,10 +39,7 @@ export class FieldSet<TFields extends Fields = Fields> {
40
39
  composite.push([...group]);
41
40
  }
42
41
 
43
- return {
44
- fields: fields as TFields,
45
- unique: deduplicated(composite) as CompositeUnique<TFields> | undefined,
46
- };
42
+ return { fields: fields as TFields, groups: composite };
47
43
  }
48
44
 
49
45
  /** A second `primary` is refused, naming both, rather than the first winning silently. */
@@ -63,13 +59,3 @@ export class FieldSet<TFields extends Fields = Fields> {
63
59
  }
64
60
 
65
61
  }
66
-
67
- /** The order is part of a group: `['a','b']` and `['b','a']` are two constraints. */
68
- export function deduplicated(
69
- groups: readonly (readonly string[])[],
70
- ): readonly (readonly string[])[] | undefined {
71
- const seen = new Map<string, readonly string[]>();
72
- for (const group of groups) seen.set(JSON.stringify(group), group);
73
-
74
- return seen.size ? [...seen.values()] : undefined;
75
- }
package/src/index.ts CHANGED
@@ -6,14 +6,14 @@ export { Schema, ANONYMOUS_SCHEMA_NAME, type SchemaConstructor } from './Schema.
6
6
  export { type SchemaView } from './SchemaView.js';
7
7
  export { Field, type Fields } from './field/Field.js';
8
8
  export { FieldSet } from './field/FieldSet.js';
9
- export { type Shape, Shapes } from './axis/shape/Shape.js';
9
+ export { type Shape, type ShapeType, Shapes } from './axis/shape/Shape.js';
10
10
  export { Role, type RoleRules } from './axis/role/Role.js';
11
11
  export { type EntityConstructor } from './axis/role/Relation.js';
12
12
  export { Lifecycle, type LifecycleRules } from './axis/lifecycle/Lifecycle.js';
13
13
  export { Boundary, type BoundaryRef } from './axis/boundary/Boundary.js';
14
14
  export { applyCreate, applyUpdate } from './axis/lifecycle/apply.js';
15
15
  export { InputRefusal } from './validator/InputRefusal.js';
16
- export { type ValidationError, type ValidationResult } from './validation.js';
16
+ export { dotted, type ValidationError, type ValidationResult } from './lib/validation.js';
17
17
 
18
18
  export { Card } from './projection/card/Card.js';
19
19
  export { Bundle } from './projection/card/Bundle.js';
@@ -0,0 +1,25 @@
1
+ export interface ValidationError {
2
+ /**
3
+ * Where the refusal happened: the field, then where under it the shape refused.
4
+ * Empty when the input itself is refused. Segments, never a sentence — a field may
5
+ * legally be named `a.b`, and re-parsing a joined path invents a segment it never had.
6
+ */
7
+ path: readonly string[];
8
+ message: string;
9
+ }
10
+
11
+ export type ValidationResult<T> =
12
+ | { success: true; data: T }
13
+ | { success: false; errors: ValidationError[] };
14
+
15
+ /**
16
+ * A field's verdict. `path` is where INSIDE the value the refusal happened — empty when the
17
+ * field itself is refused, `['street']` when the shape below it is.
18
+ */
19
+ export type Checked = { value: unknown } | { error: string; path?: readonly string[] };
20
+
21
+ /**
22
+ * A path as a message writes it — `addr.street`. One rendering among others, and the
23
+ * reason the value stays segmented: this one cannot be read back.
24
+ */
25
+ export const dotted = (path: readonly string[]): string => path.join('.');
@@ -25,8 +25,8 @@ export interface ValidationCase {
25
25
  }
26
26
 
27
27
  /**
28
- * So every field has at least one case, whatever else it declares.
29
- * FR : pour que chaque champ ait un cas, quoi qu'il déclare par ailleurs.
28
+ * A value of the wrong type, so every field yields at least one case whatever it declares.
29
+ * FR : une valeur du mauvais type, pour que chaque champ rende au moins un cas.
30
30
  * `text()` → `42`; `number()` → `'not-a-value-of-this-shape'`
31
31
  */
32
32
  function wrongTypeFor(field: Field): unknown {
@@ -36,8 +36,8 @@ function wrongTypeFor(field: Field): unknown {
36
36
  }
37
37
 
38
38
  /**
39
- * So a bound stated in a shape produces the case that breaks it, and none is invented.
40
- * FR : pour qu'une borne énoncée produise le cas qui la casse, et aucun autre.
39
+ * One case per bound the shape states, and none for a bound it does not.
40
+ * FR : un cas par borne que la forme énonce, aucun pour une borne absente.
41
41
  * `text({ maxLength: 3 })` → one case with `'xxxx'`; `text()` → no case
42
42
  */
43
43
  function outOfBoundsFor(field: Field): { why: string; value: unknown }[] {
@@ -86,11 +86,11 @@ export class Cases {
86
86
 
87
87
  static holds(
88
88
  expected: ValidationCase['expect'],
89
- result: { success: boolean; errors?: { path: string }[] },
89
+ result: { success: boolean; errors?: { path: readonly string[] }[] },
90
90
  ): boolean {
91
91
  if (expected === 'accept') return result.success;
92
92
  if (result.success) return false;
93
- return (result.errors ?? []).some((error) => error.path === expected.reject);
93
+ return (result.errors ?? []).some((error) => rejected(error.path) === expected.reject);
94
94
  }
95
95
 
96
96
  static get refusals(): string[] {
@@ -103,8 +103,10 @@ export class Cases {
103
103
  }
104
104
 
105
105
  /**
106
- * So the four axes decide which cases exist, and no list of them is maintained by hand.
107
- * FR : pour que les axes décident des cas, sans liste tenue à la main.
106
+ * Reads the four axes rather than a list kept by hand: a case exists because a field
107
+ * declares the rule it breaks.
108
+ * FR : lit les quatre axes plutôt qu'une liste tenue à la main : un cas existe parce qu'un
109
+ * champ déclare la règle qu'il casse.
108
110
  */
109
111
  function enumerate(entity: SchemaView, valid: Record<string, unknown>): ValidationCase[] {
110
112
  const fields = entity.getFields();
@@ -174,3 +176,6 @@ function enumerate(entity: SchemaView, valid: Record<string, unknown>): Validati
174
176
 
175
177
  return cases;
176
178
  }
179
+
180
+ /** What a case names: the input itself, or the field the refusal lands on. */
181
+ const rejected = (path: readonly string[]): string => (path.length === 0 ? '.' : path[0]!);
@@ -16,7 +16,7 @@ export class Visibility {
16
16
  for (const [name, field] of Object.entries(this.fields)) {
17
17
  const role = Role.of(field);
18
18
  if (role.isPrimary) continue;
19
- if (Lifecycle.of(field).create === 'now') continue;
19
+ if (Lifecycle.of(field).stampedAtCreate) continue;
20
20
  if (role.isCollection) continue;
21
21
  if (Boundary.of(field).readOnly) continue;
22
22
  result[name] = field;
@@ -16,10 +16,7 @@ export class Bundle {
16
16
  const definitions: Record<string, SchemaDescriptor> = {};
17
17
  const claimedBy = new Map<string, string>();
18
18
  const entries = Array.isArray(schemas)
19
- ? schemas.map((schema) => {
20
- const card = Card.fromSchema(schema);
21
- return { name: card.descriptor.title ?? '', schema };
22
- })
19
+ ? schemas.map((schema) => ({ name: Card.titleOf(schema), schema }))
23
20
  : Object.entries(schemas).map(([name, schema]) => ({ name, schema }));
24
21
 
25
22
  for (const entry of entries) {