@fougere/schema 0.3.0-alpha.0 → 0.4.0-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/README.md +1 -1
- package/dist/EntityAdapters.d.ts +26 -0
- package/dist/EntityAdapters.d.ts.map +1 -0
- package/dist/EntityAdapters.js +71 -0
- package/dist/EntityAdapters.js.map +1 -0
- package/dist/EntityDeclarations.d.ts +7 -14
- package/dist/EntityDeclarations.d.ts.map +1 -1
- package/dist/EntityDeclarations.js +1 -18
- package/dist/EntityDeclarations.js.map +1 -1
- package/dist/axis/Axis.d.ts +2 -0
- package/dist/axis/Axis.d.ts.map +1 -1
- package/dist/axis/Axis.js +5 -3
- package/dist/axis/Axis.js.map +1 -1
- package/dist/axis/boundary/Boundary.d.ts +1 -1
- package/dist/axis/boundary/Boundary.d.ts.map +1 -1
- package/dist/axis/boundary/{axis.d.ts → BoundaryAxis.d.ts} +1 -1
- package/dist/axis/boundary/BoundaryAxis.d.ts.map +1 -0
- package/dist/axis/boundary/{axis.js → BoundaryAxis.js} +3 -2
- package/dist/axis/boundary/BoundaryAxis.js.map +1 -0
- package/dist/axis/lifecycle/Clock.d.ts +18 -0
- package/dist/axis/lifecycle/Clock.d.ts.map +1 -0
- package/dist/axis/lifecycle/Clock.js +25 -0
- package/dist/axis/lifecycle/Clock.js.map +1 -0
- package/dist/axis/lifecycle/Generators.d.ts +16 -2
- package/dist/axis/lifecycle/Generators.d.ts.map +1 -1
- package/dist/axis/lifecycle/Generators.js +35 -5
- package/dist/axis/lifecycle/Generators.js.map +1 -1
- package/dist/axis/lifecycle/{axis.d.ts → LifecycleAxis.d.ts} +1 -1
- package/dist/axis/lifecycle/LifecycleAxis.d.ts.map +1 -0
- package/dist/axis/lifecycle/{axis.js → LifecycleAxis.js} +3 -2
- package/dist/axis/lifecycle/LifecycleAxis.js.map +1 -0
- package/dist/axis/lifecycle/apply.d.ts +1 -3
- package/dist/axis/lifecycle/apply.d.ts.map +1 -1
- package/dist/axis/lifecycle/apply.js +5 -43
- package/dist/axis/lifecycle/apply.js.map +1 -1
- package/dist/axis/role/Relation.d.ts +11 -1
- package/dist/axis/role/Relation.d.ts.map +1 -1
- package/dist/axis/role/Relation.js +18 -3
- package/dist/axis/role/Relation.js.map +1 -1
- package/dist/axis/role/Role.d.ts +5 -17
- package/dist/axis/role/Role.d.ts.map +1 -1
- package/dist/axis/role/Role.js +9 -19
- package/dist/axis/role/Role.js.map +1 -1
- package/dist/axis/role/{axis.d.ts → RoleAxis.d.ts} +1 -1
- package/dist/axis/role/RoleAxis.d.ts.map +1 -0
- package/dist/axis/role/{axis.js → RoleAxis.js} +5 -4
- package/dist/axis/role/RoleAxis.js.map +1 -0
- package/dist/card/Bundle.d.ts +15 -0
- package/dist/card/Bundle.d.ts.map +1 -0
- package/dist/card/Bundle.js +67 -0
- package/dist/card/Bundle.js.map +1 -0
- package/dist/card/Card.d.ts +27 -0
- package/dist/card/Card.d.ts.map +1 -0
- package/dist/card/Card.js +230 -0
- package/dist/card/Card.js.map +1 -0
- package/dist/card/EntityTypeSource.d.ts +12 -0
- package/dist/card/EntityTypeSource.d.ts.map +1 -0
- package/dist/card/{typescript.js → EntityTypeSource.js} +20 -35
- package/dist/card/EntityTypeSource.js.map +1 -0
- package/dist/card/FacadeTypeSource.d.ts +19 -0
- package/dist/card/FacadeTypeSource.d.ts.map +1 -0
- package/dist/card/FacadeTypeSource.js +40 -0
- package/dist/card/FacadeTypeSource.js.map +1 -0
- package/dist/{judge/form.d.ts → card/admission.d.ts} +2 -4
- package/dist/card/admission.d.ts.map +1 -0
- package/dist/{judge/form.js → card/admission.js} +1 -3
- package/dist/card/admission.js.map +1 -0
- package/dist/card/diff.d.ts +11 -55
- package/dist/card/diff.d.ts.map +1 -1
- package/dist/card/diff.js +1 -122
- package/dist/card/diff.js.map +1 -1
- package/dist/constraint/FieldGroup.d.ts +2 -2
- package/dist/constraint/FieldGroup.d.ts.map +1 -1
- package/dist/constraint/FieldGroup.js.map +1 -1
- package/dist/entity.d.ts +3 -3
- package/dist/entity.d.ts.map +1 -1
- package/dist/entity.js +8 -13
- package/dist/entity.js.map +1 -1
- package/dist/fields/Field.d.ts +24 -0
- package/dist/fields/Field.d.ts.map +1 -0
- package/dist/{Field.js → fields/Field.js} +17 -15
- package/dist/fields/Field.js.map +1 -0
- package/dist/fields/FieldSet.d.ts +28 -0
- package/dist/fields/FieldSet.d.ts.map +1 -0
- package/dist/fields/FieldSet.js +55 -0
- package/dist/fields/FieldSet.js.map +1 -0
- package/dist/index.d.ts +23 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -17
- package/dist/index.js.map +1 -1
- package/dist/judge/{cases.d.ts → Cases.d.ts} +22 -22
- package/dist/judge/Cases.d.ts.map +1 -0
- package/dist/judge/{cases.js → Cases.js} +35 -27
- package/dist/judge/Cases.js.map +1 -0
- package/dist/judge/FieldJudge.d.ts +9 -0
- package/dist/judge/FieldJudge.d.ts.map +1 -0
- package/dist/judge/FieldJudge.js +54 -0
- package/dist/judge/FieldJudge.js.map +1 -0
- package/dist/judge/RowJudge.d.ts +12 -0
- package/dist/judge/RowJudge.d.ts.map +1 -0
- package/dist/judge/RowJudge.js +81 -0
- package/dist/judge/RowJudge.js.map +1 -0
- package/dist/judge/RowRefusal.d.ts +17 -0
- package/dist/judge/RowRefusal.d.ts.map +1 -0
- package/dist/judge/RowRefusal.js +16 -0
- package/dist/judge/RowRefusal.js.map +1 -0
- package/dist/judge/ValueForm.d.ts +3 -0
- package/dist/judge/ValueForm.d.ts.map +1 -0
- package/dist/judge/ValueForm.js +3 -0
- package/dist/judge/ValueForm.js.map +1 -0
- package/dist/judge/ValueJudge.d.ts +12 -0
- package/dist/judge/ValueJudge.d.ts.map +1 -0
- package/dist/judge/ValueJudge.js +60 -0
- package/dist/judge/ValueJudge.js.map +1 -0
- package/dist/projection/SchemaOrCard.d.ts +7 -0
- package/dist/projection/SchemaOrCard.d.ts.map +1 -0
- package/dist/projection/{source.js → SchemaOrCard.js} +3 -3
- package/dist/projection/SchemaOrCard.js.map +1 -0
- package/dist/projection/Visibility.d.ts +10 -0
- package/dist/projection/Visibility.d.ts.map +1 -0
- package/dist/projection/Visibility.js +55 -0
- package/dist/projection/Visibility.js.map +1 -0
- package/dist/projection/standard.d.ts +2 -2
- package/dist/projection/standard.d.ts.map +1 -1
- package/dist/{Schema.d.ts → schema/Schema.d.ts} +30 -34
- package/dist/schema/Schema.d.ts.map +1 -0
- package/dist/{Schema.js → schema/Schema.js} +25 -48
- package/dist/schema/Schema.js.map +1 -0
- package/dist/schema/SchemaDefinition.d.ts +46 -0
- package/dist/schema/SchemaDefinition.d.ts.map +1 -0
- package/dist/schema/SchemaDefinition.js +64 -0
- package/dist/schema/SchemaDefinition.js.map +1 -0
- package/dist/schema/SchemaDerivation.d.ts +28 -0
- package/dist/schema/SchemaDerivation.d.ts.map +1 -0
- package/dist/schema/SchemaDerivation.js +37 -0
- package/dist/schema/SchemaDerivation.js.map +1 -0
- package/dist/schema/SchemaView.d.ts +23 -0
- package/dist/schema/SchemaView.d.ts.map +1 -0
- package/dist/{SchemaView.js.map → schema/SchemaView.js.map} +1 -1
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +13 -0
- package/dist/utils.js.map +1 -0
- package/dist/vocabulary/bool.d.ts +1 -1
- package/dist/vocabulary/bool.d.ts.map +1 -1
- package/dist/vocabulary/bool.js +1 -1
- package/dist/vocabulary/bool.js.map +1 -1
- package/dist/vocabulary/created.d.ts +1 -1
- package/dist/vocabulary/created.d.ts.map +1 -1
- package/dist/vocabulary/date.d.ts +1 -1
- package/dist/vocabulary/date.d.ts.map +1 -1
- package/dist/vocabulary/date.js +1 -1
- package/dist/vocabulary/date.js.map +1 -1
- package/dist/vocabulary/email.d.ts +1 -1
- package/dist/vocabulary/email.d.ts.map +1 -1
- package/dist/vocabulary/immutable.d.ts +1 -1
- package/dist/vocabulary/immutable.d.ts.map +1 -1
- package/dist/vocabulary/indexed.d.ts +1 -1
- package/dist/vocabulary/indexed.d.ts.map +1 -1
- package/dist/vocabulary/json.d.ts +2 -2
- package/dist/vocabulary/json.d.ts.map +1 -1
- package/dist/vocabulary/json.js +6 -4
- package/dist/vocabulary/json.js.map +1 -1
- package/dist/vocabulary/list.d.ts +1 -1
- package/dist/vocabulary/list.d.ts.map +1 -1
- package/dist/vocabulary/list.js +1 -1
- package/dist/vocabulary/list.js.map +1 -1
- package/dist/vocabulary/many.d.ts +1 -1
- package/dist/vocabulary/many.d.ts.map +1 -1
- package/dist/vocabulary/many.js +3 -3
- package/dist/vocabulary/many.js.map +1 -1
- package/dist/vocabulary/nullable.d.ts +1 -1
- package/dist/vocabulary/nullable.d.ts.map +1 -1
- package/dist/vocabulary/number.d.ts +1 -1
- package/dist/vocabulary/number.d.ts.map +1 -1
- package/dist/vocabulary/number.js +1 -1
- package/dist/vocabulary/number.js.map +1 -1
- package/dist/vocabulary/oneOf.d.ts +1 -1
- package/dist/vocabulary/oneOf.d.ts.map +1 -1
- package/dist/vocabulary/oneOf.js +1 -1
- package/dist/vocabulary/oneOf.js.map +1 -1
- package/dist/vocabulary/optional.d.ts +1 -1
- package/dist/vocabulary/optional.d.ts.map +1 -1
- package/dist/vocabulary/primary.d.ts +1 -1
- package/dist/vocabulary/primary.d.ts.map +1 -1
- package/dist/vocabulary/primary.js +4 -5
- package/dist/vocabulary/primary.js.map +1 -1
- package/dist/vocabulary/readOnly.d.ts +1 -1
- package/dist/vocabulary/readOnly.d.ts.map +1 -1
- package/dist/vocabulary/ref.d.ts +1 -1
- package/dist/vocabulary/ref.d.ts.map +1 -1
- package/dist/vocabulary/ref.js +3 -9
- package/dist/vocabulary/ref.js.map +1 -1
- package/dist/vocabulary/text.d.ts +1 -1
- package/dist/vocabulary/text.d.ts.map +1 -1
- package/dist/vocabulary/text.js +1 -1
- package/dist/vocabulary/text.js.map +1 -1
- package/dist/vocabulary/unique.d.ts +1 -1
- package/dist/vocabulary/unique.d.ts.map +1 -1
- package/dist/vocabulary/updated.d.ts +1 -1
- package/dist/vocabulary/updated.d.ts.map +1 -1
- package/dist/vocabulary/url.d.ts +1 -1
- package/dist/vocabulary/url.d.ts.map +1 -1
- package/dist/vocabulary/vocabulary.d.ts +2 -3
- package/dist/vocabulary/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary/vocabulary.js +4 -4
- package/dist/vocabulary/vocabulary.js.map +1 -1
- package/dist/vocabulary/writeOnly.d.ts +1 -1
- package/dist/vocabulary/writeOnly.d.ts.map +1 -1
- package/package.json +8 -4
- package/src/EntityAdapters.ts +95 -0
- package/src/EntityDeclarations.ts +19 -0
- package/src/axis/Axis.ts +25 -0
- package/src/axis/Meta.ts +3 -0
- package/src/axis/boundary/Boundaries.ts +44 -0
- package/src/axis/boundary/Boundary.ts +69 -0
- package/src/axis/boundary/BoundaryAxis.ts +32 -0
- package/src/axis/lifecycle/Clock.ts +26 -0
- package/src/axis/lifecycle/Generators.ts +46 -0
- package/src/axis/lifecycle/Lifecycle.ts +53 -0
- package/src/axis/lifecycle/LifecycleAxis.ts +49 -0
- package/src/axis/lifecycle/apply.ts +38 -0
- package/src/axis/role/Relation.ts +31 -0
- package/src/axis/role/Role.ts +56 -0
- package/src/axis/role/RoleAxis.ts +115 -0
- package/src/axis/shape/Formats.ts +22 -0
- package/src/axis/shape/Shape.ts +91 -0
- package/src/card/Bundle.ts +80 -0
- package/src/card/Card.ts +256 -0
- package/src/card/Descriptor.ts +68 -0
- package/src/card/EntityTypeSource.ts +90 -0
- package/src/card/FacadeTypeSource.ts +55 -0
- package/src/card/admission.ts +25 -0
- package/src/card/diff.ts +56 -0
- package/src/constraint/FieldGroup.ts +91 -0
- package/src/constraint/Unique.ts +3 -0
- package/src/entity.ts +15 -0
- package/src/fields/Field.ts +80 -0
- package/src/fields/FieldSet.ts +62 -0
- package/src/index.ts +64 -0
- package/src/judge/Cases.ts +139 -0
- package/src/judge/FieldJudge.ts +61 -0
- package/src/judge/RowJudge.ts +84 -0
- package/src/judge/RowRefusal.ts +17 -0
- package/src/judge/ValueForm.ts +7 -0
- package/src/judge/ValueJudge.ts +72 -0
- package/src/judge/options.ts +3 -0
- package/src/judge/result.ts +10 -0
- package/src/projection/SchemaOrCard.ts +18 -0
- package/src/projection/Visibility.ts +47 -0
- package/src/projection/standard.ts +47 -0
- package/src/schema/Schema.ts +167 -0
- package/src/schema/SchemaDefinition.ts +87 -0
- package/src/schema/SchemaDerivation.ts +46 -0
- package/src/schema/SchemaView.ts +25 -0
- package/src/utils.ts +13 -0
- package/src/vocabulary/bool.ts +9 -0
- package/src/vocabulary/created.ts +8 -0
- package/src/vocabulary/date.ts +5 -0
- package/src/vocabulary/email.ts +6 -0
- package/src/vocabulary/immutable.ts +6 -0
- package/src/vocabulary/indexed.ts +4 -0
- package/src/vocabulary/json.ts +22 -0
- package/src/vocabulary/list.ts +13 -0
- package/src/vocabulary/many.ts +9 -0
- package/src/vocabulary/nullable.ts +7 -0
- package/src/vocabulary/number.ts +17 -0
- package/src/vocabulary/oneOf.ts +24 -0
- package/src/vocabulary/optional.ts +8 -0
- package/src/vocabulary/primary.ts +33 -0
- package/src/vocabulary/readOnly.ts +7 -0
- package/src/vocabulary/ref.ts +13 -0
- package/src/vocabulary/text.ts +19 -0
- package/src/vocabulary/unique.ts +10 -0
- package/src/vocabulary/updated.ts +7 -0
- package/src/vocabulary/url.ts +6 -0
- package/src/vocabulary/vocabulary.ts +40 -0
- package/src/vocabulary/writeOnly.ts +7 -0
- package/dist/Field.d.ts +0 -21
- package/dist/Field.d.ts.map +0 -1
- package/dist/Field.js.map +0 -1
- package/dist/Schema.d.ts.map +0 -1
- package/dist/Schema.js.map +0 -1
- package/dist/SchemaView.d.ts +0 -18
- package/dist/SchemaView.d.ts.map +0 -1
- package/dist/axis/boundary/axis.d.ts.map +0 -1
- package/dist/axis/boundary/axis.js.map +0 -1
- package/dist/axis/lifecycle/axis.d.ts.map +0 -1
- package/dist/axis/lifecycle/axis.js.map +0 -1
- package/dist/axis/role/axis.d.ts.map +0 -1
- package/dist/axis/role/axis.js.map +0 -1
- package/dist/card/describe.d.ts +0 -12
- package/dist/card/describe.d.ts.map +0 -1
- package/dist/card/describe.js +0 -83
- package/dist/card/describe.js.map +0 -1
- package/dist/card/reconstruct.d.ts +0 -11
- package/dist/card/reconstruct.d.ts.map +0 -1
- package/dist/card/reconstruct.js +0 -83
- package/dist/card/reconstruct.js.map +0 -1
- package/dist/card/typescript.d.ts +0 -17
- package/dist/card/typescript.d.ts.map +0 -1
- package/dist/card/typescript.js.map +0 -1
- package/dist/clean.d.ts +0 -2
- package/dist/clean.d.ts.map +0 -1
- package/dist/clean.js +0 -7
- package/dist/clean.js.map +0 -1
- package/dist/judge/Judge.d.ts +0 -14
- package/dist/judge/Judge.d.ts.map +0 -1
- package/dist/judge/Judge.js +0 -161
- package/dist/judge/Judge.js.map +0 -1
- package/dist/judge/cases.d.ts.map +0 -1
- package/dist/judge/cases.js.map +0 -1
- package/dist/judge/form.d.ts.map +0 -1
- package/dist/judge/form.js.map +0 -1
- package/dist/name.d.ts +0 -3
- package/dist/name.d.ts.map +0 -1
- package/dist/name.js +0 -7
- package/dist/name.js.map +0 -1
- package/dist/projection/encode.d.ts +0 -3
- package/dist/projection/encode.d.ts.map +0 -1
- package/dist/projection/encode.js +0 -19
- package/dist/projection/encode.js.map +0 -1
- package/dist/projection/io.d.ts +0 -4
- package/dist/projection/io.d.ts.map +0 -1
- package/dist/projection/io.js +0 -29
- package/dist/projection/io.js.map +0 -1
- package/dist/projection/source.d.ts +0 -7
- package/dist/projection/source.d.ts.map +0 -1
- package/dist/projection/source.js.map +0 -1
- package/dist/same.d.ts +0 -6
- package/dist/same.d.ts.map +0 -1
- package/dist/same.js +0 -35
- package/dist/same.js.map +0 -1
- /package/dist/{SchemaView.js → schema/SchemaView.js} +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Axis, Resolver } from '../Axis.js';
|
|
2
|
+
import { refuse } from '../../card/admission.js';
|
|
3
|
+
import { isObject, oneOfTokens } from '../../judge/ValueForm.js';
|
|
4
|
+
import type { ValidationError } from '../../judge/result.js';
|
|
5
|
+
import { ON_DELETE, RELATION_KINDS, type EntityConstructor, type Relation } from './Relation.js';
|
|
6
|
+
import { type RoleRules } from './Role.js';
|
|
7
|
+
import { FieldGroup } from '../../constraint/FieldGroup.js';
|
|
8
|
+
import { Unique } from '../../constraint/Unique.js';
|
|
9
|
+
import type { RoleDescriptor } from '../../card/Descriptor.js';
|
|
10
|
+
import { lowerFirst } from '../../utils.js';
|
|
11
|
+
|
|
12
|
+
export const roleAxis: Axis<RoleRules, RoleDescriptor> = {
|
|
13
|
+
slot: 'role',
|
|
14
|
+
|
|
15
|
+
judge(value, errors) {
|
|
16
|
+
if (!isObject(value)) {
|
|
17
|
+
errors.push({ path: 'role', message: `Expected an object — got ${JSON.stringify(value)}` });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
for (const flag of ['primary', 'index'] as const) {
|
|
21
|
+
if (value[flag] !== undefined && typeof value[flag] !== 'boolean') {
|
|
22
|
+
errors.push({
|
|
23
|
+
path: `role.${flag}`,
|
|
24
|
+
message: `Expected a boolean — got ${JSON.stringify(value[flag])}`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (value.rules !== undefined) {
|
|
29
|
+
const legal =
|
|
30
|
+
Array.isArray(value.rules) &&
|
|
31
|
+
value.rules.every(
|
|
32
|
+
(rule) =>
|
|
33
|
+
rule instanceof FieldGroup ||
|
|
34
|
+
(Array.isArray(rule) && rule.every((member) => typeof member === 'string')),
|
|
35
|
+
);
|
|
36
|
+
if (!legal) {
|
|
37
|
+
errors.push({
|
|
38
|
+
path: 'role.rules',
|
|
39
|
+
message: 'Expected field groups, or the member lists they denote — string[][]',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (value.relation !== undefined) judgeRelation(value.relation, errors);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
describe(role, key) {
|
|
47
|
+
const out: Mutable<RoleDescriptor> = {};
|
|
48
|
+
if (role.primary) out.primary = true;
|
|
49
|
+
const unique = (role.rules ?? []).filter((rule) => rule instanceof Unique);
|
|
50
|
+
if (unique.length) out.unique = unique.map((rule) => [...rule.resolvedOn(key).members]);
|
|
51
|
+
if (role.index) out.index = true;
|
|
52
|
+
if (role.relation) {
|
|
53
|
+
const target = role.relation.to() as { name?: string };
|
|
54
|
+
out.relation = {
|
|
55
|
+
to: lowerFirst(target.name ?? ''),
|
|
56
|
+
kind: role.relation.kind,
|
|
57
|
+
...(role.relation.onDelete ? { onDelete: role.relation.onDelete } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return Object.keys(out).length ? out : undefined;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
reconstruct(wire, resolve?: Resolver) {
|
|
64
|
+
const out: RoleRules = {};
|
|
65
|
+
if (wire.primary) out.primary = true;
|
|
66
|
+
if (wire.unique?.length) out.rules = wire.unique.map((group) => new Unique(group));
|
|
67
|
+
if (wire.index) out.index = true;
|
|
68
|
+
if (wire.relation) {
|
|
69
|
+
// `judge` cannot serve here: it demands a thunk where a card carries a name.
|
|
70
|
+
if (!oneOfTokens(wire.relation.kind, RELATION_KINDS)) {
|
|
71
|
+
refuse(
|
|
72
|
+
`role.relation.kind is ${JSON.stringify(wire.relation.kind)}`,
|
|
73
|
+
`Expected one of ${RELATION_KINDS.join(', ')}.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (wire.relation.onDelete !== undefined && !oneOfTokens(wire.relation.onDelete, ON_DELETE)) {
|
|
77
|
+
refuse(
|
|
78
|
+
`role.relation.onDelete is ${JSON.stringify(wire.relation.onDelete)}`,
|
|
79
|
+
`Expected one of ${ON_DELETE.join(', ')}.`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const name = wire.relation.to;
|
|
83
|
+
out.relation = {
|
|
84
|
+
to: () => (resolve?.(name) ?? ({ name } as unknown)) as EntityConstructor,
|
|
85
|
+
kind: wire.relation.kind as Relation['kind'],
|
|
86
|
+
...(wire.relation.onDelete ? { onDelete: wire.relation.onDelete as Relation['onDelete'] } : {}),
|
|
87
|
+
} as RoleRules['relation'];
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function judgeRelation(relation: unknown, errors: ValidationError[]): void {
|
|
94
|
+
if (!isObject(relation)) {
|
|
95
|
+
errors.push({ path: 'role.relation', message: `Expected an object — got ${JSON.stringify(relation)}` });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!oneOfTokens(relation.kind, RELATION_KINDS)) {
|
|
99
|
+
errors.push({
|
|
100
|
+
path: 'role.relation.kind',
|
|
101
|
+
message: `Expected 'one' or 'many' — got ${JSON.stringify(relation.kind)}`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (typeof relation.to !== 'function') {
|
|
105
|
+
errors.push({ path: 'role.relation.to', message: 'Expected a thunk returning the target entity' });
|
|
106
|
+
}
|
|
107
|
+
if (relation.onDelete !== undefined && !oneOfTokens(relation.onDelete, ON_DELETE)) {
|
|
108
|
+
errors.push({
|
|
109
|
+
path: 'role.relation.onDelete',
|
|
110
|
+
message: `Expected 'cascade', 'restrict' or 'set null' — got ${JSON.stringify(relation.onDelete)}`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type StringFormat =
|
|
2
|
+
| 'date-time'
|
|
3
|
+
| 'date'
|
|
4
|
+
| 'time'
|
|
5
|
+
| 'email'
|
|
6
|
+
| 'uuid'
|
|
7
|
+
| 'uri'
|
|
8
|
+
| (string & {});
|
|
9
|
+
|
|
10
|
+
export type FormatPredicate = (value: string) => boolean;
|
|
11
|
+
|
|
12
|
+
export class Formats {
|
|
13
|
+
private static readonly registry = new Map<string, FormatPredicate>();
|
|
14
|
+
|
|
15
|
+
static register(name: string, predicate: FormatPredicate): void {
|
|
16
|
+
this.registry.set(name, predicate);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static resolve(name: string): FormatPredicate | undefined {
|
|
20
|
+
return this.registry.get(name);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { JSONSchema7 } from 'json-schema';
|
|
2
|
+
import type { StringFormat } from './Formats.js';
|
|
3
|
+
|
|
4
|
+
type Nullably<T extends string> = T | readonly [T, 'null'];
|
|
5
|
+
|
|
6
|
+
interface StringConstraints { minLength?: number; maxLength?: number; pattern?: string; enum?: readonly (string | null)[]; format?: StringFormat }
|
|
7
|
+
interface NumericConstraints { minimum?: number; maximum?: number }
|
|
8
|
+
interface ArrayConstraints { items?: Shape; minItems?: number; maxItems?: number }
|
|
9
|
+
interface ObjectConstraints { properties?: Record<string, unknown>; required?: readonly string[] }
|
|
10
|
+
|
|
11
|
+
export type Shape =
|
|
12
|
+
| ({ type: Nullably<'string'> } & StringConstraints)
|
|
13
|
+
| ({ type: Nullably<'number'> | Nullably<'integer'> } & NumericConstraints)
|
|
14
|
+
| { type: Nullably<'boolean'> }
|
|
15
|
+
| ({ type: Nullably<'array'> } & ArrayConstraints)
|
|
16
|
+
| ({ type: Nullably<'object'> } & ObjectConstraints);
|
|
17
|
+
|
|
18
|
+
const SHAPE_TYPES = ['string', 'number', 'integer', 'boolean', 'array', 'object'] as const;
|
|
19
|
+
|
|
20
|
+
function isShapeImpl(value: unknown): value is Shape {
|
|
21
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
22
|
+
const declared = (value as Shape).type;
|
|
23
|
+
const names = Array.isArray(declared) ? declared : [declared];
|
|
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
|
+
type BaseShape =
|
|
31
|
+
| ({ type: 'string' } & StringConstraints)
|
|
32
|
+
| ({ type: 'number' | 'integer' } & NumericConstraints)
|
|
33
|
+
| { type: 'boolean' }
|
|
34
|
+
| ({ type: 'array' } & ArrayConstraints)
|
|
35
|
+
| ({ type: 'object' } & ObjectConstraints);
|
|
36
|
+
|
|
37
|
+
function nullableShapeImpl(shape: Shape): Shape {
|
|
38
|
+
if (Array.isArray(shape.type)) return shape;
|
|
39
|
+
const out = { ...shape, type: [shape.type, 'null'] } as unknown as Shape;
|
|
40
|
+
if ('enum' in out && out.enum && !out.enum.includes(null)) {
|
|
41
|
+
(out as { enum: readonly (string | null)[] }).enum = [...out.enum, null];
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ShapeAnatomy {
|
|
47
|
+
base?: BaseShape;
|
|
48
|
+
nullable: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class Anatomy {
|
|
52
|
+
static is(value: unknown): value is Shape {
|
|
53
|
+
return isShapeImpl(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static nullable(shape: Shape): Shape {
|
|
57
|
+
return nullableShapeImpl(shape);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private static readonly cache = new WeakMap<object, ShapeAnatomy>();
|
|
61
|
+
private static readonly none: ShapeAnatomy = { base: undefined, nullable: false };
|
|
62
|
+
|
|
63
|
+
static of(shape?: Shape): ShapeAnatomy {
|
|
64
|
+
if (!shape) return this.none;
|
|
65
|
+
let a = this.cache.get(shape);
|
|
66
|
+
if (!a) {
|
|
67
|
+
if (Array.isArray(shape.type)) {
|
|
68
|
+
const baseType = shape.type.find((t) => t !== 'null');
|
|
69
|
+
const base = { ...shape, type: baseType } as BaseShape;
|
|
70
|
+
if ('enum' in base && base.enum) {
|
|
71
|
+
(base as { enum: readonly (string | null)[] }).enum = base.enum.filter((v) => v !== null);
|
|
72
|
+
}
|
|
73
|
+
a = { base, nullable: true };
|
|
74
|
+
} else {
|
|
75
|
+
a = { base: shape as BaseShape, nullable: false };
|
|
76
|
+
}
|
|
77
|
+
this.cache.set(shape, a);
|
|
78
|
+
}
|
|
79
|
+
return a;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static isNullable(shape?: Shape): boolean {
|
|
83
|
+
return this.of(shape).nullable;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type Assert<T extends true> = T;
|
|
88
|
+
type ShapeKeys<T> = T extends unknown ? keyof T : never;
|
|
89
|
+
type _ShapeConformsToJsonSchema = Assert<
|
|
90
|
+
[Exclude<ShapeKeys<Shape>, keyof JSONSchema7>] extends [never] ? true : false
|
|
91
|
+
>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Resolver } from '../axis/Axis.js';
|
|
2
|
+
import type { EntityConstructor } from '../axis/role/Relation.js';
|
|
3
|
+
import { lowerFirst } from '../utils.js';
|
|
4
|
+
import type { SchemaView } from '../schema/SchemaView.js';
|
|
5
|
+
import { Card } from './Card.js';
|
|
6
|
+
import type { SchemaBundle, SchemaDescriptor } from './Descriptor.js';
|
|
7
|
+
import type { Diff, SetDiff, SetDiffOptions } from './diff.js';
|
|
8
|
+
|
|
9
|
+
type SchemaSet = Record<string, SchemaView> | SchemaView[];
|
|
10
|
+
|
|
11
|
+
/** A portable schema map and the decisions that only a map can make. */
|
|
12
|
+
export class Bundle {
|
|
13
|
+
private constructor(readonly descriptor: SchemaBundle) {}
|
|
14
|
+
|
|
15
|
+
static fromSchemas(schemas: SchemaSet): Bundle {
|
|
16
|
+
const definitions: Record<string, SchemaDescriptor> = {};
|
|
17
|
+
const claimedBy = new Map<string, string>();
|
|
18
|
+
const entries = Array.isArray(schemas)
|
|
19
|
+
? schemas.map((schema) => {
|
|
20
|
+
const card = Card.fromSchema(schema);
|
|
21
|
+
return { name: card.descriptor.title ?? '', schema };
|
|
22
|
+
})
|
|
23
|
+
: Object.entries(schemas).map(([name, schema]) => ({ name, schema }));
|
|
24
|
+
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const key = lowerFirst(entry.name);
|
|
27
|
+
const previous = claimedBy.get(key);
|
|
28
|
+
if (previous !== undefined) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Schemas '${previous}' and '${entry.name}' both claim bundle key '${key}'. `
|
|
31
|
+
+ 'Each schema in a bundle must have a distinct registration key.',
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
claimedBy.set(key, entry.name);
|
|
35
|
+
definitions[key] = Card.fromSchema(entry.schema, key).descriptor;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return new Bundle({
|
|
39
|
+
$defs: definitions,
|
|
40
|
+
'x-fougere-version': 1,
|
|
41
|
+
'x-fougere-vendor': 'fougere',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static fromDescriptor(descriptor: SchemaBundle): Bundle {
|
|
46
|
+
return new Bundle(descriptor);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
toSchemas(): Record<string, SchemaView> {
|
|
50
|
+
const map: Record<string, EntityConstructor> = {};
|
|
51
|
+
const resolve: Resolver = (name) => map[name.toLowerCase()];
|
|
52
|
+
const schemas: Record<string, SchemaView> = {};
|
|
53
|
+
for (const [name, descriptor] of Object.entries(this.descriptor.$defs)) {
|
|
54
|
+
const schema = Card.fromDescriptor(descriptor).toSchema(resolve, name);
|
|
55
|
+
map[name.toLowerCase()] = schema as unknown as EntityConstructor;
|
|
56
|
+
schemas[name] = schema;
|
|
57
|
+
}
|
|
58
|
+
return schemas;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
diff(other: Bundle, options: SetDiffOptions = {}): SetDiff {
|
|
62
|
+
const before = this.descriptor.$defs ?? {};
|
|
63
|
+
const after = other.descriptor.$defs ?? {};
|
|
64
|
+
const entities: Record<string, Diff> = {};
|
|
65
|
+
for (const [name, descriptor] of Object.entries(before)) {
|
|
66
|
+
const target = after[name];
|
|
67
|
+
if (target === undefined) continue;
|
|
68
|
+
const answer = Card.fromDescriptor(descriptor).diff(
|
|
69
|
+
Card.fromDescriptor(target),
|
|
70
|
+
{ renamed: options.renamed?.[name] ?? {} },
|
|
71
|
+
);
|
|
72
|
+
if (answer.changes.length > 0 || answer.ambiguous.length > 0) entities[name] = answer;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
entitiesAdded: Object.keys(after).filter((name) => !(name in before)),
|
|
76
|
+
entitiesRemoved: Object.keys(before).filter((name) => !(name in after)),
|
|
77
|
+
entities,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/card/Card.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { EXTENSION_AXES, EXTENSION_SLOTS, type Resolver } from '../axis/Axis.js';
|
|
2
|
+
import { dequal } from 'dequal';
|
|
3
|
+
import { clean } from '../utils.js';
|
|
4
|
+
import { Field, type Fields } from '../fields/Field.js';
|
|
5
|
+
import { isObject } from '../judge/ValueForm.js';
|
|
6
|
+
import { RowJudge } from '../judge/RowJudge.js';
|
|
7
|
+
import { Schema, type SchemaConstructor } from '../schema/Schema.js';
|
|
8
|
+
import type { Row, SchemaView } from '../schema/SchemaView.js';
|
|
9
|
+
import { refuse } from './admission.js';
|
|
10
|
+
import type {
|
|
11
|
+
DerivedFrom,
|
|
12
|
+
FieldDescriptor,
|
|
13
|
+
FieldExtension,
|
|
14
|
+
SchemaDescriptor,
|
|
15
|
+
} from './Descriptor.js';
|
|
16
|
+
import type { Change, Diff, DiffOptions, RenameCandidate, TypeSet } from './diff.js';
|
|
17
|
+
|
|
18
|
+
type FieldsOf<T> = { [K in keyof T]-?: Field<T[K]> };
|
|
19
|
+
|
|
20
|
+
/** One portable schema description and every decision made about that description. */
|
|
21
|
+
export class Card<T = Row<Fields>> {
|
|
22
|
+
private constructor(readonly descriptor: SchemaDescriptor) {}
|
|
23
|
+
|
|
24
|
+
static fromSchema<TFields extends Fields>(schema: SchemaView<TFields>, name?: string): Card<Row<TFields>> {
|
|
25
|
+
const fields = schema.getFields();
|
|
26
|
+
const judge = RowJudge.of(fields);
|
|
27
|
+
const properties: Record<string, FieldDescriptor> = {};
|
|
28
|
+
const required: string[] = [];
|
|
29
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
30
|
+
properties[key] = describeField(field, key);
|
|
31
|
+
if (judge.onAbsent(field) === null) required.push(key);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const descriptor: SchemaDescriptor = {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties,
|
|
37
|
+
'x-fougere-version': 1,
|
|
38
|
+
'x-fougere-vendor': 'fougere',
|
|
39
|
+
};
|
|
40
|
+
const title = name ?? schema.derivation?.sourceName ?? schema.name;
|
|
41
|
+
if (title) descriptor.title = title;
|
|
42
|
+
if (required.length) descriptor.required = required;
|
|
43
|
+
|
|
44
|
+
const origin = originOf(schema);
|
|
45
|
+
if (origin) descriptor['x-fougere-derived'] = origin;
|
|
46
|
+
return new Card<Row<TFields>>(descriptor);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static fromDescriptor<T = Row<Fields>>(descriptor: SchemaDescriptor): Card<T> {
|
|
50
|
+
return new Card<T>(descriptor);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The root derivation recorded by this card, when its schema was cut from another. */
|
|
54
|
+
get origin(): DerivedFrom | undefined {
|
|
55
|
+
return this.descriptor['x-fougere-derived'];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Rebuild the live schema. A bundle supplies `resolve` so relation thunks can find the
|
|
60
|
+
* other reconstructed cards; a lone card deliberately falls back to a name stand-in.
|
|
61
|
+
*/
|
|
62
|
+
toSchema(resolve?: Resolver, name?: string): SchemaConstructor<FieldsOf<T>> {
|
|
63
|
+
const descriptor = this.descriptor;
|
|
64
|
+
const where = name ? `schema '${name}'` : 'this schema';
|
|
65
|
+
if (!isObject(descriptor)) refuse(`${where} is not an object`, 'A card carries one JSON Schema per door.');
|
|
66
|
+
const version = descriptor['x-fougere-version'];
|
|
67
|
+
if (version !== 1) {
|
|
68
|
+
refuse(
|
|
69
|
+
`${where} states \`x-fougere-version: ${JSON.stringify(version)}\` and this reader speaks 1`,
|
|
70
|
+
'A producer and its readers move together: re-sync the consumer, or serve the version it speaks.',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (!isObject(descriptor.properties)) {
|
|
74
|
+
refuse(`${where} carries no \`properties\` object`, 'A schema is its fields; there is nothing to rebuild.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const fields: Fields = {};
|
|
78
|
+
for (const [key, property] of Object.entries(descriptor.properties)) {
|
|
79
|
+
fields[key] = reconstructField(property, key, resolve);
|
|
80
|
+
}
|
|
81
|
+
const schema = Schema.of({ fields });
|
|
82
|
+
const title = name ?? descriptor.title;
|
|
83
|
+
if (title) Object.defineProperty(schema, 'name', { value: title, configurable: true });
|
|
84
|
+
return schema as unknown as SchemaConstructor<FieldsOf<T>>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** What must change to turn this card into `other`. */
|
|
88
|
+
diff(other: Card, options: DiffOptions = {}): Diff {
|
|
89
|
+
const changes: Change[] = [];
|
|
90
|
+
const renamed = options.renamed ?? {};
|
|
91
|
+
const before = this.descriptor.properties ?? {};
|
|
92
|
+
const after = other.descriptor.properties ?? {};
|
|
93
|
+
const requiredBefore = new Set(this.descriptor.required ?? []);
|
|
94
|
+
const requiredAfter = new Set(other.descriptor.required ?? []);
|
|
95
|
+
|
|
96
|
+
// Apply a declared rename first so subsequent differences use the new field name.
|
|
97
|
+
const nameAfter = (field: string): string => renamed[field] ?? field;
|
|
98
|
+
const removed: string[] = [];
|
|
99
|
+
for (const [field, descriptor] of Object.entries(before)) {
|
|
100
|
+
const now = nameAfter(field);
|
|
101
|
+
const target = after[now];
|
|
102
|
+
if (target === undefined) {
|
|
103
|
+
removed.push(field);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (now !== field) changes.push({ kind: 'renamed', from: field, to: now, field: target });
|
|
108
|
+
|
|
109
|
+
const wasType = typesOf(descriptor);
|
|
110
|
+
const isType = typesOf(target);
|
|
111
|
+
if (!dequal(wasType, isType)) changes.push({ kind: 'retyped', field: now, from: wasType, to: isType });
|
|
112
|
+
else if (!dequal(boundsOf(descriptor), boundsOf(target))) {
|
|
113
|
+
changes.push({ kind: 'reshaped', field: now, from: descriptor, to: target });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const wasRequired = requiredBefore.has(field);
|
|
117
|
+
const isRequired = requiredAfter.has(now);
|
|
118
|
+
if (wasRequired !== isRequired) {
|
|
119
|
+
changes.push({ kind: 'required', field: now, from: wasRequired, to: isRequired });
|
|
120
|
+
}
|
|
121
|
+
changes.push(...restated(now, descriptor['x-fougere'], target['x-fougere']));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const claimed = new Set(Object.values(renamed));
|
|
125
|
+
const added = Object.keys(after).filter((field) => !(field in before) && !claimed.has(field));
|
|
126
|
+
for (const field of removed) {
|
|
127
|
+
changes.push({ kind: 'removed', field, from: before[field], required: requiredBefore.has(field) });
|
|
128
|
+
}
|
|
129
|
+
for (const field of added) {
|
|
130
|
+
changes.push({ kind: 'added', field, to: after[field], required: requiredAfter.has(field) });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { changes, ambiguous: candidates(removed, added, before, after) };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function describeExtension(field: Field, key: string): FieldExtension | undefined {
|
|
138
|
+
const extension: Record<string, unknown> = {};
|
|
139
|
+
for (const axis of EXTENSION_AXES) {
|
|
140
|
+
const declared = (field as unknown as Record<string, unknown>)[axis.slot];
|
|
141
|
+
if (declared === undefined) continue;
|
|
142
|
+
const wire = axis.describe(declared, key);
|
|
143
|
+
if (wire !== undefined) extension[axis.slot] = wire;
|
|
144
|
+
}
|
|
145
|
+
clean(extension);
|
|
146
|
+
return Object.keys(extension).length ? (extension as FieldExtension) : undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function describeField(field: Field, key: string): FieldDescriptor {
|
|
150
|
+
const descriptor: FieldDescriptor = {};
|
|
151
|
+
for (const [shapeKey, value] of Object.entries(field.shape)) {
|
|
152
|
+
if (value !== undefined) (descriptor as Record<string, unknown>)[shapeKey] = value;
|
|
153
|
+
}
|
|
154
|
+
if (field.meta?.description) descriptor.description = field.meta.description;
|
|
155
|
+
const extension = describeExtension(field, key);
|
|
156
|
+
if (extension) descriptor['x-fougere'] = extension;
|
|
157
|
+
return descriptor;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function originOf(schema: SchemaView): DerivedFrom | undefined {
|
|
161
|
+
const { derivation } = schema;
|
|
162
|
+
if (!derivation) return undefined;
|
|
163
|
+
const survived = Object.fromEntries(Object.entries(derivation.survived).map(([key, value]) => [key, value ?? null]));
|
|
164
|
+
return { from: derivation.sourceName, survived };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function reconstructShape(property: FieldDescriptor): Field['shape'] | undefined {
|
|
168
|
+
const types = Array.isArray(property.type) ? property.type : property.type ? [property.type] : [];
|
|
169
|
+
const base = types.find((type) => type !== 'null');
|
|
170
|
+
if (base === undefined) return undefined;
|
|
171
|
+
const type = property.type as Field['shape']['type'];
|
|
172
|
+
if (base === 'array') {
|
|
173
|
+
const items = property.items ? reconstructShape(property.items) : undefined;
|
|
174
|
+
return clean({ type, items, minItems: property.minItems, maxItems: property.maxItems }) as Field['shape'];
|
|
175
|
+
}
|
|
176
|
+
switch (base) {
|
|
177
|
+
case 'string':
|
|
178
|
+
return clean({
|
|
179
|
+
type,
|
|
180
|
+
minLength: property.minLength,
|
|
181
|
+
maxLength: property.maxLength,
|
|
182
|
+
pattern: property.pattern,
|
|
183
|
+
enum: property.enum,
|
|
184
|
+
format: property.format,
|
|
185
|
+
}) as Field['shape'];
|
|
186
|
+
case 'number':
|
|
187
|
+
case 'integer':
|
|
188
|
+
return clean({ type, minimum: property.minimum, maximum: property.maximum }) as Field['shape'];
|
|
189
|
+
case 'boolean':
|
|
190
|
+
return { type } as Field['shape'];
|
|
191
|
+
default:
|
|
192
|
+
return clean({ type, properties: property.properties, required: property.required }) as Field['shape'];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function reconstructField(property: FieldDescriptor, key: string, resolve?: Resolver): Field {
|
|
197
|
+
const shape = reconstructShape(property);
|
|
198
|
+
if (!shape) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Field '${key}': the card carries no \`type\` for it, so there is no shape to rebuild. `
|
|
201
|
+
+ 'A field always states one.',
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const extension = property['x-fougere'];
|
|
205
|
+
const axes: Record<string, unknown> = {};
|
|
206
|
+
for (const axis of EXTENSION_AXES) {
|
|
207
|
+
const wire = (extension as Record<string, unknown> | undefined)?.[axis.slot];
|
|
208
|
+
if (wire !== undefined) axes[axis.slot] = axis.reconstruct(wire, resolve);
|
|
209
|
+
}
|
|
210
|
+
return new Field({
|
|
211
|
+
shape,
|
|
212
|
+
...axes,
|
|
213
|
+
meta: property.description !== undefined ? { description: property.description } : undefined,
|
|
214
|
+
} as never);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function restated(field: string, before: FieldExtension | undefined, after: FieldExtension | undefined): Change[] {
|
|
218
|
+
return EXTENSION_SLOTS.filter((axis) => !dequal(before?.[axis], after?.[axis])).map(
|
|
219
|
+
(axis) => ({ kind: 'restated', field, axis, from: before?.[axis], to: after?.[axis] }) as Change,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function shapeOf(descriptor: FieldDescriptor): Record<string, unknown> {
|
|
224
|
+
const { 'x-fougere': _extension, ...shape } = descriptor;
|
|
225
|
+
return shape as Record<string, unknown>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function typesOf(descriptor: FieldDescriptor): TypeSet {
|
|
229
|
+
const type = descriptor.type;
|
|
230
|
+
if (type === undefined) return [];
|
|
231
|
+
return (Array.isArray(type) ? [...type] : [type]).sort();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function boundsOf(descriptor: FieldDescriptor): Record<string, unknown> {
|
|
235
|
+
const { type: _type, ...rest } = shapeOf(descriptor);
|
|
236
|
+
return rest;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function candidates(
|
|
240
|
+
removed: string[],
|
|
241
|
+
added: string[],
|
|
242
|
+
before: Record<string, FieldDescriptor>,
|
|
243
|
+
after: Record<string, FieldDescriptor>,
|
|
244
|
+
): RenameCandidate[] {
|
|
245
|
+
const found: RenameCandidate[] = [];
|
|
246
|
+
for (const gone of removed) {
|
|
247
|
+
for (const appeared of added) {
|
|
248
|
+
if (dequal(shapeOf(before[gone]), shapeOf(after[appeared]))) found.push({ removed: gone, added: appeared });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const was = Object.keys(before);
|
|
252
|
+
const now = Object.keys(after);
|
|
253
|
+
const apart = ({ removed: gone, added: appeared }: RenameCandidate): number =>
|
|
254
|
+
Math.abs(now.indexOf(appeared) - was.indexOf(gone));
|
|
255
|
+
return found.sort((a, b) => apart(a) - apart(b));
|
|
256
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { BoundaryRef } from '../axis/boundary/Boundary.js';
|
|
2
|
+
import type { LifecycleRules } from '../axis/lifecycle/Lifecycle.js';
|
|
3
|
+
import type { Relation } from '../axis/role/Relation.js';
|
|
4
|
+
import type { RoleRules } from '../axis/role/Role.js';
|
|
5
|
+
import type { Shape } from '../axis/shape/Shape.js';
|
|
6
|
+
|
|
7
|
+
type JsonSchemaType =
|
|
8
|
+
| 'string'
|
|
9
|
+
| 'number'
|
|
10
|
+
| 'integer'
|
|
11
|
+
| 'boolean'
|
|
12
|
+
| 'object'
|
|
13
|
+
| 'array'
|
|
14
|
+
| 'null';
|
|
15
|
+
|
|
16
|
+
type ShapeKeywords = UnionToIntersection<KeywordsOf<Shape>>;
|
|
17
|
+
|
|
18
|
+
type KeywordsOf<S> = S extends unknown ? Partial<Omit<S, 'type' | 'items' | 'properties'>> : never;
|
|
19
|
+
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
|
20
|
+
|
|
21
|
+
export type FieldDescriptor = ShapeKeywords & {
|
|
22
|
+
type?: JsonSchemaType | JsonSchemaType[];
|
|
23
|
+
items?: FieldDescriptor;
|
|
24
|
+
properties?: Record<string, FieldDescriptor>;
|
|
25
|
+
description?: string;
|
|
26
|
+
'x-fougere'?: FieldExtension;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface FieldExtension {
|
|
30
|
+
role?: RoleDescriptor;
|
|
31
|
+
lifecycle?: LifecycleRules;
|
|
32
|
+
boundary?: BoundaryRef;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type RoleDescriptor = Pick<RoleRules, 'primary' | 'index'> & {
|
|
36
|
+
unique?: string[][];
|
|
37
|
+
relation?: RelationDescriptor;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type RelationDescriptor = Pick<Relation, 'kind' | 'onDelete'> & { to: string };
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What a derivation was cut from, and what the cut left — `survived` keyed by the
|
|
44
|
+
* ORIGIN's field names. Absent on a declaration that derives from nothing.
|
|
45
|
+
*
|
|
46
|
+
* A dropped field is `null` and never `undefined`: JSON.stringify erases the second,
|
|
47
|
+
* which would leave the card saying only what remains — what `properties` already says.
|
|
48
|
+
*/
|
|
49
|
+
export interface DerivedFrom {
|
|
50
|
+
from: string;
|
|
51
|
+
survived: Record<string, string | null>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SchemaDescriptor {
|
|
55
|
+
title?: string;
|
|
56
|
+
type: 'object';
|
|
57
|
+
properties: Record<string, FieldDescriptor>;
|
|
58
|
+
required?: string[];
|
|
59
|
+
'x-fougere-derived'?: DerivedFrom;
|
|
60
|
+
'x-fougere-version': 1;
|
|
61
|
+
'x-fougere-vendor': 'fougere';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SchemaBundle {
|
|
65
|
+
$defs: Record<string, SchemaDescriptor>;
|
|
66
|
+
'x-fougere-version': 1;
|
|
67
|
+
'x-fougere-vendor': 'fougere';
|
|
68
|
+
}
|