@fougere/schema 0.12.1-alpha.0 → 0.12.2-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/FougereFieldAxes.d.ts.map +1 -0
- package/dist/FougereFieldAxes.js.map +1 -0
- package/dist/Schema.d.ts +4 -3
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +3 -0
- package/dist/Schema.js.map +1 -1
- package/dist/SchemaConstraints.d.ts +13 -10
- package/dist/SchemaConstraints.d.ts.map +1 -1
- package/dist/SchemaConstraints.js +32 -20
- package/dist/SchemaConstraints.js.map +1 -1
- package/dist/SchemaDefinition.d.ts.map +1 -1
- package/dist/SchemaDefinition.js +6 -5
- package/dist/SchemaDefinition.js.map +1 -1
- package/dist/SchemaError.d.ts +4 -1
- package/dist/SchemaError.d.ts.map +1 -1
- package/dist/SchemaError.js +5 -2
- package/dist/SchemaError.js.map +1 -1
- package/dist/SchemaView.d.ts +3 -2
- package/dist/SchemaView.d.ts.map +1 -1
- package/dist/axis/Axes.d.ts +1 -1
- package/dist/axis/Axes.js +3 -3
- package/dist/axis/Axis.d.ts +12 -2
- package/dist/axis/Axis.d.ts.map +1 -1
- package/dist/axis/boundary/Boundary.d.ts +9 -2
- package/dist/axis/boundary/Boundary.d.ts.map +1 -1
- package/dist/axis/boundary/Boundary.js +6 -2
- package/dist/axis/boundary/Boundary.js.map +1 -1
- package/dist/axis/boundary/Decoder.js.map +1 -1
- package/dist/axis/lifecycle/Lifecycle.d.ts +13 -4
- package/dist/axis/lifecycle/Lifecycle.d.ts.map +1 -1
- package/dist/axis/lifecycle/Lifecycle.js +8 -4
- package/dist/axis/lifecycle/Lifecycle.js.map +1 -1
- package/dist/axis/lifecycle/apply.js +1 -1
- package/dist/axis/lifecycle/apply.js.map +1 -1
- package/dist/axis/role/Role.d.ts +17 -10
- package/dist/axis/role/Role.d.ts.map +1 -1
- package/dist/axis/role/Role.js +30 -10
- package/dist/axis/role/Role.js.map +1 -1
- package/dist/axis/shape/Shape.d.ts +32 -9
- package/dist/axis/shape/Shape.d.ts.map +1 -1
- package/dist/axis/shape/Shape.js +43 -12
- package/dist/axis/shape/Shape.js.map +1 -1
- package/dist/axis/shape/ShapeFormat.d.ts +10 -0
- package/dist/axis/shape/ShapeFormat.d.ts.map +1 -0
- package/dist/axis/shape/ShapeFormat.js +32 -0
- package/dist/axis/shape/ShapeFormat.js.map +1 -0
- package/dist/entity/EntityDeclarations.d.ts +3 -2
- package/dist/entity/EntityDeclarations.d.ts.map +1 -1
- package/dist/entity/FieldGroups.d.ts +9 -0
- package/dist/entity/FieldGroups.d.ts.map +1 -0
- package/dist/entity/FieldGroups.js +2 -0
- package/dist/entity/FieldGroups.js.map +1 -0
- package/dist/field/Field.d.ts +35 -16
- package/dist/field/Field.d.ts.map +1 -1
- package/dist/field/Field.js +53 -8
- package/dist/field/Field.js.map +1 -1
- package/dist/field/FieldFormat.d.ts +1 -1
- package/dist/field/FieldFormat.js +4 -4
- package/dist/field/FieldFormat.js.map +1 -1
- package/dist/field/FieldSet.d.ts +12 -3
- package/dist/field/FieldSet.d.ts.map +1 -1
- package/dist/field/FieldSet.js +22 -15
- package/dist/field/FieldSet.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/Format.d.ts +21 -2
- package/dist/lib/Format.d.ts.map +1 -1
- package/dist/lib/Format.js +31 -3
- package/dist/lib/Format.js.map +1 -1
- package/dist/lib/UnionToIntersection.d.ts +7 -0
- package/dist/lib/UnionToIntersection.d.ts.map +1 -0
- package/dist/lib/UnionToIntersection.js +2 -0
- package/dist/lib/UnionToIntersection.js.map +1 -0
- package/dist/lib/utils.d.ts +1 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/projection/Cases.d.ts +1 -1
- package/dist/projection/Cases.d.ts.map +1 -1
- package/dist/projection/Cases.js +20 -8
- package/dist/projection/Cases.js.map +1 -1
- package/dist/projection/Visibility.d.ts.map +1 -1
- package/dist/projection/Visibility.js +6 -6
- package/dist/projection/Visibility.js.map +1 -1
- package/dist/projection/card/Bundle.d.ts.map +1 -1
- package/dist/projection/card/Bundle.js.map +1 -1
- package/dist/projection/card/Card.d.ts.map +1 -1
- package/dist/projection/card/Card.js +21 -30
- package/dist/projection/card/Card.js.map +1 -1
- package/dist/projection/card/CardForms.js +1 -1
- package/dist/projection/card/Diff.js.map +1 -1
- package/dist/projection/card/FieldExtension.d.ts +2 -2
- package/dist/projection/card/FieldExtension.d.ts.map +1 -1
- package/dist/projection/card/RoleDescriptor.d.ts +7 -2
- package/dist/projection/card/RoleDescriptor.d.ts.map +1 -1
- package/dist/projection/card/RoleDescriptor.js +2 -2
- package/dist/projection/card/RoleDescriptor.js.map +1 -1
- package/dist/projection/card/ShapeKeywords.d.ts +1 -1
- package/dist/projection/card/ShapeKeywords.d.ts.map +1 -1
- package/dist/validator/FieldDeclarationValidator.d.ts.map +1 -1
- package/dist/validator/FieldDeclarationValidator.js +7 -10
- package/dist/validator/FieldDeclarationValidator.js.map +1 -1
- package/dist/validator/FieldValueValidator.d.ts.map +1 -1
- package/dist/validator/FieldValueValidator.js +0 -2
- package/dist/validator/FieldValueValidator.js.map +1 -1
- package/dist/validator/InputValidator.d.ts +7 -1
- package/dist/validator/InputValidator.d.ts.map +1 -1
- package/dist/validator/InputValidator.js +15 -14
- package/dist/validator/InputValidator.js.map +1 -1
- package/dist/vocabulary/preset/created.d.ts +2 -2
- package/dist/vocabulary/preset/created.d.ts.map +1 -1
- package/dist/vocabulary/preset/created.js +2 -2
- package/dist/vocabulary/preset/created.js.map +1 -1
- package/dist/vocabulary/preset/updated.d.ts +2 -2
- package/dist/vocabulary/preset/updated.d.ts.map +1 -1
- package/dist/vocabulary/preset/updated.js +2 -2
- package/dist/vocabulary/preset/updated.js.map +1 -1
- package/dist/vocabulary/primitive/bool.d.ts +2 -5
- package/dist/vocabulary/primitive/bool.d.ts.map +1 -1
- package/dist/vocabulary/primitive/bool.js +1 -5
- package/dist/vocabulary/primitive/bool.js.map +1 -1
- package/dist/vocabulary/primitive/date.d.ts +2 -3
- package/dist/vocabulary/primitive/date.d.ts.map +1 -1
- package/dist/vocabulary/primitive/date.js +2 -3
- package/dist/vocabulary/primitive/date.js.map +1 -1
- package/dist/vocabulary/primitive/json.d.ts +3 -3
- package/dist/vocabulary/primitive/json.d.ts.map +1 -1
- package/dist/vocabulary/primitive/json.js +11 -11
- package/dist/vocabulary/primitive/json.js.map +1 -1
- package/dist/vocabulary/primitive/list.d.ts +6 -5
- package/dist/vocabulary/primitive/list.d.ts.map +1 -1
- package/dist/vocabulary/primitive/list.js +13 -7
- package/dist/vocabulary/primitive/list.js.map +1 -1
- package/dist/vocabulary/primitive/many.d.ts +2 -4
- package/dist/vocabulary/primitive/many.d.ts.map +1 -1
- package/dist/vocabulary/primitive/many.js +2 -4
- package/dist/vocabulary/primitive/many.js.map +1 -1
- package/dist/vocabulary/primitive/number.d.ts +2 -4
- package/dist/vocabulary/primitive/number.d.ts.map +1 -1
- package/dist/vocabulary/primitive/number.js +1 -3
- package/dist/vocabulary/primitive/number.js.map +1 -1
- package/dist/vocabulary/primitive/oneOf.d.ts +4 -7
- package/dist/vocabulary/primitive/oneOf.d.ts.map +1 -1
- package/dist/vocabulary/primitive/oneOf.js +19 -11
- package/dist/vocabulary/primitive/oneOf.js.map +1 -1
- package/dist/vocabulary/primitive/primary.d.ts +2 -2
- package/dist/vocabulary/primitive/primary.d.ts.map +1 -1
- package/dist/vocabulary/primitive/primary.js +2 -3
- package/dist/vocabulary/primitive/primary.js.map +1 -1
- package/dist/vocabulary/primitive/ref.d.ts +2 -6
- package/dist/vocabulary/primitive/ref.d.ts.map +1 -1
- package/dist/vocabulary/primitive/ref.js +1 -1
- package/dist/vocabulary/primitive/ref.js.map +1 -1
- package/dist/vocabulary/primitive/text.d.ts +2 -4
- package/dist/vocabulary/primitive/text.d.ts.map +1 -1
- package/dist/vocabulary/primitive/text.js +1 -3
- package/dist/vocabulary/primitive/text.js.map +1 -1
- package/dist/vocabulary/rule/nullable.d.ts +0 -1
- package/dist/vocabulary/rule/nullable.d.ts.map +1 -1
- package/dist/vocabulary/rule/nullable.js +0 -1
- package/dist/vocabulary/rule/nullable.js.map +1 -1
- package/dist/vocabulary/rule/optional.d.ts +0 -1
- package/dist/vocabulary/rule/optional.d.ts.map +1 -1
- package/dist/vocabulary/rule/optional.js +0 -1
- package/dist/vocabulary/rule/optional.js.map +1 -1
- package/dist/vocabulary/rule/readOnly.d.ts +0 -2
- package/dist/vocabulary/rule/readOnly.d.ts.map +1 -1
- package/dist/vocabulary/rule/readOnly.js +0 -2
- package/dist/vocabulary/rule/readOnly.js.map +1 -1
- package/dist/vocabulary/rule/writeOnly.d.ts +0 -2
- package/dist/vocabulary/rule/writeOnly.d.ts.map +1 -1
- package/dist/vocabulary/rule/writeOnly.js +0 -2
- package/dist/vocabulary/rule/writeOnly.js.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 +17 -21
- package/dist/vocabulary/vocabulary.js.map +1 -1
- package/package.json +1 -1
- package/src/Schema.ts +11 -8
- package/src/SchemaConstraints.ts +40 -22
- package/src/SchemaDefinition.ts +7 -5
- package/src/SchemaError.ts +8 -2
- package/src/SchemaView.ts +3 -2
- package/src/axis/Axes.ts +3 -3
- package/src/axis/Axis.ts +13 -2
- package/src/axis/boundary/Boundary.ts +16 -2
- package/src/axis/boundary/Decoder.ts +2 -0
- package/src/axis/lifecycle/Lifecycle.ts +15 -4
- package/src/axis/lifecycle/apply.ts +1 -1
- package/src/axis/role/Role.ts +41 -9
- package/src/axis/shape/Shape.ts +89 -23
- package/src/axis/shape/ShapeFormat.ts +37 -0
- package/src/entity/EntityDeclarations.ts +3 -2
- package/src/entity/FieldGroups.ts +9 -0
- package/src/field/Field.ts +84 -21
- package/src/field/FieldFormat.ts +5 -5
- package/src/field/FieldSet.ts +27 -19
- package/src/index.ts +1 -0
- package/src/lib/Format.ts +36 -3
- package/src/lib/UnionToIntersection.ts +10 -0
- package/src/lib/utils.ts +5 -1
- package/src/projection/Cases.ts +25 -8
- package/src/projection/Visibility.ts +9 -6
- package/src/projection/card/Bundle.ts +2 -0
- package/src/projection/card/Card.ts +26 -27
- package/src/projection/card/CardForms.ts +1 -1
- package/src/projection/card/Diff.ts +4 -0
- package/src/projection/card/FieldExtension.ts +2 -2
- package/src/projection/card/RoleDescriptor.ts +9 -4
- package/src/projection/card/ShapeKeywords.ts +1 -1
- package/src/validator/FieldDeclarationValidator.ts +7 -10
- package/src/validator/FieldValueValidator.ts +3 -1
- package/src/validator/InputValidator.ts +17 -12
- package/src/vocabulary/preset/created.ts +3 -3
- package/src/vocabulary/preset/updated.ts +4 -3
- package/src/vocabulary/primitive/bool.ts +3 -8
- package/src/vocabulary/primitive/date.ts +5 -4
- package/src/vocabulary/primitive/json.ts +27 -12
- package/src/vocabulary/primitive/list.ts +16 -9
- package/src/vocabulary/primitive/many.ts +3 -4
- package/src/vocabulary/primitive/number.ts +3 -8
- package/src/vocabulary/primitive/oneOf.ts +40 -24
- package/src/vocabulary/primitive/primary.ts +9 -5
- package/src/vocabulary/primitive/ref.ts +3 -7
- package/src/vocabulary/primitive/text.ts +3 -8
- package/src/vocabulary/rule/nullable.ts +0 -1
- package/src/vocabulary/rule/optional.ts +0 -1
- package/src/vocabulary/rule/readOnly.ts +0 -2
- package/src/vocabulary/rule/writeOnly.ts +0 -2
- package/src/vocabulary/vocabulary.ts +18 -22
- package/dist/entity/CompositeUnique.d.ts +0 -4
- package/dist/entity/CompositeUnique.d.ts.map +0 -1
- package/dist/entity/CompositeUnique.js +0 -2
- package/dist/entity/CompositeUnique.js.map +0 -1
- package/dist/field/FougereFieldAxes.d.ts.map +0 -1
- package/dist/field/FougereFieldAxes.js.map +0 -1
- package/dist/field/Meta.d.ts +0 -6
- package/dist/field/Meta.d.ts.map +0 -1
- package/dist/field/Meta.js +0 -3
- package/dist/field/Meta.js.map +0 -1
- package/src/entity/CompositeUnique.ts +0 -6
- package/src/field/Meta.ts +0 -5
- /package/dist/{field/FougereFieldAxes.d.ts → FougereFieldAxes.d.ts} +0 -0
- /package/dist/{field/FougereFieldAxes.js → FougereFieldAxes.js} +0 -0
- /package/src/{field/FougereFieldAxes.ts → FougereFieldAxes.ts} +0 -0
|
@@ -1,43 +1,39 @@
|
|
|
1
1
|
import { Axes } from '../axis/Axes.js';
|
|
2
2
|
import { dequal } from 'dequal';
|
|
3
3
|
import { SchemaError } from '../SchemaError.js';
|
|
4
|
-
const memberSlots = () => [...Axes.names, 'meta'];
|
|
5
4
|
/**
|
|
6
|
-
* Builds a `rule/` word
|
|
7
|
-
* FR : fabrique un mot de `rule/`, qui énonce des membres sur le champ reçu.
|
|
5
|
+
* Builds a `rule/` word
|
|
8
6
|
* `vocabulary('indexed', () => ({ role: { index: true } }))`
|
|
9
7
|
*/
|
|
10
|
-
export function vocabulary(
|
|
11
|
-
return (field) => field.with(merge(
|
|
8
|
+
export function vocabulary(word, states) {
|
|
9
|
+
return (field) => field.with(merge(word, field, states(field)));
|
|
12
10
|
}
|
|
13
11
|
/**
|
|
14
|
-
* Refuses two words stating one member
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* Refuses two words stating one member DIFFERENTLY, rather than letting the outer win.
|
|
13
|
+
* `readOnly(writeOnly(text()))` → both apply, they write two members; `immutable(created())`
|
|
14
|
+
* too, since both say `update: 'forbidden'`; `immutable(updated())` → throws.
|
|
17
15
|
*/
|
|
18
|
-
function merge(
|
|
16
|
+
function merge(word, field, changes) {
|
|
19
17
|
const merged = {};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
for (const slot of memberSlots()) {
|
|
25
|
-
const members = stated[slot];
|
|
18
|
+
if ('shape' in changes)
|
|
19
|
+
merged.shape = changes.shape;
|
|
20
|
+
for (const name of Axes.names) {
|
|
21
|
+
const members = changes[name];
|
|
26
22
|
if (members === undefined)
|
|
27
23
|
continue;
|
|
28
|
-
const already =
|
|
29
|
-
if (typeof members !== 'object' || members === null || typeof already !== 'object') {
|
|
30
|
-
merged[
|
|
24
|
+
const already = field.axis(name);
|
|
25
|
+
if (typeof members !== 'object' || members === null || typeof already !== 'object' || already === null) {
|
|
26
|
+
merged[name] = members;
|
|
31
27
|
continue;
|
|
32
28
|
}
|
|
33
29
|
for (const [member, value] of Object.entries(members)) {
|
|
34
|
-
const previous = already
|
|
30
|
+
const previous = already[member];
|
|
35
31
|
if (previous === undefined || dequal(previous, value))
|
|
36
32
|
continue;
|
|
37
|
-
throw new SchemaError(`vocabulary: \`${
|
|
33
|
+
throw new SchemaError(`vocabulary: \`${word}\` states ${name}.${member} = ${JSON.stringify(value)}, but the ` +
|
|
38
34
|
`field already states ${JSON.stringify(previous)}. Apply one or the other, not both.`);
|
|
39
35
|
}
|
|
40
|
-
merged[
|
|
36
|
+
merged[name] = { ...already, ...members };
|
|
41
37
|
}
|
|
42
38
|
return merged;
|
|
43
39
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vocabulary.js","sourceRoot":"","sources":["../../src/vocabulary/vocabulary.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD
|
|
1
|
+
{"version":3,"file":"vocabulary.js","sourceRoot":"","sources":["../../src/vocabulary/vocabulary.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD;;;GAGG;AACH,MAAM,UAAU,UAAU,CACxB,IAAY,EACZ,MAAwC;IAExC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC;AAID;;;;GAIG;AACH,SAAS,KAAK,CAAC,IAAY,EAAE,KAAY,EAAE,OAAkC;IAC3E,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,IAAI,OAAO,IAAI,OAAO;QAAE,MAAM,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAErD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAmC,EAAE,CAAC;QAC5D,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,OAAO,KAAK,SAAS;YAAE,SAAS;QACpC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACvG,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC;YACvB,SAAS;QACX,CAAC;QACD,KAAK,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YACtD,MAAM,QAAQ,GAAI,OAAmC,CAAC,MAAM,CAAC,CAAC;YAC9D,IAAI,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC;gBAAE,SAAS;YAChE,MAAM,IAAI,WAAW,CACnB,iBAAiB,IAAI,aAAa,IAAI,IAAI,MAAM,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,YAAY;gBACrF,wBAAwB,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,qCAAqC,CACxF,CAAC;QACJ,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC;IAC5C,CAAC;IAED,OAAO,MAAwB,CAAC;AAClC,CAAC"}
|
package/package.json
CHANGED
package/src/Schema.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
|
2
2
|
|
|
3
3
|
import { Boundary } from './axis/boundary/Boundary.js';
|
|
4
4
|
import { type Fields } from './field/Fields.js';
|
|
5
|
-
import { type
|
|
5
|
+
import { type FieldGroups } from './entity/FieldGroups.js';
|
|
6
6
|
import { type EntityDeclarations } from './entity/EntityDeclarations.js';
|
|
7
7
|
import { type PreviousNames } from './entity/PreviousNames.js';
|
|
8
8
|
import { type EntityAdapters } from './entity/EntityAdapters.js';
|
|
@@ -16,6 +16,7 @@ import type { SchemaView } from './SchemaView.js';
|
|
|
16
16
|
|
|
17
17
|
import type { SchemaConstructor } from './SchemaConstructor.js';
|
|
18
18
|
import { SchemaError } from './SchemaError.js';
|
|
19
|
+
import type { UnionToIntersection } from './lib/UnionToIntersection.js';
|
|
19
20
|
|
|
20
21
|
export const ANONYMOUS_SCHEMA_NAME = 'Schema';
|
|
21
22
|
|
|
@@ -53,9 +54,13 @@ export class Schema {
|
|
|
53
54
|
static getAdapters(): EntityAdapters<Fields> {
|
|
54
55
|
return this.definition.adapterSet.adapters;
|
|
55
56
|
}
|
|
56
|
-
static getUnique():
|
|
57
|
+
static getUnique(): FieldGroups<Fields> | undefined {
|
|
57
58
|
return this.definition.constraints.unique;
|
|
58
59
|
}
|
|
60
|
+
|
|
61
|
+
static getIndex(): FieldGroups<Fields> | undefined {
|
|
62
|
+
return this.definition.constraints.index;
|
|
63
|
+
}
|
|
59
64
|
static getOpts(): ValidateOptions {
|
|
60
65
|
return this.definition.opts;
|
|
61
66
|
}
|
|
@@ -78,6 +83,7 @@ export class Schema {
|
|
|
78
83
|
const decoded = Boundary.of(field).decode(value);
|
|
79
84
|
row[key] = 'message' in decoded ? value : decoded.value;
|
|
80
85
|
}
|
|
86
|
+
|
|
81
87
|
return row;
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -90,6 +96,7 @@ export class Schema {
|
|
|
90
96
|
validate(value: unknown) {
|
|
91
97
|
const verdict = InputValidator.of(fields, opts).validate(value);
|
|
92
98
|
if (verdict.success) return { value: verdict.data };
|
|
99
|
+
|
|
93
100
|
return {
|
|
94
101
|
issues: verdict.errors.map((e) => ({
|
|
95
102
|
message: e.message,
|
|
@@ -138,6 +145,7 @@ export class Schema {
|
|
|
138
145
|
);
|
|
139
146
|
}
|
|
140
147
|
Object.defineProperty(this, 'name', { value: name, configurable: true });
|
|
148
|
+
|
|
141
149
|
return this;
|
|
142
150
|
}
|
|
143
151
|
|
|
@@ -168,16 +176,11 @@ export class Schema {
|
|
|
168
176
|
value: ANONYMOUS_SCHEMA_NAME,
|
|
169
177
|
configurable: true,
|
|
170
178
|
});
|
|
179
|
+
|
|
171
180
|
return Derived as unknown as SchemaConstructor<Fields>;
|
|
172
181
|
}
|
|
173
182
|
}
|
|
174
183
|
|
|
175
|
-
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (
|
|
176
|
-
k: infer I,
|
|
177
|
-
) => void
|
|
178
|
-
? I
|
|
179
|
-
: never;
|
|
180
|
-
|
|
181
184
|
type Merged<T extends SchemaView[]> = UnionToIntersection<
|
|
182
185
|
T[number] extends { getFields(): infer F } ? F : Fields
|
|
183
186
|
> &
|
package/src/SchemaConstraints.ts
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { type FieldName } from './field/FieldName.js';
|
|
2
2
|
import { type Fields } from './field/Fields.js';
|
|
3
|
-
import { type
|
|
3
|
+
import { type FieldGroups } from './entity/FieldGroups.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* What a schema
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* What a schema states beyond any single field: groups of names. `unique` constrains the
|
|
7
|
+
* rows, `index` says how they are reached — two decisions, and neither implies the other.
|
|
8
|
+
* They sit under an owner rather than beside `fields` and `opts` because the next rule
|
|
9
|
+
* spanning two fields — a check, an exclusion — is a member here, not an eighth positional
|
|
10
|
+
* argument in a list that has already lost one.
|
|
11
11
|
*/
|
|
12
12
|
export class SchemaConstraints<TFields extends Fields = Fields> {
|
|
13
|
-
readonly unique?:
|
|
13
|
+
readonly unique?: FieldGroups<TFields>;
|
|
14
|
+
readonly index?: FieldGroups<TFields>;
|
|
14
15
|
|
|
15
|
-
private constructor(unique?:
|
|
16
|
+
private constructor(unique?: FieldGroups<TFields>, index?: FieldGroups<TFields>) {
|
|
16
17
|
if (unique) this.unique = unique;
|
|
18
|
+
if (index) this.index = index;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
static readonly none = new SchemaConstraints();
|
|
@@ -24,23 +26,39 @@ export class SchemaConstraints<TFields extends Fields = Fields> {
|
|
|
24
26
|
* `SchemaConstraints.of([['a','b'], ['a','b']])` → `unique` holds one group
|
|
25
27
|
*/
|
|
26
28
|
static of<TFields extends Fields>(
|
|
27
|
-
groups:
|
|
29
|
+
groups: { unique?: FieldGroups<TFields>; index?: FieldGroups<TFields> },
|
|
28
30
|
): SchemaConstraints<TFields> {
|
|
29
|
-
|
|
30
|
-
for (const group of groups) seen.set(JSON.stringify(group), group);
|
|
31
|
-
|
|
32
|
-
return new SchemaConstraints(seen.size ? [...seen.values()] : undefined);
|
|
31
|
+
return new SchemaConstraints(deduped(groups.unique), deduped(groups.index));
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
/** One member gone and the group is gone: it
|
|
34
|
+
/** One member gone and the group is gone: it named a pair that no longer exists. */
|
|
36
35
|
renamed(transform: (key: string) => string | undefined): SchemaConstraints {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (renamed.every((key): key is string => key !== undefined)) kept.push(renamed);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return SchemaConstraints.of(kept);
|
|
36
|
+
return SchemaConstraints.of({
|
|
37
|
+
unique: survivors(this.unique, transform),
|
|
38
|
+
index: survivors(this.index, transform),
|
|
39
|
+
});
|
|
45
40
|
}
|
|
46
41
|
}
|
|
42
|
+
|
|
43
|
+
const deduped = <TFields extends Fields>(
|
|
44
|
+
groups?: FieldGroups<TFields>,
|
|
45
|
+
): FieldGroups<TFields> | undefined => {
|
|
46
|
+
if (!groups) return undefined;
|
|
47
|
+
const seen = new Map<string, readonly FieldName<TFields>[]>();
|
|
48
|
+
for (const group of groups) seen.set(JSON.stringify(group), group);
|
|
49
|
+
|
|
50
|
+
return seen.size ? [...seen.values()] : undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const survivors = (
|
|
54
|
+
groups: FieldGroups<Fields> | undefined,
|
|
55
|
+
transform: (key: string) => string | undefined,
|
|
56
|
+
): string[][] => {
|
|
57
|
+
const kept: string[][] = [];
|
|
58
|
+
for (const group of groups ?? []) {
|
|
59
|
+
const renamed = group.map(transform);
|
|
60
|
+
if (renamed.every((key): key is string => key !== undefined)) kept.push(renamed);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return kept;
|
|
64
|
+
};
|
package/src/SchemaDefinition.ts
CHANGED
|
@@ -113,7 +113,7 @@ export class SchemaDefinition {
|
|
|
113
113
|
...addressed,
|
|
114
114
|
...Object.keys(declarations.previous ?? {}),
|
|
115
115
|
]);
|
|
116
|
-
const declared = FieldSet.declaring(this.fields, declarations
|
|
116
|
+
const declared = FieldSet.declaring(this.fields, declarations);
|
|
117
117
|
|
|
118
118
|
return this.restated({
|
|
119
119
|
fields: declared.fields,
|
|
@@ -123,9 +123,9 @@ export class SchemaDefinition {
|
|
|
123
123
|
]),
|
|
124
124
|
previous: declarations.previous ?? this.previous,
|
|
125
125
|
constraints:
|
|
126
|
-
declarations.unique === undefined
|
|
126
|
+
declarations.unique === undefined && declarations.index === undefined
|
|
127
127
|
? this.constraints
|
|
128
|
-
: SchemaConstraints.of(declared
|
|
128
|
+
: SchemaConstraints.of(declared),
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -199,11 +199,13 @@ export class SchemaDefinition {
|
|
|
199
199
|
static merged(views: readonly SchemaView[]): SchemaDefinition {
|
|
200
200
|
const fields: Fields = {};
|
|
201
201
|
let opts: ValidateOptions = {};
|
|
202
|
-
const groups: (readonly string[])[]
|
|
202
|
+
const groups: { unique: (readonly string[])[]; index: (readonly string[])[] } =
|
|
203
|
+
{ unique: [], index: [] };
|
|
203
204
|
for (const view of views) {
|
|
204
205
|
Object.assign(fields, view.getFields());
|
|
205
206
|
opts = { ...opts, ...view.getOpts() };
|
|
206
|
-
groups.push(...(view.getUnique() ?? []));
|
|
207
|
+
groups.unique.push(...(view.getUnique() ?? []));
|
|
208
|
+
groups.index.push(...(view.getIndex() ?? []));
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
return SchemaDefinition.derived({
|
package/src/SchemaError.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
export class SchemaError extends Error {
|
|
2
|
-
constructor(message: string) {
|
|
3
|
-
super(
|
|
2
|
+
constructor(message: string, options?: { received: unknown }) {
|
|
3
|
+
super(
|
|
4
|
+
options ? `${message} — got ${SchemaError.inspect(options.received)}` : message,
|
|
5
|
+
);
|
|
4
6
|
|
|
5
7
|
this.name = 'SchemaError';
|
|
6
8
|
}
|
|
9
|
+
|
|
10
|
+
static inspect(value: unknown): string {
|
|
11
|
+
return JSON.stringify(value) ?? typeof value;
|
|
12
|
+
}
|
|
7
13
|
}
|
package/src/SchemaView.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Fields } from './field/Fields.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { FieldGroups } from './entity/FieldGroups.js';
|
|
3
3
|
import type { PreviousNames } from './entity/PreviousNames.js';
|
|
4
4
|
import type { EntityAdapters } from './entity/EntityAdapters.js';
|
|
5
5
|
import type { ValidationResult } from './lib/ValidationResult.js';
|
|
@@ -17,7 +17,8 @@ export interface SchemaView<TFields extends Fields = Fields> {
|
|
|
17
17
|
readonly anchored?: boolean;
|
|
18
18
|
getFields(): TFields;
|
|
19
19
|
getAdapters(): EntityAdapters<TFields>;
|
|
20
|
-
getUnique():
|
|
20
|
+
getUnique(): FieldGroups<TFields> | undefined;
|
|
21
|
+
getIndex(): FieldGroups<TFields> | undefined;
|
|
21
22
|
getOpts(): ValidateOptions;
|
|
22
23
|
validate(input: unknown): ValidationResult<Values<TFields>>;
|
|
23
24
|
}
|
package/src/axis/Axes.ts
CHANGED
|
@@ -9,10 +9,10 @@ import { Role } from './role/Role.js';
|
|
|
9
9
|
class AxisRegistry extends Registry<Axis> {
|
|
10
10
|
private composed?: JsonSchema;
|
|
11
11
|
|
|
12
|
-
register(
|
|
12
|
+
register(name: string, axis: Axis): Axis {
|
|
13
13
|
this.composed = undefined;
|
|
14
14
|
|
|
15
|
-
return super.register(
|
|
15
|
+
return super.register(name, axis);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** The document a field declaration is judged against, rebuilt the next time one arrives. */
|
|
@@ -27,7 +27,7 @@ class AxisRegistry extends Registry<Axis> {
|
|
|
27
27
|
* The axes this process reads, and the door an axis declared elsewhere comes through.
|
|
28
28
|
* `Axes.register('tenancy', Tenancy)` — from a `vocabulary/` file, read before `entities/`
|
|
29
29
|
*/
|
|
30
|
-
export const Axes = new AxisRegistry('axis', 'call Axes.register(
|
|
30
|
+
export const Axes = new AxisRegistry('axis', 'call Axes.register(name, axis)', [
|
|
31
31
|
['role', Role],
|
|
32
32
|
['lifecycle', Lifecycle],
|
|
33
33
|
['boundary', Boundary],
|
package/src/axis/Axis.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import type { Format } from '../lib/Format.js';
|
|
2
|
+
import type { Field } from '../field/Field.js';
|
|
2
3
|
import type { ValidationError } from '../lib/ValidationError.js';
|
|
3
4
|
|
|
4
5
|
export interface Axis {
|
|
5
6
|
readonly format: Format;
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* What a format cannot state: `role.relation.to` is a function, and JSON holds none.
|
|
10
|
+
* The whole declaration comes with the name, because a contradiction is a PAIR — a
|
|
11
|
+
* primary key that admits null states one legal `role` beside one legal `shape`.
|
|
12
|
+
*/
|
|
13
|
+
refusals?(value: unknown, declaration: Record<string, unknown>): ValidationError[];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Whether this axis alone lets the field be absent from what a caller sends. Asked of every
|
|
17
|
+
* REGISTERED axis, so a fourth one weighs on it without the validator naming it.
|
|
18
|
+
*/
|
|
19
|
+
admitsAbsence?(field: Field): boolean;
|
|
9
20
|
}
|
|
@@ -47,9 +47,15 @@ export class Boundary {
|
|
|
47
47
|
|
|
48
48
|
const alias = Boundaries.aliases.find(ref);
|
|
49
49
|
if (!alias) throw new SchemaError(`Unknown boundary alias: '${ref}'`);
|
|
50
|
+
|
|
50
51
|
return new Boundary(alias);
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
/** The server fills it, so a caller never sends it. */
|
|
55
|
+
static admitsAbsence(field: Field): boolean {
|
|
56
|
+
return this.of(field).readOnly();
|
|
57
|
+
}
|
|
58
|
+
|
|
53
59
|
static of(field: Field): Boundary {
|
|
54
60
|
const declared = Boundary.declared(field);
|
|
55
61
|
const derived = Boundary.forShape(field.shape);
|
|
@@ -57,6 +63,7 @@ export class Boundary {
|
|
|
57
63
|
in: declared.rules.in ?? derived.rules.in,
|
|
58
64
|
out: declared.rules.out ?? derived.rules.out,
|
|
59
65
|
};
|
|
66
|
+
|
|
60
67
|
return new Boundary(rules, {
|
|
61
68
|
decode:
|
|
62
69
|
typeof rules.in === 'object'
|
|
@@ -72,6 +79,7 @@ export class Boundary {
|
|
|
72
79
|
/** `date-time` means a `Date` on both sides, without a word in the entity. */
|
|
73
80
|
static forShape(shape: Shape | undefined): Boundary {
|
|
74
81
|
if (Shapes.typeOf(shape) === 'date') return new Boundary(Boundaries.aliases.resolve('isoDate'));
|
|
82
|
+
|
|
75
83
|
return new Boundary();
|
|
76
84
|
}
|
|
77
85
|
|
|
@@ -80,11 +88,17 @@ export class Boundary {
|
|
|
80
88
|
return { in: overrides.in ?? this.rules.in, out: overrides.out ?? this.rules.out };
|
|
81
89
|
}
|
|
82
90
|
|
|
83
|
-
|
|
91
|
+
readOnly(): boolean {
|
|
84
92
|
return this.rules.in === 'closed';
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
|
|
95
|
+
writeOnly(): boolean {
|
|
88
96
|
return this.rules.out === 'closed';
|
|
89
97
|
}
|
|
90
98
|
}
|
|
99
|
+
|
|
100
|
+
declare module '../../FougereFieldAxes.js' {
|
|
101
|
+
interface FougereFieldAxes {
|
|
102
|
+
readonly boundary?: BoundaryRef;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -33,8 +33,10 @@ Boundaries.decoders.register('isoDate', (value) => {
|
|
|
33
33
|
if (value instanceof Date) return { value };
|
|
34
34
|
if (typeof value === 'string') {
|
|
35
35
|
const date = new Date(value);
|
|
36
|
+
|
|
36
37
|
return Number.isNaN(date.getTime()) ? { message: 'Invalid date' } : { value: date };
|
|
37
38
|
}
|
|
39
|
+
|
|
38
40
|
return { message: 'Expected a date' };
|
|
39
41
|
});
|
|
40
42
|
Boundaries.encoders.register('isoDate', (value) =>
|
|
@@ -23,6 +23,11 @@ export class Lifecycle {
|
|
|
23
23
|
|
|
24
24
|
private constructor(private readonly rules: LifecycleRules = {}) {}
|
|
25
25
|
|
|
26
|
+
/** `optional()`, a default, a generator: the rule says the value need not arrive. */
|
|
27
|
+
static admitsAbsence(field: { lifecycle?: LifecycleRules }): boolean {
|
|
28
|
+
return !this.of(field).requiredAtCreate();
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
static of(field: { lifecycle?: LifecycleRules }): Lifecycle {
|
|
27
32
|
return new Lifecycle(field.lifecycle);
|
|
28
33
|
}
|
|
@@ -47,19 +52,19 @@ export class Lifecycle {
|
|
|
47
52
|
return { value: Generators.resolve(rule.generate)() };
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
requiredAtCreate(): boolean {
|
|
51
56
|
return this.create === undefined;
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
|
|
59
|
+
stampedAtCreate(): boolean {
|
|
55
60
|
return this.create === 'now';
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
|
|
63
|
+
immutable(): boolean {
|
|
59
64
|
return this.update === 'forbidden';
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
|
|
67
|
+
stampedOnUpdate(): boolean {
|
|
63
68
|
return this.update === 'now';
|
|
64
69
|
}
|
|
65
70
|
|
|
@@ -71,3 +76,9 @@ export class Lifecycle {
|
|
|
71
76
|
: undefined;
|
|
72
77
|
}
|
|
73
78
|
}
|
|
79
|
+
|
|
80
|
+
declare module '../../FougereFieldAxes.js' {
|
|
81
|
+
interface FougereFieldAxes {
|
|
82
|
+
readonly lifecycle?: LifecycleRules;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -39,7 +39,7 @@ export function applyUpdate(
|
|
|
39
39
|
const instant = Date.now();
|
|
40
40
|
|
|
41
41
|
for (const [name, field] of Object.entries(fields) as [string, Field][]) {
|
|
42
|
-
if (Lifecycle.of(field).stampedOnUpdate && !(name in values))
|
|
42
|
+
if (Lifecycle.of(field).stampedOnUpdate() && !(name in values))
|
|
43
43
|
values[name] = new Date(instant);
|
|
44
44
|
}
|
|
45
45
|
|
package/src/axis/role/Role.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Relation } from './Relation.js';
|
|
2
2
|
import type { EntityConstructor } from './EntityConstructor.js';
|
|
3
3
|
import { isObject } from '../../lib/utils.js';
|
|
4
|
+
import { Shapes, type Shape } from '../shape/Shape.js';
|
|
5
|
+
import type { ValidationError } from '../../lib/ValidationError.js';
|
|
4
6
|
import { Format, type Accepted } from '../../lib/Format.js';
|
|
5
7
|
import { ON_DELETE, RELATION_KINDS } from './Relation.js';
|
|
6
8
|
|
|
@@ -20,42 +22,66 @@ export class Role {
|
|
|
20
22
|
)
|
|
21
23
|
.closed();
|
|
22
24
|
|
|
23
|
-
static refusals(value: unknown) {
|
|
25
|
+
static refusals(value: unknown, declaration: Record<string, unknown> = {}): ValidationError[] {
|
|
26
|
+
const role = Role.of({ role: isObject(value) ? (value as RoleRules) : undefined });
|
|
27
|
+
const errors: ValidationError[] = [];
|
|
24
28
|
const relation = isObject(value) ? value.relation : undefined;
|
|
25
|
-
if (!isObject(relation) || !('to' in relation) || typeof relation.to === 'function') return [];
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
if (isObject(relation) && 'to' in relation && typeof relation.to !== 'function')
|
|
31
|
+
errors.push({
|
|
32
|
+
path: ['role', 'relation', 'to'],
|
|
33
|
+
message: 'Expected a function returning the target entity, such as () => Post',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (role.isPrimary() && Shapes.isNullable(declaration.shape as Shape))
|
|
37
|
+
errors.push({
|
|
38
|
+
path: ['role', 'primary'],
|
|
39
|
+
message: 'A primary key admits no null — a row is addressed by it',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (role.isCollection() && (role.isUnique() || role.isIndexed()))
|
|
43
|
+
errors.push({
|
|
44
|
+
path: ['role', role.isUnique() ? 'unique' : 'index'],
|
|
45
|
+
message: 'A collection has no column of its own, so there is nothing to index',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return errors;
|
|
28
49
|
}
|
|
29
50
|
|
|
30
51
|
private constructor(private readonly rules: RoleRules = {}) {}
|
|
31
52
|
|
|
53
|
+
/** A collection has no column: the far side of the relation carries the key. */
|
|
54
|
+
static admitsAbsence(field: { role?: RoleRules }): boolean {
|
|
55
|
+
return this.of(field).isCollection();
|
|
56
|
+
}
|
|
57
|
+
|
|
32
58
|
static of(field: { role?: RoleRules }): Role {
|
|
33
59
|
return new Role(field.role);
|
|
34
60
|
}
|
|
35
61
|
|
|
36
|
-
|
|
62
|
+
isPrimary(): boolean {
|
|
37
63
|
return this.rules.primary === true;
|
|
38
64
|
}
|
|
39
65
|
|
|
40
|
-
|
|
66
|
+
isIndexed(): boolean {
|
|
41
67
|
return this.rules.index === true;
|
|
42
68
|
}
|
|
43
69
|
|
|
44
70
|
/** A field unique on its own. A group spanning several belongs to the schema. */
|
|
45
|
-
|
|
71
|
+
isUnique(): boolean {
|
|
46
72
|
return this.rules.unique === true;
|
|
47
73
|
}
|
|
48
74
|
|
|
49
|
-
|
|
75
|
+
isCollection(): boolean {
|
|
50
76
|
return this.rules.relation?.kind === 'many';
|
|
51
77
|
}
|
|
52
78
|
|
|
53
|
-
|
|
79
|
+
isReference(): boolean {
|
|
54
80
|
return this.rules.relation?.kind === 'one';
|
|
55
81
|
}
|
|
56
82
|
|
|
57
83
|
/** Either kind — what a CLI flag and a GraphQL selection both leave out. */
|
|
58
|
-
|
|
84
|
+
isRelation(): boolean {
|
|
59
85
|
return this.rules.relation !== undefined;
|
|
60
86
|
}
|
|
61
87
|
|
|
@@ -70,3 +96,9 @@ export class Role {
|
|
|
70
96
|
}
|
|
71
97
|
|
|
72
98
|
export type RoleRules = Accepted<typeof Role.format>;
|
|
99
|
+
|
|
100
|
+
declare module '../../FougereFieldAxes.js' {
|
|
101
|
+
interface FougereFieldAxes {
|
|
102
|
+
readonly role?: RoleRules;
|
|
103
|
+
}
|
|
104
|
+
}
|