@fougere/schema 0.12.0-alpha.0 → 0.12.1-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/SchemaError.js +1 -1
- package/dist/SchemaError.js.map +1 -1
- package/dist/axis/Axes.d.ts +16 -0
- package/dist/axis/Axes.d.ts.map +1 -0
- package/dist/axis/Axes.js +27 -0
- package/dist/axis/Axes.js.map +1 -0
- package/dist/axis/Axis.d.ts +5 -9
- package/dist/axis/Axis.d.ts.map +1 -1
- package/dist/axis/Axis.js +1 -10
- package/dist/axis/Axis.js.map +1 -1
- package/dist/axis/boundary/Boundary.d.ts +29 -3
- package/dist/axis/boundary/Boundary.d.ts.map +1 -1
- package/dist/axis/boundary/Boundary.js +12 -9
- package/dist/axis/boundary/Boundary.js.map +1 -1
- package/dist/axis/boundary/Decoder.d.ts +8 -2
- package/dist/axis/boundary/Decoder.d.ts.map +1 -1
- package/dist/axis/lifecycle/Lifecycle.d.ts +16 -4
- package/dist/axis/lifecycle/Lifecycle.d.ts.map +1 -1
- package/dist/axis/lifecycle/Lifecycle.js +18 -7
- package/dist/axis/lifecycle/Lifecycle.js.map +1 -1
- package/dist/axis/lifecycle/apply.d.ts.map +1 -1
- package/dist/axis/lifecycle/apply.js +2 -3
- package/dist/axis/lifecycle/apply.js.map +1 -1
- package/dist/axis/role/Role.d.ts +18 -6
- package/dist/axis/role/Role.d.ts.map +1 -1
- package/dist/axis/role/Role.js +30 -16
- package/dist/axis/role/Role.js.map +1 -1
- package/dist/axis/shape/{Formats.d.ts → StringFormats.d.ts} +3 -3
- package/dist/axis/shape/StringFormats.d.ts.map +1 -0
- package/dist/axis/shape/{Formats.js → StringFormats.js} +3 -3
- package/dist/axis/shape/StringFormats.js.map +1 -0
- package/dist/field/Field.d.ts +11 -5
- package/dist/field/Field.d.ts.map +1 -1
- package/dist/field/Field.js +5 -3
- package/dist/field/Field.js.map +1 -1
- package/dist/field/FieldFormat.d.ts +9 -0
- package/dist/field/FieldFormat.d.ts.map +1 -0
- package/dist/field/FieldFormat.js +12 -0
- package/dist/field/FieldFormat.js.map +1 -0
- package/dist/field/FougereFieldAxes.d.ts +3 -0
- package/dist/field/FougereFieldAxes.d.ts.map +1 -0
- package/dist/field/FougereFieldAxes.js +2 -0
- package/dist/field/FougereFieldAxes.js.map +1 -0
- package/dist/field/Meta.d.ts +5 -3
- package/dist/field/Meta.d.ts.map +1 -1
- package/dist/field/Meta.js +2 -1
- package/dist/field/Meta.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/Format.d.ts +56 -0
- package/dist/lib/Format.d.ts.map +1 -0
- package/dist/lib/Format.js +95 -0
- package/dist/lib/Format.js.map +1 -0
- package/dist/lib/JsonSchema.d.ts +2 -0
- package/dist/lib/JsonSchema.d.ts.map +1 -0
- package/dist/lib/JsonSchema.js +2 -0
- package/dist/lib/JsonSchema.js.map +1 -0
- package/dist/lib/Registry.d.ts +4 -2
- package/dist/lib/Registry.d.ts.map +1 -1
- package/dist/lib/Registry.js +11 -5
- package/dist/lib/Registry.js.map +1 -1
- package/dist/lib/ValidationResult.d.ts +2 -0
- package/dist/lib/ValidationResult.d.ts.map +1 -1
- package/dist/lib/ValidationResult.js +6 -0
- package/dist/lib/ValidationResult.js.map +1 -1
- package/dist/projection/Visibility.d.ts +11 -0
- package/dist/projection/Visibility.d.ts.map +1 -1
- package/dist/projection/Visibility.js +22 -0
- package/dist/projection/Visibility.js.map +1 -1
- package/dist/projection/card/Card.d.ts.map +1 -1
- package/dist/projection/card/Card.js +12 -9
- package/dist/projection/card/Card.js.map +1 -1
- package/dist/projection/card/CardForm.d.ts +7 -0
- package/dist/projection/card/CardForm.d.ts.map +1 -0
- package/dist/projection/card/CardForm.js +2 -0
- package/dist/projection/card/CardForm.js.map +1 -0
- package/dist/projection/card/CardForms.d.ts +10 -0
- package/dist/projection/card/CardForms.d.ts.map +1 -0
- package/dist/projection/card/CardForms.js +12 -0
- package/dist/projection/card/CardForms.js.map +1 -0
- package/dist/projection/card/Change.d.ts +2 -2
- package/dist/projection/card/Change.d.ts.map +1 -1
- package/dist/projection/card/Diff.js +2 -2
- package/dist/projection/card/Diff.js.map +1 -1
- package/dist/projection/card/FieldExtension.d.ts +4 -2
- package/dist/projection/card/FieldExtension.d.ts.map +1 -1
- package/dist/projection/card/RoleDescriptor.d.ts +9 -2
- package/dist/projection/card/RoleDescriptor.d.ts.map +1 -1
- package/dist/projection/card/RoleDescriptor.js +40 -1
- package/dist/projection/card/RoleDescriptor.js.map +1 -1
- package/dist/projection/card/admission.d.ts +0 -14
- package/dist/projection/card/admission.d.ts.map +1 -1
- package/dist/projection/card/admission.js +0 -20
- package/dist/projection/card/admission.js.map +1 -1
- package/dist/validator/FieldDeclarationValidator.d.ts.map +1 -1
- package/dist/validator/FieldDeclarationValidator.js +5 -17
- package/dist/validator/FieldDeclarationValidator.js.map +1 -1
- package/dist/validator/FieldValueValidator.d.ts.map +1 -1
- package/dist/validator/FieldValueValidator.js +9 -27
- package/dist/validator/FieldValueValidator.js.map +1 -1
- package/dist/validator/JsonSchemaValidator.d.ts +23 -0
- package/dist/validator/JsonSchemaValidator.d.ts.map +1 -0
- package/dist/validator/JsonSchemaValidator.js +113 -0
- package/dist/validator/JsonSchemaValidator.js.map +1 -0
- package/dist/vocabulary/vocabulary.js +7 -5
- package/dist/vocabulary/vocabulary.js.map +1 -1
- package/package.json +1 -1
- package/src/SchemaError.ts +1 -1
- package/src/axis/Axes.ts +34 -0
- package/src/axis/Axis.ts +5 -20
- package/src/axis/boundary/Boundary.ts +23 -11
- package/src/axis/boundary/Decoder.ts +1 -1
- package/src/axis/lifecycle/Lifecycle.ts +28 -11
- package/src/axis/lifecycle/apply.ts +11 -5
- package/src/axis/role/Role.ts +35 -19
- package/src/axis/shape/{Formats.ts → StringFormats.ts} +2 -2
- package/src/field/Field.ts +18 -8
- package/src/field/FieldFormat.ts +18 -0
- package/src/field/FougereFieldAxes.ts +2 -0
- package/src/field/Meta.ts +5 -3
- package/src/index.ts +6 -3
- package/src/lib/Format.ts +139 -0
- package/src/lib/JsonSchema.ts +1 -0
- package/src/lib/Registry.ts +14 -6
- package/src/lib/ValidationResult.ts +8 -0
- package/src/projection/Visibility.ts +22 -0
- package/src/projection/card/Card.ts +12 -9
- package/src/projection/card/CardForm.ts +8 -0
- package/src/projection/card/CardForms.ts +13 -0
- package/src/projection/card/Change.ts +2 -2
- package/src/projection/card/Diff.ts +2 -2
- package/src/projection/card/FieldExtension.ts +5 -2
- package/src/projection/card/RoleDescriptor.ts +48 -2
- package/src/projection/card/admission.ts +0 -33
- package/src/validator/FieldDeclarationValidator.ts +5 -17
- package/src/validator/FieldValueValidator.ts +10 -35
- package/src/validator/JsonSchemaValidator.ts +142 -0
- package/src/vocabulary/vocabulary.ts +7 -5
- package/dist/axis/boundary/BoundaryAxis.d.ts +0 -4
- package/dist/axis/boundary/BoundaryAxis.d.ts.map +0 -1
- package/dist/axis/boundary/BoundaryAxis.js +0 -30
- package/dist/axis/boundary/BoundaryAxis.js.map +0 -1
- package/dist/axis/boundary/BoundaryRef.d.ts +0 -3
- package/dist/axis/boundary/BoundaryRef.d.ts.map +0 -1
- package/dist/axis/boundary/BoundaryRef.js +0 -2
- package/dist/axis/boundary/BoundaryRef.js.map +0 -1
- package/dist/axis/boundary/BoundaryRules.d.ts +0 -9
- package/dist/axis/boundary/BoundaryRules.d.ts.map +0 -1
- package/dist/axis/boundary/BoundaryRules.js +0 -2
- package/dist/axis/boundary/BoundaryRules.js.map +0 -1
- package/dist/axis/lifecycle/Clock.d.ts +0 -10
- package/dist/axis/lifecycle/Clock.d.ts.map +0 -1
- package/dist/axis/lifecycle/Clock.js +0 -19
- package/dist/axis/lifecycle/Clock.js.map +0 -1
- package/dist/axis/lifecycle/LifecycleAxis.d.ts +0 -4
- package/dist/axis/lifecycle/LifecycleAxis.d.ts.map +0 -1
- package/dist/axis/lifecycle/LifecycleAxis.js +0 -52
- package/dist/axis/lifecycle/LifecycleAxis.js.map +0 -1
- package/dist/axis/lifecycle/LifecycleRules.d.ts +0 -12
- package/dist/axis/lifecycle/LifecycleRules.d.ts.map +0 -1
- package/dist/axis/lifecycle/LifecycleRules.js +0 -3
- package/dist/axis/lifecycle/LifecycleRules.js.map +0 -1
- package/dist/axis/role/RoleAxis.d.ts +0 -5
- package/dist/axis/role/RoleAxis.d.ts.map +0 -1
- package/dist/axis/role/RoleAxis.js +0 -93
- package/dist/axis/role/RoleAxis.js.map +0 -1
- package/dist/axis/role/RoleRules.d.ts +0 -8
- package/dist/axis/role/RoleRules.d.ts.map +0 -1
- package/dist/axis/role/RoleRules.js +0 -2
- package/dist/axis/role/RoleRules.js.map +0 -1
- package/dist/axis/shape/Formats.d.ts.map +0 -1
- package/dist/axis/shape/Formats.js.map +0 -1
- package/dist/projection/card/RelationDescriptor.d.ts +0 -5
- package/dist/projection/card/RelationDescriptor.d.ts.map +0 -1
- package/dist/projection/card/RelationDescriptor.js +0 -2
- package/dist/projection/card/RelationDescriptor.js.map +0 -1
- package/dist/validator/AdapterFieldValidator.d.ts +0 -14
- package/dist/validator/AdapterFieldValidator.d.ts.map +0 -1
- package/dist/validator/AdapterFieldValidator.js +0 -37
- package/dist/validator/AdapterFieldValidator.js.map +0 -1
- package/src/axis/boundary/BoundaryAxis.ts +0 -32
- package/src/axis/boundary/BoundaryRef.ts +0 -3
- package/src/axis/boundary/BoundaryRules.ts +0 -4
- package/src/axis/lifecycle/Clock.ts +0 -22
- package/src/axis/lifecycle/LifecycleAxis.ts +0 -55
- package/src/axis/lifecycle/LifecycleRules.ts +0 -9
- package/src/axis/role/RoleAxis.ts +0 -104
- package/src/axis/role/RoleRules.ts +0 -8
- package/src/projection/card/RelationDescriptor.ts +0 -3
- package/src/validator/AdapterFieldValidator.ts +0 -41
package/package.json
CHANGED
package/src/SchemaError.ts
CHANGED
package/src/axis/Axes.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Registry } from '../lib/Registry.js';
|
|
2
|
+
import type { JsonSchema } from '../lib/JsonSchema.js';
|
|
3
|
+
import { fieldFormat } from '../field/FieldFormat.js';
|
|
4
|
+
import type { Axis } from './Axis.js';
|
|
5
|
+
import { Boundary } from './boundary/Boundary.js';
|
|
6
|
+
import { Lifecycle } from './lifecycle/Lifecycle.js';
|
|
7
|
+
import { Role } from './role/Role.js';
|
|
8
|
+
|
|
9
|
+
class AxisRegistry extends Registry<Axis> {
|
|
10
|
+
private composed?: JsonSchema;
|
|
11
|
+
|
|
12
|
+
register(slot: string, axis: Axis): Axis {
|
|
13
|
+
this.composed = undefined;
|
|
14
|
+
|
|
15
|
+
return super.register(slot, axis);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The document a field declaration is judged against, rebuilt the next time one arrives. */
|
|
19
|
+
get fieldFormat(): JsonSchema {
|
|
20
|
+
this.composed ??= fieldFormat(this.entries);
|
|
21
|
+
|
|
22
|
+
return this.composed;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The axes this process reads, and the door an axis declared elsewhere comes through.
|
|
28
|
+
* `Axes.register('tenancy', Tenancy)` — from a `vocabulary/` file, read before `entities/`
|
|
29
|
+
*/
|
|
30
|
+
export const Axes = new AxisRegistry('axis', 'call Axes.register(slot, axis)', [
|
|
31
|
+
['role', Role],
|
|
32
|
+
['lifecycle', Lifecycle],
|
|
33
|
+
['boundary', Boundary],
|
|
34
|
+
]);
|
package/src/axis/Axis.ts
CHANGED
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { lifecycleAxis } from './lifecycle/LifecycleAxis.js';
|
|
3
|
-
import { boundaryAxis } from './boundary/BoundaryAxis.js';
|
|
1
|
+
import type { Format } from '../lib/Format.js';
|
|
4
2
|
import type { ValidationError } from '../lib/ValidationError.js';
|
|
5
|
-
import type { Resolver } from './Resolver.js';
|
|
6
3
|
|
|
7
|
-
export interface Axis
|
|
8
|
-
readonly
|
|
4
|
+
export interface Axis {
|
|
5
|
+
readonly format: Format;
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
describe(value: Declared, key: string): Wire | undefined;
|
|
13
|
-
|
|
14
|
-
reconstruct(wire: Wire, resolve?: Resolver): Declared;
|
|
7
|
+
/** What a format cannot state: `role.relation.to` is a function, and JSON holds none. */
|
|
8
|
+
refusals?(value: unknown): ValidationError[];
|
|
15
9
|
}
|
|
16
|
-
|
|
17
|
-
export const EXTENSION_AXES: readonly Axis[] = [
|
|
18
|
-
roleAxis,
|
|
19
|
-
lifecycleAxis,
|
|
20
|
-
boundaryAxis,
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
/** The slots they occupy: the keys of `x-fougere` on a field descriptor. */
|
|
24
|
-
export const EXTENSION_SLOTS = EXTENSION_AXES.map((axis) => axis.slot);
|
|
@@ -4,25 +4,37 @@ import { Boundaries } from './Decoder.js';
|
|
|
4
4
|
import type { Field } from '../../field/Field.js';
|
|
5
5
|
import { type Shape } from '../shape/Shape.js';
|
|
6
6
|
import { Shapes } from '../shape/Shape.js';
|
|
7
|
-
import type { BoundaryRules } from './BoundaryRules.js';
|
|
8
7
|
import { SchemaError } from '../../SchemaError.js';
|
|
8
|
+
import { Format, type Accepted } from '../../lib/Format.js';
|
|
9
|
+
|
|
10
|
+
const closedOr = <Verb extends 'decode' | 'encode'>(verb: Verb) =>
|
|
11
|
+
Format.either(Format.tokens(['closed']), Format.of().key(verb, Format.text).needs(verb).closed());
|
|
12
|
+
|
|
13
|
+
const RULES = Format.of().key('in', closedOr('decode')).key('out', closedOr('encode')).closed();
|
|
14
|
+
|
|
15
|
+
const BOUNDARY = Format.named(
|
|
16
|
+
'axis/boundary',
|
|
17
|
+
Format.either(Format.text.as<'isoDate' | (string & {})>(), RULES),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export type BoundaryRules = Accepted<typeof RULES>;
|
|
21
|
+
|
|
22
|
+
export type BoundaryRef = Accepted<typeof BOUNDARY>;
|
|
9
23
|
|
|
10
24
|
const identityDecoder: Decoder = (value) => ({ value });
|
|
11
25
|
|
|
12
26
|
const identityEncoder: Encoder = (value) => value;
|
|
13
27
|
|
|
14
28
|
export class Boundary {
|
|
15
|
-
|
|
16
|
-
|
|
29
|
+
static readonly format = BOUNDARY;
|
|
30
|
+
|
|
17
31
|
readonly decode: Decoder;
|
|
18
32
|
readonly encode: Encoder;
|
|
19
33
|
|
|
20
34
|
private constructor(
|
|
21
|
-
rules: BoundaryRules = {},
|
|
35
|
+
private readonly rules: BoundaryRules = {},
|
|
22
36
|
codecs?: { decode: Decoder; encode: Encoder },
|
|
23
37
|
) {
|
|
24
|
-
this.in = rules.in;
|
|
25
|
-
this.out = rules.out;
|
|
26
38
|
this.decode = codecs?.decode ?? identityDecoder;
|
|
27
39
|
this.encode = codecs?.encode ?? identityEncoder;
|
|
28
40
|
}
|
|
@@ -42,8 +54,8 @@ export class Boundary {
|
|
|
42
54
|
const declared = Boundary.declared(field);
|
|
43
55
|
const derived = Boundary.forShape(field.shape);
|
|
44
56
|
const rules: BoundaryRules = {
|
|
45
|
-
in: declared.in ?? derived.in,
|
|
46
|
-
out: declared.out ?? derived.out,
|
|
57
|
+
in: declared.rules.in ?? derived.rules.in,
|
|
58
|
+
out: declared.rules.out ?? derived.rules.out,
|
|
47
59
|
};
|
|
48
60
|
return new Boundary(rules, {
|
|
49
61
|
decode:
|
|
@@ -65,14 +77,14 @@ export class Boundary {
|
|
|
65
77
|
|
|
66
78
|
/** The dual of `declared`: what `readOnly()` writes back on the field, not a judge. */
|
|
67
79
|
declaring(overrides: BoundaryRules): BoundaryRules {
|
|
68
|
-
return { in: overrides.in ?? this.in, out: overrides.out ?? this.out };
|
|
80
|
+
return { in: overrides.in ?? this.rules.in, out: overrides.out ?? this.rules.out };
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
get readOnly(): boolean {
|
|
72
|
-
return this.in === 'closed';
|
|
84
|
+
return this.rules.in === 'closed';
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
get writeOnly(): boolean {
|
|
76
|
-
return this.out === 'closed';
|
|
88
|
+
return this.rules.out === 'closed';
|
|
77
89
|
}
|
|
78
90
|
}
|
|
@@ -1,19 +1,40 @@
|
|
|
1
|
+
import { Format, type Accepted } from '../../lib/Format.js';
|
|
1
2
|
import { Generators } from './Generators.js';
|
|
2
|
-
import type {
|
|
3
|
+
import type { GeneratorRef } from './Generators.js';
|
|
3
4
|
|
|
5
|
+
export type LifecycleRules = Accepted<typeof Lifecycle.format>;
|
|
4
6
|
export class Lifecycle {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
static readonly CREATE = ['now', 'optional'] as const;
|
|
8
|
+
static readonly UPDATE = ['now', 'forbidden'] as const;
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
static readonly format = Format.of('axis/lifecycle')
|
|
11
|
+
.key(
|
|
12
|
+
'create',
|
|
13
|
+
Format.either(
|
|
14
|
+
Format.tokens(Lifecycle.CREATE),
|
|
15
|
+
Format.exactlyOne({
|
|
16
|
+
value: Format.anything,
|
|
17
|
+
generate: Format.text.as<GeneratorRef>(),
|
|
18
|
+
}),
|
|
19
|
+
),
|
|
20
|
+
)
|
|
21
|
+
.key('update', Format.tokens(Lifecycle.UPDATE))
|
|
22
|
+
.closed();
|
|
23
|
+
|
|
24
|
+
private constructor(private readonly rules: LifecycleRules = {}) {}
|
|
12
25
|
|
|
13
26
|
static of(field: { lifecycle?: LifecycleRules }): Lifecycle {
|
|
14
27
|
return new Lifecycle(field.lifecycle);
|
|
15
28
|
}
|
|
16
29
|
|
|
30
|
+
private get create() {
|
|
31
|
+
return this.rules.create;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private get update() {
|
|
35
|
+
return this.rules.update;
|
|
36
|
+
}
|
|
37
|
+
|
|
17
38
|
bornWith(instant: number): { value: unknown } | undefined {
|
|
18
39
|
const rule = this.create;
|
|
19
40
|
|
|
@@ -34,10 +55,6 @@ export class Lifecycle {
|
|
|
34
55
|
return this.create === 'now';
|
|
35
56
|
}
|
|
36
57
|
|
|
37
|
-
get stampedOnce(): boolean {
|
|
38
|
-
return this.stampedAtCreate && !this.stampedOnUpdate;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
58
|
get immutable(): boolean {
|
|
42
59
|
return this.update === 'forbidden';
|
|
43
60
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Lifecycle } from './Lifecycle.js';
|
|
2
|
-
import { Clock } from './Clock.js';
|
|
3
2
|
import { type Field } from '../../field/Field.js';
|
|
4
3
|
import { type Fields } from '../../field/Fields.js';
|
|
5
4
|
|
|
@@ -8,12 +7,16 @@ import { type Fields } from '../../field/Fields.js';
|
|
|
8
7
|
* FR : remplit ce que la déclaration laisse au storage : aucun handler n'estampe lui-même.
|
|
9
8
|
* `applyCreate({ id: primary(), createdAt: created() }, { title: 'a' })` → `id`, `createdAt`
|
|
10
9
|
*/
|
|
11
|
-
export function applyCreate(
|
|
10
|
+
export function applyCreate(
|
|
11
|
+
fields: Fields,
|
|
12
|
+
input: Record<string, unknown>,
|
|
13
|
+
): Record<string, unknown> {
|
|
12
14
|
const values: Record<string, unknown> = { ...input };
|
|
13
|
-
const instant =
|
|
15
|
+
const instant = Date.now();
|
|
14
16
|
|
|
15
17
|
for (const [name, field] of Object.entries(fields) as [string, Field][]) {
|
|
16
18
|
if (name in values) continue;
|
|
19
|
+
|
|
17
20
|
const born = Lifecycle.of(field).bornWith(instant);
|
|
18
21
|
|
|
19
22
|
if (born) values[name] = born.value;
|
|
@@ -28,9 +31,12 @@ export function applyCreate(fields: Fields, input: Record<string, unknown>): Rec
|
|
|
28
31
|
* sont touchés.
|
|
29
32
|
* `applyUpdate(fields, { title: 'b' })` → `updatedAt` added, nothing else
|
|
30
33
|
*/
|
|
31
|
-
export function applyUpdate(
|
|
34
|
+
export function applyUpdate(
|
|
35
|
+
fields: Fields,
|
|
36
|
+
patch: Record<string, unknown>,
|
|
37
|
+
): Record<string, unknown> {
|
|
32
38
|
const values: Record<string, unknown> = { ...patch };
|
|
33
|
-
const instant =
|
|
39
|
+
const instant = Date.now();
|
|
34
40
|
|
|
35
41
|
for (const [name, field] of Object.entries(fields) as [string, Field][]) {
|
|
36
42
|
if (Lifecycle.of(field).stampedOnUpdate && !(name in values))
|
package/src/axis/role/Role.ts
CHANGED
|
@@ -1,56 +1,72 @@
|
|
|
1
|
-
import type { EntityConstructor } from './EntityConstructor.js';
|
|
2
1
|
import type { Relation } from './Relation.js';
|
|
3
|
-
import type {
|
|
2
|
+
import type { EntityConstructor } from './EntityConstructor.js';
|
|
3
|
+
import { isObject } from '../../lib/utils.js';
|
|
4
|
+
import { Format, type Accepted } from '../../lib/Format.js';
|
|
5
|
+
import { ON_DELETE, RELATION_KINDS } from './Relation.js';
|
|
4
6
|
|
|
5
7
|
export class Role {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
static readonly format = Format.of('axis/role')
|
|
9
|
+
.key('primary', Format.flag)
|
|
10
|
+
.key('index', Format.flag)
|
|
11
|
+
.key('unique', Format.flag)
|
|
12
|
+
.key(
|
|
13
|
+
'relation',
|
|
14
|
+
Format.of()
|
|
15
|
+
.key('to', Format.anything.as<() => EntityConstructor>())
|
|
16
|
+
.key('kind', Format.tokens(RELATION_KINDS))
|
|
17
|
+
.key('onDelete', Format.tokens(ON_DELETE))
|
|
18
|
+
.needs('to', 'kind')
|
|
19
|
+
.closed(),
|
|
20
|
+
)
|
|
21
|
+
.closed();
|
|
22
|
+
|
|
23
|
+
static refusals(value: unknown) {
|
|
24
|
+
const relation = isObject(value) ? value.relation : undefined;
|
|
25
|
+
if (!isObject(relation) || !('to' in relation) || typeof relation.to === 'function') return [];
|
|
10
26
|
|
|
11
|
-
|
|
12
|
-
this.primary = rules.primary;
|
|
13
|
-
this.index = rules.index;
|
|
14
|
-
this.unique = rules.unique;
|
|
15
|
-
this.relation = rules.relation;
|
|
27
|
+
return [{ path: ['role', 'relation', 'to'], message: 'Expected a function returning the target entity, such as () => Post' }];
|
|
16
28
|
}
|
|
17
29
|
|
|
30
|
+
private constructor(private readonly rules: RoleRules = {}) {}
|
|
31
|
+
|
|
18
32
|
static of(field: { role?: RoleRules }): Role {
|
|
19
33
|
return new Role(field.role);
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
get isPrimary(): boolean {
|
|
23
|
-
return this.primary === true;
|
|
37
|
+
return this.rules.primary === true;
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
get isIndexed(): boolean {
|
|
27
|
-
return this.index === true;
|
|
41
|
+
return this.rules.index === true;
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
/** A field unique on its own. A group spanning several belongs to the schema. */
|
|
31
45
|
get isUnique(): boolean {
|
|
32
|
-
return this.unique === true;
|
|
46
|
+
return this.rules.unique === true;
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
get isCollection(): boolean {
|
|
36
|
-
return this.relation?.kind === 'many';
|
|
50
|
+
return this.rules.relation?.kind === 'many';
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
get isReference(): boolean {
|
|
40
|
-
return this.relation?.kind === 'one';
|
|
54
|
+
return this.rules.relation?.kind === 'one';
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
/** Either kind — what a CLI flag and a GraphQL selection both leave out. */
|
|
44
58
|
get isRelation(): boolean {
|
|
45
|
-
return this.relation !== undefined;
|
|
59
|
+
return this.rules.relation !== undefined;
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
/** Calls `() => Post` so no caller has to. */
|
|
49
63
|
get target(): EntityConstructor | undefined {
|
|
50
|
-
return this.relation?.to();
|
|
64
|
+
return this.rules.relation?.to();
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
get onDelete(): Relation['onDelete'] {
|
|
54
|
-
return this.relation?.onDelete;
|
|
68
|
+
return this.rules.relation?.onDelete;
|
|
55
69
|
}
|
|
56
70
|
}
|
|
71
|
+
|
|
72
|
+
export type RoleRules = Accepted<typeof Role.format>;
|
|
@@ -5,6 +5,6 @@ import type { FormatPredicate } from './FormatPredicate.js';
|
|
|
5
5
|
* String formats the JSON Schema engine does not know. `find` answers `undefined` for
|
|
6
6
|
* `email` or `uuid`, which it validates on its own.
|
|
7
7
|
* FR : les formats de chaîne que le moteur ignore ; `find` rend `undefined` pour les siens.
|
|
8
|
-
* `
|
|
8
|
+
* `StringFormats.register('siret', (v) => /^\d{14}$/.test(v))`
|
|
9
9
|
*/
|
|
10
|
-
export const
|
|
10
|
+
export const StringFormats = new Registry<FormatPredicate>('format');
|
package/src/field/Field.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import type { Shape } from '../axis/shape/Shape.js';
|
|
2
|
-
import type { RoleRules } from '../axis/role/
|
|
3
|
-
import type { LifecycleRules } from '../axis/lifecycle/
|
|
4
|
-
import type { BoundaryRef } from '../axis/boundary/
|
|
2
|
+
import type { RoleRules } from '../axis/role/Role.js';
|
|
3
|
+
import type { LifecycleRules } from '../axis/lifecycle/Lifecycle.js';
|
|
4
|
+
import type { BoundaryRef } from '../axis/boundary/Boundary.js';
|
|
5
5
|
import type { Meta } from './Meta.js';
|
|
6
|
-
import
|
|
6
|
+
import { Axes } from '../axis/Axes.js';
|
|
7
|
+
import type { FougereFieldAxes } from './FougereFieldAxes.js';
|
|
7
8
|
import { FieldDeclarationValidator } from '../validator/FieldDeclarationValidator.js';
|
|
8
9
|
import { FieldValueValidator } from '../validator/FieldValueValidator.js';
|
|
9
10
|
import { dotted } from '../lib/ValidationResult.js';
|
|
10
11
|
import { SchemaError } from '../SchemaError.js';
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
interface FieldDeclaration extends FougereFieldAxes {
|
|
14
|
+
shape: Shape;
|
|
15
|
+
role?: RoleRules;
|
|
16
|
+
lifecycle?: LifecycleRules;
|
|
17
|
+
boundary?: BoundaryRef;
|
|
18
|
+
meta?: Meta;
|
|
19
|
+
}
|
|
13
20
|
|
|
14
21
|
export class Field<T = unknown> {
|
|
15
22
|
readonly shape: Shape;
|
|
@@ -31,11 +38,14 @@ export class Field<T = unknown> {
|
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
this.shape = init.shape;
|
|
34
|
-
this.role = init.role;
|
|
35
|
-
this.lifecycle = init.lifecycle;
|
|
36
|
-
this.boundary = init.boundary;
|
|
37
41
|
this.meta = init.meta;
|
|
38
42
|
|
|
43
|
+
const stated = init as unknown as Record<string, unknown>;
|
|
44
|
+
|
|
45
|
+
for (const slot of Axes.names) {
|
|
46
|
+
(this as unknown as Record<string, unknown>)[slot] = stated[slot];
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
const create = this.lifecycle?.create;
|
|
40
50
|
if (typeof create === 'object' && create !== null && 'value' in create) {
|
|
41
51
|
const checked = FieldValueValidator.of(this).validate(create.value);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Axis } from '../axis/Axis.js';
|
|
2
|
+
import { Format } from '../lib/Format.js';
|
|
3
|
+
import type { JsonSchema } from '../lib/JsonSchema.js';
|
|
4
|
+
import { META_FORMAT } from './Meta.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a field may state, composed from the keys that state a format: each is carried whole and
|
|
8
|
+
* cited by its `$id`, so the document travels alone and a key keeps its own identity. `shape` is
|
|
9
|
+
* JSON Schema itself, and `Shapes.is` is what reads it.
|
|
10
|
+
*/
|
|
11
|
+
export function fieldFormat(axes: readonly [string, Axis][]): JsonSchema {
|
|
12
|
+
const document = axes.reduce(
|
|
13
|
+
(held, [slot, axis]) => held.cites(slot, axis.format),
|
|
14
|
+
Format.of('field').key('shape', Format.anything),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
return document.cites('meta', META_FORMAT).closed().schema as JsonSchema;
|
|
18
|
+
}
|
package/src/field/Meta.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -37,11 +37,14 @@ export type { SetDiff } from './projection/card/SetDiff.js';
|
|
|
37
37
|
export type { SetDiffOptions } from './projection/card/SetDiffOptions.js';
|
|
38
38
|
|
|
39
39
|
export * from './lib/index.js';
|
|
40
|
-
export {
|
|
40
|
+
export { StringFormats } from './axis/shape/StringFormats.js';
|
|
41
41
|
export { Generators } from './axis/lifecycle/Generators.js';
|
|
42
42
|
export { Boundaries } from './axis/boundary/Decoder.js';
|
|
43
|
-
export { Clock } from './axis/lifecycle/Clock.js';
|
|
44
43
|
export { type FougereEntityAdapters } from './entity/FougereEntityAdapters.js';
|
|
45
44
|
export { InputValidator } from './validator/InputValidator.js';
|
|
46
45
|
export { FieldValueValidator } from './validator/FieldValueValidator.js';
|
|
47
|
-
export {
|
|
46
|
+
export { JsonSchemaValidator } from './validator/JsonSchemaValidator.js';
|
|
47
|
+
export type { JsonSchema } from './lib/JsonSchema.js';
|
|
48
|
+
export { Axes } from './axis/Axes.js';
|
|
49
|
+
export { Format } from './lib/Format.js';
|
|
50
|
+
export type { Axis } from './axis/Axis.js';
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { JsonSchema } from './JsonSchema.js';
|
|
2
|
+
import { SchemaError } from '../SchemaError.js';
|
|
3
|
+
|
|
4
|
+
type Stated = JsonSchema | boolean;
|
|
5
|
+
|
|
6
|
+
const HERE = 'https://fougere.dev/schema/';
|
|
7
|
+
|
|
8
|
+
const absolute = (id?: string): string | undefined =>
|
|
9
|
+
id === undefined || id.includes('://') ? id : `${HERE}${id}`;
|
|
10
|
+
|
|
11
|
+
/** The values a format lets in, as a type — `Accepted<typeof Lifecycle.format>`. */
|
|
12
|
+
export type Accepted<F> = F extends Format<infer T> ? T : never;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* How a format is written here: its keys, never its JSON Schema. `$id`, `$ref`, `$defs` and the
|
|
16
|
+
* closing are produced, and what it accepts is carried as a TYPE, so a declaration is stated once.
|
|
17
|
+
* FR : on écrit les clés d'un format ; le document et le type sont produits.
|
|
18
|
+
* `Format.of('axis/tenancy').key('scope', Format.tokens(SCOPES)).closed()`
|
|
19
|
+
*/
|
|
20
|
+
export class Format<T = unknown> {
|
|
21
|
+
static readonly anything = new Format<unknown>(true);
|
|
22
|
+
static readonly text = new Format<string>({ type: 'string' });
|
|
23
|
+
static readonly flag = new Format<boolean>({ type: 'boolean' });
|
|
24
|
+
|
|
25
|
+
declare readonly _admits?: T;
|
|
26
|
+
|
|
27
|
+
protected constructor(
|
|
28
|
+
private readonly stated: Stated,
|
|
29
|
+
readonly id?: string,
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
/** `Format.tokens(['now', 'optional'])` → `Instance does not match any of ["now","optional"].` */
|
|
33
|
+
static tokens<const Words extends readonly string[]>(words: Words): Format<Words[number]> {
|
|
34
|
+
return new Format({ enum: [...words] });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A word or a shape, told apart by TYPE, so a refusal names the half the value belongs to. */
|
|
38
|
+
static either<Word, Shape>(word: Format<Word>, shape: Format<Shape>): Format<Word | Shape> {
|
|
39
|
+
return new Format({
|
|
40
|
+
if: { type: 'string' },
|
|
41
|
+
then: word.schema as JsonSchema,
|
|
42
|
+
else: shape.schema as JsonSchema,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One of these keys and no other — `{ value }` or `{ generate }`, never both and never neither. */
|
|
47
|
+
static exactlyOne<Keys extends Record<string, Format<unknown>>>(
|
|
48
|
+
keys: Keys,
|
|
49
|
+
): Format<{ [K in keyof Keys]: { [P in K]: Accepted<Keys[K]> } }[keyof Keys]> {
|
|
50
|
+
return new Format({
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: Object.fromEntries(
|
|
53
|
+
Object.entries(keys).map(([key, format]) => [key, format.schema]),
|
|
54
|
+
),
|
|
55
|
+
propertyNames: { enum: Object.keys(keys) },
|
|
56
|
+
minProperties: 1,
|
|
57
|
+
maxProperties: 1,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A name resolves under this project's own space; another one states its whole `$id`. */
|
|
62
|
+
static of(id?: string): ObjectFormat<object> {
|
|
63
|
+
return new ObjectFormat(absolute(id));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** What `of` does for an object, for a format that is not one — `either`, a list of words. */
|
|
67
|
+
static named<Value>(id: string, format: Format<Value>): Format<Value> {
|
|
68
|
+
const named = absolute(id) as string;
|
|
69
|
+
|
|
70
|
+
return new Format({ $id: named, ...(format.schema as JsonSchema) }, named);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The type JSON Schema cannot carry: a generator's name, a thunk to an entity. */
|
|
74
|
+
as<Value>(): Format<Value> {
|
|
75
|
+
return this as unknown as Format<Value>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get schema(): Stated {
|
|
79
|
+
return this.stated;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class ObjectFormat<T extends object> extends Format<T> {
|
|
84
|
+
private readonly keys = new Map<string, Stated>();
|
|
85
|
+
private readonly cited = new Map<string, JsonSchema>();
|
|
86
|
+
private readonly needed: string[] = [];
|
|
87
|
+
|
|
88
|
+
constructor(id?: string) {
|
|
89
|
+
super(true, id);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
key<Name extends string, Value>(
|
|
93
|
+
name: Name,
|
|
94
|
+
format: Format<Value>,
|
|
95
|
+
): ObjectFormat<T & { [K in Name]?: Value }> {
|
|
96
|
+
this.keys.set(name, format.schema);
|
|
97
|
+
|
|
98
|
+
return this as ObjectFormat<T & { [K in Name]?: Value }>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A key carried whole and pointed at by its `$id`, so it keeps an identity of its own. */
|
|
102
|
+
cites<Name extends string, Value>(
|
|
103
|
+
name: Name,
|
|
104
|
+
format: Format<Value>,
|
|
105
|
+
): ObjectFormat<T & { [K in Name]?: Value }> {
|
|
106
|
+
if (!format.id) {
|
|
107
|
+
throw new SchemaError(
|
|
108
|
+
`A format cited as '${name}' states no \`$id\`, so nothing can point at it.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
this.keys.set(name, { $ref: format.id });
|
|
112
|
+
this.cited.set(name, format.schema as JsonSchema);
|
|
113
|
+
|
|
114
|
+
return this as ObjectFormat<T & { [K in Name]?: Value }>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
needs<Name extends keyof T & string>(
|
|
118
|
+
...names: Name[]
|
|
119
|
+
): ObjectFormat<Omit<T, Name> & Required<Pick<T, Name>>> {
|
|
120
|
+
this.needed.push(...names);
|
|
121
|
+
|
|
122
|
+
return this as unknown as ObjectFormat<Omit<T, Name> & Required<Pick<T, Name>>>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** These keys and no other: a name nothing states is refused UNDER that name. */
|
|
126
|
+
closed(): Format<{ [K in keyof T]: T[K] }> {
|
|
127
|
+
return new Format(
|
|
128
|
+
{
|
|
129
|
+
...(this.id ? { $id: this.id } : {}),
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: Object.fromEntries(this.keys),
|
|
132
|
+
propertyNames: { enum: [...this.keys.keys()] },
|
|
133
|
+
...(this.needed.length ? { required: [...this.needed] } : {}),
|
|
134
|
+
...(this.cited.size ? { $defs: Object.fromEntries(this.cited) } : {}),
|
|
135
|
+
},
|
|
136
|
+
this.id,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { Schema as JsonSchema } from '@cfworker/json-schema';
|
package/src/lib/Registry.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { SchemaError } from '../SchemaError.js';
|
|
2
2
|
|
|
3
3
|
export class Registry<T> {
|
|
4
|
-
private readonly
|
|
4
|
+
private readonly held: Map<string, T>;
|
|
5
5
|
|
|
6
6
|
constructor(
|
|
7
7
|
private readonly label: string,
|
|
8
8
|
private readonly hint?: string,
|
|
9
9
|
entries?: Iterable<readonly [string, T]>,
|
|
10
10
|
) {
|
|
11
|
-
this.
|
|
11
|
+
this.held = new Map(entries);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
register(name: string, value: T): T {
|
|
15
|
-
this.
|
|
15
|
+
this.held.set(name, value);
|
|
16
16
|
|
|
17
17
|
return value;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
find(name: string): T | undefined {
|
|
21
|
-
return this.
|
|
21
|
+
return this.held.get(name);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
resolve(name: string, path?: string): T {
|
|
@@ -32,7 +32,15 @@ export class Registry<T> {
|
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
return [...this.
|
|
35
|
+
get names(): string[] {
|
|
36
|
+
return [...this.held.keys()];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get entries(): [string, T][] {
|
|
40
|
+
return [...this.held];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get all(): T[] {
|
|
44
|
+
return [...this.held.values()];
|
|
37
45
|
}
|
|
38
46
|
}
|
|
@@ -9,3 +9,11 @@ export type ValidationResult<T> =
|
|
|
9
9
|
* reason the value stays segmented: this one cannot be read back.
|
|
10
10
|
*/
|
|
11
11
|
export const dotted = (path: readonly string[]): string => path.join('.');
|
|
12
|
+
|
|
13
|
+
/** `#/city/zip` — a JSON Pointer fragment, and `~1`/`~0` are how it spells `/` and `~`. */
|
|
14
|
+
export const locationOf = (instanceLocation: string): string[] =>
|
|
15
|
+
instanceLocation
|
|
16
|
+
.replace(/^#/, '')
|
|
17
|
+
.split('/')
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'));
|