@fougere/schema 0.8.3-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.
- package/dist/Schema.d.ts +3 -6
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +9 -17
- package/dist/Schema.js.map +1 -1
- package/dist/SchemaDefinition.d.ts +24 -6
- package/dist/SchemaDefinition.d.ts.map +1 -1
- package/dist/SchemaDefinition.js +76 -44
- package/dist/SchemaDefinition.js.map +1 -1
- package/dist/SchemaView.d.ts +1 -1
- package/dist/SchemaView.d.ts.map +1 -1
- package/dist/axis/Axis.d.ts +1 -1
- package/dist/axis/Axis.d.ts.map +1 -1
- package/dist/axis/boundary/Boundaries.d.ts +5 -0
- package/dist/axis/boundary/Boundaries.d.ts.map +1 -1
- package/dist/axis/boundary/Boundaries.js.map +1 -1
- package/dist/axis/boundary/Boundary.d.ts +5 -4
- package/dist/axis/boundary/Boundary.d.ts.map +1 -1
- package/dist/axis/boundary/Boundary.js +4 -5
- package/dist/axis/boundary/Boundary.js.map +1 -1
- package/dist/axis/boundary/BoundaryAxis.js +2 -2
- package/dist/axis/boundary/BoundaryAxis.js.map +1 -1
- package/dist/axis/lifecycle/Lifecycle.d.ts +5 -3
- package/dist/axis/lifecycle/Lifecycle.d.ts.map +1 -1
- package/dist/axis/lifecycle/Lifecycle.js +5 -1
- package/dist/axis/lifecycle/Lifecycle.js.map +1 -1
- package/dist/axis/lifecycle/LifecycleAxis.js +6 -6
- package/dist/axis/lifecycle/LifecycleAxis.js.map +1 -1
- package/dist/axis/lifecycle/apply.d.ts +6 -6
- package/dist/axis/lifecycle/apply.d.ts.map +1 -1
- package/dist/axis/lifecycle/apply.js +10 -9
- package/dist/axis/lifecycle/apply.js.map +1 -1
- package/dist/axis/role/Role.d.ts +8 -5
- package/dist/axis/role/Role.d.ts.map +1 -1
- package/dist/axis/role/Role.js +7 -0
- package/dist/axis/role/Role.js.map +1 -1
- package/dist/axis/role/RoleAxis.js +9 -9
- package/dist/axis/role/RoleAxis.js.map +1 -1
- package/dist/axis/shape/Shape.d.ts +14 -0
- package/dist/axis/shape/Shape.d.ts.map +1 -1
- package/dist/axis/shape/Shape.js +43 -34
- package/dist/axis/shape/Shape.js.map +1 -1
- package/dist/entity/EntityAdapterSet.d.ts +2 -2
- package/dist/entity/EntityAdapterSet.d.ts.map +1 -1
- package/dist/entity/EntityAdapterSet.js +2 -2
- package/dist/entity/EntityAdapterSet.js.map +1 -1
- package/dist/entity.d.ts +2 -2
- package/dist/entity.js +2 -2
- package/dist/field/Field.d.ts.map +1 -1
- package/dist/field/Field.js +2 -1
- package/dist/field/Field.js.map +1 -1
- package/dist/field/FieldSet.d.ts +4 -7
- package/dist/field/FieldSet.d.ts.map +1 -1
- package/dist/field/FieldSet.js +4 -15
- package/dist/field/FieldSet.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/validation.d.ts +32 -0
- package/dist/lib/validation.d.ts.map +1 -0
- package/dist/lib/validation.js +6 -0
- package/dist/lib/validation.js.map +1 -0
- package/dist/projection/Cases.d.ts +1 -1
- package/dist/projection/Cases.d.ts.map +1 -1
- package/dist/projection/Cases.js +11 -7
- package/dist/projection/Cases.js.map +1 -1
- package/dist/projection/Visibility.js +1 -1
- package/dist/projection/Visibility.js.map +1 -1
- package/dist/projection/card/Bundle.d.ts.map +1 -1
- package/dist/projection/card/Bundle.js +1 -4
- package/dist/projection/card/Bundle.js.map +1 -1
- package/dist/projection/card/Card.d.ts +7 -1
- package/dist/projection/card/Card.d.ts.map +1 -1
- package/dist/projection/card/Card.js +25 -133
- package/dist/projection/card/Card.js.map +1 -1
- package/dist/projection/card/Descriptor.d.ts +2 -2
- package/dist/projection/card/Descriptor.d.ts.map +1 -1
- package/dist/projection/card/admission.d.ts +12 -11
- package/dist/projection/card/admission.d.ts.map +1 -1
- package/dist/projection/card/admission.js +13 -11
- package/dist/projection/card/admission.js.map +1 -1
- package/dist/projection/card/diff.d.ts +7 -1
- package/dist/projection/card/diff.d.ts.map +1 -1
- package/dist/projection/card/diff.js +111 -1
- package/dist/projection/card/diff.js.map +1 -1
- package/dist/validator/FieldDeclarationValidator.d.ts +1 -1
- package/dist/validator/FieldDeclarationValidator.d.ts.map +1 -1
- package/dist/validator/FieldDeclarationValidator.js +4 -4
- package/dist/validator/FieldDeclarationValidator.js.map +1 -1
- package/dist/validator/FieldValueValidator.d.ts +6 -1
- package/dist/validator/FieldValueValidator.d.ts.map +1 -1
- package/dist/validator/FieldValueValidator.js +36 -7
- package/dist/validator/FieldValueValidator.js.map +1 -1
- package/dist/validator/InputValidator.d.ts +1 -1
- package/dist/validator/InputValidator.d.ts.map +1 -1
- package/dist/validator/InputValidator.js +8 -18
- package/dist/validator/InputValidator.js.map +1 -1
- package/dist/vocabulary/preset/created.d.ts +0 -5
- package/dist/vocabulary/preset/created.d.ts.map +1 -1
- package/dist/vocabulary/preset/created.js +0 -5
- package/dist/vocabulary/preset/created.js.map +1 -1
- package/dist/vocabulary/preset/email.d.ts +0 -5
- package/dist/vocabulary/preset/email.d.ts.map +1 -1
- package/dist/vocabulary/preset/email.js +0 -5
- package/dist/vocabulary/preset/email.js.map +1 -1
- package/dist/vocabulary/preset/updated.d.ts +0 -5
- package/dist/vocabulary/preset/updated.d.ts.map +1 -1
- package/dist/vocabulary/preset/updated.js +0 -5
- package/dist/vocabulary/preset/updated.js.map +1 -1
- package/dist/vocabulary/preset/url.d.ts +0 -5
- package/dist/vocabulary/preset/url.d.ts.map +1 -1
- package/dist/vocabulary/preset/url.js +0 -5
- package/dist/vocabulary/preset/url.js.map +1 -1
- package/dist/vocabulary/primitive/bool.d.ts +0 -5
- package/dist/vocabulary/primitive/bool.d.ts.map +1 -1
- package/dist/vocabulary/primitive/bool.js +0 -5
- package/dist/vocabulary/primitive/bool.js.map +1 -1
- package/dist/vocabulary/primitive/date.d.ts +3 -3
- package/dist/vocabulary/primitive/date.js +3 -3
- package/dist/vocabulary/primitive/json.js +3 -3
- package/dist/vocabulary/primitive/list.d.ts +3 -3
- package/dist/vocabulary/primitive/list.js +3 -3
- package/dist/vocabulary/primitive/many.d.ts +3 -3
- package/dist/vocabulary/primitive/many.js +3 -3
- package/dist/vocabulary/primitive/number.d.ts +0 -5
- package/dist/vocabulary/primitive/number.d.ts.map +1 -1
- package/dist/vocabulary/primitive/number.js +0 -5
- package/dist/vocabulary/primitive/number.js.map +1 -1
- package/dist/vocabulary/primitive/oneOf.js +3 -3
- package/dist/vocabulary/primitive/primary.js +3 -3
- package/dist/vocabulary/primitive/ref.d.ts +0 -5
- package/dist/vocabulary/primitive/ref.d.ts.map +1 -1
- package/dist/vocabulary/primitive/ref.js +0 -5
- package/dist/vocabulary/primitive/ref.js.map +1 -1
- package/dist/vocabulary/primitive/text.d.ts +0 -5
- package/dist/vocabulary/primitive/text.d.ts.map +1 -1
- package/dist/vocabulary/primitive/text.js +0 -5
- package/dist/vocabulary/primitive/text.js.map +1 -1
- package/dist/vocabulary/rule/immutable.d.ts +0 -5
- package/dist/vocabulary/rule/immutable.d.ts.map +1 -1
- package/dist/vocabulary/rule/immutable.js +0 -5
- package/dist/vocabulary/rule/immutable.js.map +1 -1
- package/dist/vocabulary/rule/indexed.d.ts +0 -5
- package/dist/vocabulary/rule/indexed.d.ts.map +1 -1
- package/dist/vocabulary/rule/indexed.js +0 -5
- package/dist/vocabulary/rule/indexed.js.map +1 -1
- package/dist/vocabulary/rule/nullable.d.ts +3 -3
- package/dist/vocabulary/rule/nullable.js +3 -3
- package/dist/vocabulary/rule/optional.d.ts +3 -3
- package/dist/vocabulary/rule/optional.js +3 -3
- package/dist/vocabulary/rule/readOnly.d.ts +3 -3
- package/dist/vocabulary/rule/readOnly.js +4 -4
- package/dist/vocabulary/rule/readOnly.js.map +1 -1
- package/dist/vocabulary/rule/unique.d.ts +0 -5
- package/dist/vocabulary/rule/unique.d.ts.map +1 -1
- package/dist/vocabulary/rule/unique.js +0 -5
- package/dist/vocabulary/rule/unique.js.map +1 -1
- package/dist/vocabulary/rule/writeOnly.d.ts +2 -2
- package/dist/vocabulary/rule/writeOnly.js +3 -3
- package/dist/vocabulary/rule/writeOnly.js.map +1 -1
- package/dist/vocabulary/vocabulary.d.ts +2 -2
- package/dist/vocabulary/vocabulary.js +5 -5
- package/package.json +1 -1
- package/src/Schema.ts +14 -20
- package/src/SchemaDefinition.ts +74 -48
- package/src/SchemaView.ts +1 -1
- package/src/axis/Axis.ts +1 -1
- package/src/axis/boundary/Boundaries.ts +5 -0
- package/src/axis/boundary/Boundary.ts +7 -9
- package/src/axis/boundary/BoundaryAxis.ts +2 -2
- package/src/axis/lifecycle/Lifecycle.ts +9 -4
- package/src/axis/lifecycle/LifecycleAxis.ts +7 -7
- package/src/axis/lifecycle/apply.ts +10 -9
- package/src/axis/role/Role.ts +14 -5
- package/src/axis/role/RoleAxis.ts +10 -10
- package/src/axis/shape/Shape.ts +61 -37
- package/src/entity/EntityAdapterSet.ts +2 -2
- package/src/entity.ts +2 -2
- package/src/field/Field.ts +2 -1
- package/src/field/FieldSet.ts +6 -20
- package/src/index.ts +2 -2
- package/src/lib/validation.ts +25 -0
- package/src/projection/Cases.ts +13 -8
- package/src/projection/Visibility.ts +1 -1
- package/src/projection/card/Bundle.ts +1 -4
- package/src/projection/card/Card.ts +26 -159
- package/src/projection/card/Descriptor.ts +2 -10
- package/src/projection/card/admission.ts +14 -12
- package/src/projection/card/diff.ts +142 -1
- package/src/validator/FieldDeclarationValidator.ts +5 -5
- package/src/validator/FieldValueValidator.ts +45 -12
- package/src/validator/InputValidator.ts +8 -18
- package/src/vocabulary/preset/created.ts +0 -5
- package/src/vocabulary/preset/email.ts +0 -5
- package/src/vocabulary/preset/updated.ts +0 -5
- package/src/vocabulary/preset/url.ts +0 -5
- package/src/vocabulary/primitive/bool.ts +0 -5
- package/src/vocabulary/primitive/date.ts +3 -3
- package/src/vocabulary/primitive/json.ts +3 -3
- package/src/vocabulary/primitive/list.ts +3 -3
- package/src/vocabulary/primitive/many.ts +3 -3
- package/src/vocabulary/primitive/number.ts +0 -5
- package/src/vocabulary/primitive/oneOf.ts +3 -3
- package/src/vocabulary/primitive/primary.ts +3 -3
- package/src/vocabulary/primitive/ref.ts +0 -5
- package/src/vocabulary/primitive/text.ts +0 -5
- package/src/vocabulary/rule/immutable.ts +0 -5
- package/src/vocabulary/rule/indexed.ts +0 -5
- package/src/vocabulary/rule/nullable.ts +3 -3
- package/src/vocabulary/rule/optional.ts +3 -3
- package/src/vocabulary/rule/readOnly.ts +4 -4
- package/src/vocabulary/rule/unique.ts +0 -5
- package/src/vocabulary/rule/writeOnly.ts +3 -3
- package/src/vocabulary/vocabulary.ts +5 -5
- package/dist/validation.d.ts +0 -17
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -2
- package/dist/validation.js.map +0 -1
- 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:
|
|
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
|
-
*
|
|
77
|
-
* FR :
|
|
78
|
-
* `{ to: User, kind: 'one' }` →
|
|
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
|
|
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
|
|
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
|
|
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
|
|
96
|
+
path: ['role', 'relation', 'onDelete'],
|
|
97
97
|
message: `Expected 'cascade', 'restrict' or 'set null' — got ${shown(relation.onDelete)}`,
|
|
98
98
|
});
|
|
99
99
|
}
|
package/src/axis/shape/Shape.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
7
|
-
* FR :
|
|
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>(
|
package/src/field/Field.ts
CHANGED
|
@@ -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
|
|
package/src/field/FieldSet.ts
CHANGED
|
@@ -10,20 +10,19 @@ export class FieldSet<TFields extends Fields = Fields> {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
* FR :
|
|
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;
|
|
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:
|
|
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('.');
|
package/src/projection/Cases.ts
CHANGED
|
@@ -25,8 +25,8 @@ export interface ValidationCase {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
* FR : pour que chaque champ
|
|
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
|
-
*
|
|
40
|
-
* FR :
|
|
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
|
-
*
|
|
107
|
-
*
|
|
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).
|
|
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) {
|