@fougere/schema 0.10.0-alpha.0 → 0.12.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 +4 -2
- package/dist/Schema.d.ts +1 -1
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +4 -3
- package/dist/Schema.js.map +1 -1
- package/dist/SchemaConstructor.d.ts +3 -4
- package/dist/SchemaConstructor.d.ts.map +1 -1
- package/dist/SchemaDefinition.d.ts.map +1 -1
- package/dist/SchemaDefinition.js +2 -1
- package/dist/SchemaDefinition.js.map +1 -1
- package/dist/SchemaError.d.ts +4 -0
- package/dist/SchemaError.d.ts.map +1 -0
- package/dist/SchemaError.js +7 -0
- package/dist/SchemaError.js.map +1 -0
- package/dist/SchemaView.d.ts +1 -1
- package/dist/SchemaView.d.ts.map +1 -1
- package/dist/axis/boundary/Boundary.d.ts.map +1 -1
- package/dist/axis/boundary/Boundary.js +3 -2
- package/dist/axis/boundary/Boundary.js.map +1 -1
- package/dist/axis/boundary/Decoder.d.ts +2 -5
- package/dist/axis/boundary/Decoder.d.ts.map +1 -1
- package/dist/axis/boundary/Decoder.js +2 -2
- package/dist/axis/boundary/Decoder.js.map +1 -1
- package/dist/axis/lifecycle/Clock.d.ts +4 -12
- package/dist/axis/lifecycle/Clock.d.ts.map +1 -1
- package/dist/axis/lifecycle/Clock.js +7 -13
- package/dist/axis/lifecycle/Clock.js.map +1 -1
- package/dist/axis/lifecycle/Generators.d.ts +5 -5
- package/dist/axis/lifecycle/Generators.d.ts.map +1 -1
- package/dist/axis/lifecycle/Generators.js +5 -15
- package/dist/axis/lifecycle/Generators.js.map +1 -1
- package/dist/axis/lifecycle/Lifecycle.d.ts +3 -3
- package/dist/axis/lifecycle/Lifecycle.d.ts.map +1 -1
- package/dist/axis/lifecycle/Lifecycle.js +11 -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 +3 -19
- package/dist/axis/lifecycle/apply.js.map +1 -1
- package/dist/axis/role/Relation.d.ts +4 -2
- package/dist/axis/role/Relation.d.ts.map +1 -1
- package/dist/axis/role/Relation.js +2 -2
- package/dist/axis/role/Relation.js.map +1 -1
- package/dist/axis/shape/Shape.d.ts +2 -2
- package/dist/axis/shape/Shape.d.ts.map +1 -1
- package/dist/axis/shape/Shape.js +2 -2
- package/dist/axis/shape/Shape.js.map +1 -1
- package/dist/entity/EntityAdapterSet.d.ts.map +1 -1
- package/dist/entity/EntityAdapterSet.js +3 -2
- package/dist/entity/EntityAdapterSet.js.map +1 -1
- package/dist/entity.d.ts +0 -1
- package/dist/entity.d.ts.map +1 -1
- package/dist/entity.js +0 -1
- package/dist/entity.js.map +1 -1
- package/dist/field/Field.d.ts.map +1 -1
- package/dist/field/Field.js +5 -4
- package/dist/field/Field.js.map +1 -1
- package/dist/field/FieldSet.d.ts.map +1 -1
- package/dist/field/FieldSet.js +5 -3
- package/dist/field/FieldSet.js.map +1 -1
- package/dist/field/Values.d.ts +6 -0
- package/dist/field/Values.d.ts.map +1 -0
- package/dist/field/Values.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/Registry.d.ts +1 -2
- package/dist/lib/Registry.d.ts.map +1 -1
- package/dist/lib/Registry.js +4 -4
- package/dist/lib/Registry.js.map +1 -1
- package/dist/lib/Verdict.d.ts +12 -0
- package/dist/lib/Verdict.d.ts.map +1 -0
- package/dist/lib/Verdict.js +2 -0
- package/dist/lib/Verdict.js.map +1 -0
- package/dist/projection/Cases.js +52 -54
- package/dist/projection/Cases.js.map +1 -1
- package/dist/projection/card/Bundle.d.ts.map +1 -1
- package/dist/projection/card/Bundle.js +2 -1
- package/dist/projection/card/Bundle.js.map +1 -1
- package/dist/projection/card/Card.d.ts +1 -1
- package/dist/projection/card/Card.d.ts.map +1 -1
- package/dist/projection/card/Card.js +7 -2
- package/dist/projection/card/Card.js.map +1 -1
- package/dist/projection/card/Diff.d.ts.map +1 -1
- package/dist/projection/card/Diff.js +34 -37
- package/dist/projection/card/Diff.js.map +1 -1
- package/dist/projection/card/admission.d.ts.map +1 -1
- package/dist/projection/card/admission.js +2 -1
- package/dist/projection/card/admission.js.map +1 -1
- package/dist/validator/AdapterFieldValidator.d.ts.map +1 -1
- package/dist/validator/AdapterFieldValidator.js +3 -2
- package/dist/validator/AdapterFieldValidator.js.map +1 -1
- package/dist/validator/FieldValueValidator.d.ts +3 -3
- package/dist/validator/FieldValueValidator.d.ts.map +1 -1
- package/dist/validator/FieldValueValidator.js +12 -11
- package/dist/validator/FieldValueValidator.js.map +1 -1
- package/dist/validator/InputValidator.d.ts +3 -0
- package/dist/validator/InputValidator.d.ts.map +1 -1
- package/dist/validator/InputValidator.js +28 -31
- package/dist/validator/InputValidator.js.map +1 -1
- package/dist/vocabulary/primitive/list.d.ts.map +1 -1
- package/dist/vocabulary/primitive/list.js +2 -1
- package/dist/vocabulary/primitive/list.js.map +1 -1
- package/dist/vocabulary/primitive/ref.d.ts +6 -1
- 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/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary/vocabulary.js +2 -1
- package/dist/vocabulary/vocabulary.js.map +1 -1
- package/package.json +1 -1
- package/src/Schema.ts +6 -4
- package/src/SchemaConstructor.ts +4 -4
- package/src/SchemaDefinition.ts +2 -1
- package/src/SchemaError.ts +7 -0
- package/src/SchemaView.ts +1 -1
- package/src/axis/boundary/Boundary.ts +3 -2
- package/src/axis/boundary/Decoder.ts +4 -3
- package/src/axis/lifecycle/Clock.ts +9 -13
- package/src/axis/lifecycle/Generators.ts +6 -18
- package/src/axis/lifecycle/Lifecycle.ts +14 -9
- package/src/axis/lifecycle/apply.ts +2 -16
- package/src/axis/role/Relation.ts +6 -3
- package/src/axis/shape/Shape.ts +3 -3
- package/src/entity/EntityAdapterSet.ts +3 -2
- package/src/entity.ts +0 -1
- package/src/field/Field.ts +5 -4
- package/src/field/FieldSet.ts +5 -3
- package/src/{Values.ts → field/Values.ts} +2 -2
- package/src/index.ts +3 -0
- package/src/lib/Registry.ts +5 -4
- package/src/lib/Verdict.ts +10 -0
- package/src/projection/Cases.ts +61 -57
- package/src/projection/card/Bundle.ts +2 -1
- package/src/projection/card/Card.ts +12 -4
- package/src/projection/card/Diff.ts +42 -42
- package/src/projection/card/admission.ts +2 -1
- package/src/validator/AdapterFieldValidator.ts +3 -2
- package/src/validator/FieldValueValidator.ts +15 -14
- package/src/validator/InputValidator.ts +30 -32
- package/src/vocabulary/primitive/list.ts +2 -1
- package/src/vocabulary/primitive/ref.ts +7 -3
- package/src/vocabulary/vocabulary.ts +2 -1
- package/dist/PartialValues.d.ts +0 -4
- package/dist/PartialValues.d.ts.map +0 -1
- package/dist/PartialValues.js +0 -2
- package/dist/PartialValues.js.map +0 -1
- package/dist/Values.d.ts +0 -6
- package/dist/Values.d.ts.map +0 -1
- package/dist/Values.js.map +0 -1
- package/dist/axis/boundary/Boundaries.d.ts +0 -17
- package/dist/axis/boundary/Boundaries.d.ts.map +0 -1
- package/dist/axis/boundary/Boundaries.js +0 -25
- package/dist/axis/boundary/Boundaries.js.map +0 -1
- package/dist/lib/Checked.d.ts +0 -11
- package/dist/lib/Checked.d.ts.map +0 -1
- package/dist/lib/Checked.js +0 -2
- package/dist/lib/Checked.js.map +0 -1
- package/dist/projection/standard.d.ts +0 -14
- package/dist/projection/standard.d.ts.map +0 -1
- package/dist/projection/standard.js +0 -2
- package/dist/projection/standard.js.map +0 -1
- package/src/PartialValues.ts +0 -4
- package/src/axis/boundary/Boundaries.ts +0 -36
- package/src/lib/Checked.ts +0 -5
- package/src/projection/standard.ts +0 -13
- /package/dist/{Values.js → field/Values.js} +0 -0
|
@@ -3,7 +3,7 @@ import { Field } from '../../field/Field.js';
|
|
|
3
3
|
export function ref(target, opts) {
|
|
4
4
|
return new Field({
|
|
5
5
|
shape: { type: 'string' },
|
|
6
|
-
role: { relation: Relation.one(target, opts?.
|
|
6
|
+
role: { relation: Relation.one(target, opts?.onDelete) },
|
|
7
7
|
});
|
|
8
8
|
}
|
|
9
9
|
//# sourceMappingURL=ref.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ref.js","sourceRoot":"","sources":["../../../src/vocabulary/primitive/ref.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,
|
|
1
|
+
{"version":3,"file":"ref.js","sourceRoot":"","sources":["../../../src/vocabulary/primitive/ref.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAiB,MAAM,6BAA6B,CAAC;AACtE,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAU7C,MAAM,UAAU,GAAG,CACjB,MAAqB,EACrB,IAAiB;IAEjB,OAAO,IAAI,KAAK,CAAS;QACvB,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QACzB,IAAI,EAAE,EAAE,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE;KACzD,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vocabulary.d.ts","sourceRoot":"","sources":["../../src/vocabulary/vocabulary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"vocabulary.d.ts","sourceRoot":"","sources":["../../src/vocabulary/vocabulary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAO1C;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,KAAK,CAAC,GACvC,SAAS,CAEX;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,CAAC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EXTENSION_SLOTS } from '../axis/Axis.js';
|
|
2
2
|
import { dequal } from 'dequal';
|
|
3
|
+
import { SchemaError } from '../SchemaError.js';
|
|
3
4
|
const MEMBER_SLOTS = [...EXTENSION_SLOTS, 'meta'];
|
|
4
5
|
/**
|
|
5
6
|
* Builds a `rule/` word, which states members on the field it receives.
|
|
@@ -31,7 +32,7 @@ function merge(name, field, given) {
|
|
|
31
32
|
const previous = already?.[member];
|
|
32
33
|
if (previous === undefined || dequal(previous, value))
|
|
33
34
|
continue;
|
|
34
|
-
throw new
|
|
35
|
+
throw new SchemaError(`vocabulary: \`${name}\` states ${slot}.${member} = ${JSON.stringify(value)}, but the ` +
|
|
35
36
|
`field already states ${JSON.stringify(previous)}. Apply one or the other, not both.`);
|
|
36
37
|
}
|
|
37
38
|
merged[slot] = { ...already, ...members };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vocabulary.js","sourceRoot":"","sources":["../../src/vocabulary/vocabulary.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"vocabulary.js","sourceRoot":"","sources":["../../src/vocabulary/vocabulary.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,YAAY,GAAG,CAAC,GAAG,eAAe,EAAE,MAAM,CAAU,CAAC;AAE3D;;;;GAIG;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,KAAqB;IAC9D,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,IAAI,OAAO,IAAI,KAAK;QAAE,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAEjD,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5B,IAAI,OAAO,KAAK,SAAS;YAAE,SAAS;QACpC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAwC,CAAC;QACnE,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YACnF,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,GAAG,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC;YACnC,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;IACD,OAAO,MAAwB,CAAC;AAClC,CAAC"}
|
package/package.json
CHANGED
package/src/Schema.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
|
|
1
3
|
import { Boundary } from './axis/boundary/Boundary.js';
|
|
2
4
|
import { type Fields } from './field/Fields.js';
|
|
3
5
|
import { type CompositeUnique } from './entity/CompositeUnique.js';
|
|
@@ -9,11 +11,11 @@ import { SchemaDerivation } from './SchemaDerivation.js';
|
|
|
9
11
|
import { type SchemaConstraints } from './SchemaConstraints.js';
|
|
10
12
|
import { SchemaDefinition } from './SchemaDefinition.js';
|
|
11
13
|
import { type ValidateOptions } from './validator/ValidateOptions.js';
|
|
12
|
-
import type { StandardSchemaV1 } from './projection/standard.js';
|
|
13
14
|
|
|
14
15
|
import type { SchemaView } from './SchemaView.js';
|
|
15
16
|
|
|
16
17
|
import type { SchemaConstructor } from './SchemaConstructor.js';
|
|
18
|
+
import { SchemaError } from './SchemaError.js';
|
|
17
19
|
|
|
18
20
|
export const ANONYMOUS_SCHEMA_NAME = 'Schema';
|
|
19
21
|
|
|
@@ -74,7 +76,7 @@ export class Schema {
|
|
|
74
76
|
continue;
|
|
75
77
|
}
|
|
76
78
|
const decoded = Boundary.of(field).decode(value);
|
|
77
|
-
row[key] = '
|
|
79
|
+
row[key] = 'message' in decoded ? value : decoded.value;
|
|
78
80
|
}
|
|
79
81
|
return row;
|
|
80
82
|
}
|
|
@@ -128,10 +130,10 @@ export class Schema {
|
|
|
128
130
|
|
|
129
131
|
static named(name: string) {
|
|
130
132
|
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) {
|
|
131
|
-
throw new
|
|
133
|
+
throw new SchemaError(`named(): \`${name}\` is not a valid class name.`);
|
|
132
134
|
}
|
|
133
135
|
if (this.name !== ANONYMOUS_SCHEMA_NAME) {
|
|
134
|
-
throw new
|
|
136
|
+
throw new SchemaError(
|
|
135
137
|
`named(): \`${this.name}\` is already named by its class declaration.`,
|
|
136
138
|
);
|
|
137
139
|
}
|
package/src/SchemaConstructor.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
|
|
1
3
|
import { type Fields } from './field/Fields.js';
|
|
2
4
|
|
|
3
5
|
import { type EntityDeclarations } from './entity/EntityDeclarations.js';
|
|
4
6
|
import { type PreviousNames } from './entity/PreviousNames.js';
|
|
5
7
|
import { SchemaDerivation } from './SchemaDerivation.js';
|
|
6
|
-
import type { StandardSchemaV1 } from './projection/standard.js';
|
|
7
|
-
import type { PartialValues } from './PartialValues.js';
|
|
8
8
|
import type { SchemaView } from './SchemaView.js';
|
|
9
|
-
import type { Values } from './Values.js';
|
|
9
|
+
import type { Values } from './field/Values.js';
|
|
10
10
|
|
|
11
11
|
export interface SchemaConstructor<TFields extends Fields> extends SchemaView<TFields> {
|
|
12
|
-
new (data:
|
|
12
|
+
new (data: Partial<Values<TFields>>): Values<TFields>;
|
|
13
13
|
readonly '~standard': StandardSchemaV1.Props<Record<string, unknown>, Values<TFields>>;
|
|
14
14
|
readonly derivation?: SchemaDerivation;
|
|
15
15
|
readonly previous?: PreviousNames<TFields>;
|
package/src/SchemaDefinition.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { SchemaDerivation } from './SchemaDerivation.js';
|
|
|
9
9
|
import { type ValidateOptions } from './validator/ValidateOptions.js';
|
|
10
10
|
import type { SchemaView } from './SchemaView.js';
|
|
11
11
|
import { SchemaConstraints } from './SchemaConstraints.js';
|
|
12
|
+
import { SchemaError } from './SchemaError.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Everything a definition is made of, every member required.
|
|
@@ -225,7 +226,7 @@ export class SchemaDefinition {
|
|
|
225
226
|
const strangers = keys.filter((key) => !Object.hasOwn(this.fields, key));
|
|
226
227
|
if (strangers.length === 0) return;
|
|
227
228
|
|
|
228
|
-
throw new
|
|
229
|
+
throw new SchemaError(
|
|
229
230
|
`${operation}(): unknown field ${strangers.map((s) => `\`${s}\``).join(', ')}. ` +
|
|
230
231
|
`This schema carries ${Object.keys(this.fields).join(', ')}.`,
|
|
231
232
|
);
|
package/src/SchemaView.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { EntityAdapters } from './entity/EntityAdapters.js';
|
|
|
5
5
|
import type { ValidationResult } from './lib/ValidationResult.js';
|
|
6
6
|
import type { ValidateOptions } from './validator/ValidateOptions.js';
|
|
7
7
|
import type { SchemaDerivation } from './SchemaDerivation.js';
|
|
8
|
-
import type { Values } from './Values.js';
|
|
8
|
+
import type { Values } from './field/Values.js';
|
|
9
9
|
|
|
10
10
|
export interface SchemaView<TFields extends Fields = Fields> {
|
|
11
11
|
readonly name: string;
|
|
@@ -5,6 +5,7 @@ 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
7
|
import type { BoundaryRules } from './BoundaryRules.js';
|
|
8
|
+
import { SchemaError } from '../../SchemaError.js';
|
|
8
9
|
|
|
9
10
|
const identityDecoder: Decoder = (value) => ({ value });
|
|
10
11
|
|
|
@@ -33,7 +34,7 @@ export class Boundary {
|
|
|
33
34
|
if (typeof ref !== 'string') return new Boundary(ref);
|
|
34
35
|
|
|
35
36
|
const alias = Boundaries.aliases.find(ref);
|
|
36
|
-
if (!alias) throw new
|
|
37
|
+
if (!alias) throw new SchemaError(`Unknown boundary alias: '${ref}'`);
|
|
37
38
|
return new Boundary(alias);
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -58,7 +59,7 @@ export class Boundary {
|
|
|
58
59
|
|
|
59
60
|
/** `date-time` means a `Date` on both sides, without a word in the entity. */
|
|
60
61
|
static forShape(shape: Shape | undefined): Boundary {
|
|
61
|
-
if (Shapes.typeOf(shape) === 'date') return new Boundary(Boundaries.aliases.
|
|
62
|
+
if (Shapes.typeOf(shape) === 'date') return new Boundary(Boundaries.aliases.resolve('isoDate'));
|
|
62
63
|
return new Boundary();
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { BoundaryRules } from './BoundaryRules.js';
|
|
2
2
|
import { Registry } from '../../lib/Registry.js';
|
|
3
3
|
import type { Encoder } from './Encoder.js';
|
|
4
|
+
import type { Verdict } from '../../lib/Verdict.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Wire to domain, and it must ANSWER a value it already produced: two facades decode — the
|
|
7
8
|
* client one on what arrives, `StorageGuard` on what a handler writes — so a decoder that
|
|
8
9
|
* halves cents halves them twice and stores a hundredth.
|
|
9
10
|
*/
|
|
10
|
-
export type Decoder = (value: unknown) =>
|
|
11
|
+
export type Decoder = (value: unknown) => Verdict;
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* `decoders` for a value coming in, `encoders` for one going out, `aliases` for the word
|
|
@@ -32,9 +33,9 @@ Boundaries.decoders.register('isoDate', (value) => {
|
|
|
32
33
|
if (value instanceof Date) return { value };
|
|
33
34
|
if (typeof value === 'string') {
|
|
34
35
|
const date = new Date(value);
|
|
35
|
-
return Number.isNaN(date.getTime()) ? {
|
|
36
|
+
return Number.isNaN(date.getTime()) ? { message: 'Invalid date' } : { value: date };
|
|
36
37
|
}
|
|
37
|
-
return {
|
|
38
|
+
return { message: 'Expected a date' };
|
|
38
39
|
});
|
|
39
40
|
Boundaries.encoders.register('isoDate', (value) =>
|
|
40
41
|
value instanceof Date ? value.toISOString() : value,
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What "now" means, for the two functions that stamp it.
|
|
3
|
-
*
|
|
4
|
-
* The ONE place either of them reads the time, which is what makes it substitutable at
|
|
5
|
-
* all: `created()`, `updated()` and `create: 'now'` are realized through this and nowhere
|
|
6
|
-
* else, so a test that needs a stable instant sets it here instead of intercepting `Date`
|
|
7
|
-
* globally — where Rails had to build `travel_to` over the language.
|
|
8
|
-
*
|
|
9
|
-
* A value rather than a mock: nothing here is a channel, and a frozen clock is a fact
|
|
10
|
-
* about the run, not an interception of a call.
|
|
11
|
-
*/
|
|
12
1
|
export class Clock {
|
|
13
2
|
private static reading: () => number = Date.now;
|
|
14
3
|
|
|
@@ -16,11 +5,18 @@ export class Clock {
|
|
|
16
5
|
return this.reading();
|
|
17
6
|
}
|
|
18
7
|
|
|
19
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
*
|
|
10
|
+
* @returns function to restore the clock
|
|
11
|
+
*/
|
|
20
12
|
static freeze(at: number | Date): () => void {
|
|
21
13
|
const previous = this.reading;
|
|
22
14
|
const instant = at instanceof Date ? at.getTime() : at;
|
|
15
|
+
|
|
23
16
|
this.reading = () => instant;
|
|
24
|
-
|
|
17
|
+
|
|
18
|
+
return () => {
|
|
19
|
+
this.reading = previous;
|
|
20
|
+
};
|
|
25
21
|
}
|
|
26
22
|
}
|
|
@@ -2,28 +2,16 @@ import { createId } from '@paralleldrive/cuid2';
|
|
|
2
2
|
|
|
3
3
|
import { Registry } from '../../lib/Registry.js';
|
|
4
4
|
|
|
5
|
-
export type GeneratorRef = 'cuid2' |
|
|
6
|
-
|
|
7
|
-
const NANOID_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
8
|
-
|
|
9
|
-
function nanoid(): string {
|
|
10
|
-
const bytes = new Uint8Array(21);
|
|
11
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
12
|
-
return Array.from(bytes, (byte) => NANOID_ALPHABET[byte & 63]).join('');
|
|
13
|
-
}
|
|
5
|
+
export type GeneratorRef = 'cuid2' | (string & {});
|
|
14
6
|
|
|
15
7
|
/**
|
|
16
|
-
* Who answers a `generate:` name. `cuid2
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* `
|
|
8
|
+
* Who answers a `generate:` name. `cuid2` is registered like any other, so
|
|
9
|
+
* `Generators.resolve('uuid')` is refused here and names what the process holds.
|
|
10
|
+
* `Generators.register('uuid', () => globalThis.crypto.randomUUID())` →
|
|
11
|
+
* `create: { generate: 'uuid' }` now resolves
|
|
20
12
|
*/
|
|
21
13
|
export const Generators = new Registry<() => string>(
|
|
22
14
|
'generator',
|
|
23
15
|
'call Generators.register(name, fn)',
|
|
24
|
-
[
|
|
25
|
-
['cuid2', createId],
|
|
26
|
-
['uuid', () => globalThis.crypto.randomUUID()],
|
|
27
|
-
['nanoid', nanoid],
|
|
28
|
-
],
|
|
16
|
+
[['cuid2', createId]],
|
|
29
17
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Generators } from './Generators.js';
|
|
2
2
|
import type { LifecycleRules } from './LifecycleRules.js';
|
|
3
3
|
|
|
4
4
|
export class Lifecycle {
|
|
@@ -14,11 +14,22 @@ export class Lifecycle {
|
|
|
14
14
|
return new Lifecycle(field.lifecycle);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
bornWith(instant: number): { value: unknown } | undefined {
|
|
18
|
+
const rule = this.create;
|
|
19
|
+
|
|
20
|
+
if (rule === 'now') return { value: new Date(instant) };
|
|
21
|
+
|
|
22
|
+
if (typeof rule !== 'object') return undefined;
|
|
23
|
+
|
|
24
|
+
if ('value' in rule) return { value: structuredClone(rule.value) };
|
|
25
|
+
|
|
26
|
+
return { value: Generators.resolve(rule.generate)() };
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
get requiredAtCreate(): boolean {
|
|
18
30
|
return this.create === undefined;
|
|
19
31
|
}
|
|
20
32
|
|
|
21
|
-
/** The server fills it at create, so a client form never carries it. */
|
|
22
33
|
get stampedAtCreate(): boolean {
|
|
23
34
|
return this.create === 'now';
|
|
24
35
|
}
|
|
@@ -37,15 +48,9 @@ export class Lifecycle {
|
|
|
37
48
|
|
|
38
49
|
get literal(): { value: unknown } | undefined {
|
|
39
50
|
const rule = this.create;
|
|
51
|
+
|
|
40
52
|
return typeof rule === 'object' && rule !== null && 'value' in rule
|
|
41
53
|
? { value: (rule as { value: unknown }).value }
|
|
42
54
|
: undefined;
|
|
43
55
|
}
|
|
44
|
-
|
|
45
|
-
get generator(): GeneratorRef | undefined {
|
|
46
|
-
const rule = this.create;
|
|
47
|
-
return typeof rule === 'object' && rule !== null && 'generate' in rule
|
|
48
|
-
? (rule as { generate: GeneratorRef }).generate
|
|
49
|
-
: undefined;
|
|
50
|
-
}
|
|
51
56
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Lifecycle } from './Lifecycle.js';
|
|
2
|
-
import { Generators } from './Generators.js';
|
|
3
2
|
import { Clock } from './Clock.js';
|
|
4
3
|
import { type Field } from '../../field/Field.js';
|
|
5
4
|
import { type Fields } from '../../field/Fields.js';
|
|
@@ -15,27 +14,14 @@ export function applyCreate(fields: Fields, input: Record<string, unknown>): Rec
|
|
|
15
14
|
|
|
16
15
|
for (const [name, field] of Object.entries(fields) as [string, Field][]) {
|
|
17
16
|
if (name in values) continue;
|
|
18
|
-
const
|
|
17
|
+
const born = Lifecycle.of(field).bornWith(instant);
|
|
19
18
|
|
|
20
|
-
if (
|
|
21
|
-
else if (rule.literal) values[name] = freshValue(rule.literal.value);
|
|
22
|
-
else if (rule.generator) values[name] = Generators.resolve(rule.generator)();
|
|
19
|
+
if (born) values[name] = born.value;
|
|
23
20
|
}
|
|
24
21
|
|
|
25
22
|
return values;
|
|
26
23
|
}
|
|
27
24
|
|
|
28
|
-
/**
|
|
29
|
-
* Clones a declared default, so two rows born of `create: { value: [] }` hold two arrays.
|
|
30
|
-
* FR : clone un défaut déclaré, pour que deux lignes nées de `create: { value: [] }`
|
|
31
|
-
* tiennent deux tableaux.
|
|
32
|
-
* `create: { value: [] }` → each instance gets its own array
|
|
33
|
-
*/
|
|
34
|
-
function freshValue(value: unknown): unknown {
|
|
35
|
-
if (value === null || typeof value !== 'object') return value;
|
|
36
|
-
return structuredClone(value);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
25
|
/**
|
|
40
26
|
* The dual of `applyCreate` on a patch: only `update: 'now'` fields are touched.
|
|
41
27
|
* FR : le dual d'`applyCreate` sur une modification : seuls les champs `update: 'now'`
|
|
@@ -4,10 +4,13 @@ export const RELATION_KINDS = ['one', 'many'] as const;
|
|
|
4
4
|
|
|
5
5
|
export const ON_DELETE = ['cascade', 'restrict', 'set null'] as const;
|
|
6
6
|
|
|
7
|
+
/** What becomes of the rows that name a deleted one. */
|
|
8
|
+
export type OnDelete = (typeof ON_DELETE)[number];
|
|
9
|
+
|
|
7
10
|
export interface Relation {
|
|
8
11
|
to: () => EntityConstructor;
|
|
9
12
|
kind: (typeof RELATION_KINDS)[number];
|
|
10
|
-
onDelete?:
|
|
13
|
+
onDelete?: OnDelete;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
/**
|
|
@@ -19,8 +22,8 @@ export interface Relation {
|
|
|
19
22
|
*/
|
|
20
23
|
export const Relation = {
|
|
21
24
|
/** `() => Post` so two entities can point at each other. */
|
|
22
|
-
one(target: EntityConstructor | (() => EntityConstructor),
|
|
23
|
-
return { to: normalizeTarget(target), kind: 'one', onDelete
|
|
25
|
+
one(target: EntityConstructor | (() => EntityConstructor), onDelete?: OnDelete): Relation {
|
|
26
|
+
return { to: normalizeTarget(target), kind: 'one', ...(onDelete ? { onDelete } : {}) };
|
|
24
27
|
},
|
|
25
28
|
|
|
26
29
|
many(target: EntityConstructor | (() => EntityConstructor)): Relation {
|
package/src/axis/shape/Shape.ts
CHANGED
|
@@ -48,6 +48,9 @@ type _ShapeTypesAreTheStandardsLessNull = Assert<
|
|
|
48
48
|
>;
|
|
49
49
|
|
|
50
50
|
export class Shapes {
|
|
51
|
+
private static readonly cache = new WeakMap<object, ShapeParts>();
|
|
52
|
+
private static readonly none: ShapeParts = { base: undefined, nullable: false };
|
|
53
|
+
|
|
51
54
|
static is(value: unknown): value is Shape {
|
|
52
55
|
if (typeof value !== 'object' || value === null) return false;
|
|
53
56
|
const type = (value as Shape).type;
|
|
@@ -96,9 +99,6 @@ export class Shapes {
|
|
|
96
99
|
return nullable;
|
|
97
100
|
}
|
|
98
101
|
|
|
99
|
-
private static readonly cache = new WeakMap<object, ShapeParts>();
|
|
100
|
-
private static readonly none: ShapeParts = { base: undefined, nullable: false };
|
|
101
|
-
|
|
102
102
|
static of(shape?: Shape): ShapeParts {
|
|
103
103
|
if (!shape) return this.none;
|
|
104
104
|
let parts = this.cache.get(shape);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Fields } from '../field/Fields.js';
|
|
2
2
|
import { isObject } from '../lib/utils.js';
|
|
3
3
|
import type { EntityAdapters } from './EntityAdapters.js';
|
|
4
|
+
import { SchemaError } from '../SchemaError.js';
|
|
4
5
|
|
|
5
6
|
type AdapterConfiguration = Record<string, unknown>;
|
|
6
7
|
type AdapterConfigurations = Record<string, AdapterConfiguration>;
|
|
@@ -19,14 +20,14 @@ export class EntityAdapterSet {
|
|
|
19
20
|
if (!adapters) return new EntityAdapterSet({});
|
|
20
21
|
|
|
21
22
|
if (!isObject(adapters)) {
|
|
22
|
-
throw new
|
|
23
|
+
throw new SchemaError(
|
|
23
24
|
`adapters: expected an object keyed by adapter name, got ${typeof adapters}.`,
|
|
24
25
|
);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
for (const [adapter, fields] of Object.entries(adapters)) {
|
|
28
29
|
if (!isObject(fields)) {
|
|
29
|
-
throw new
|
|
30
|
+
throw new SchemaError(
|
|
30
31
|
`adapters.${adapter}: expected an object keyed by field name, got ${typeof fields}. ` +
|
|
31
32
|
`What an adapter is handed is addressed by the field it applies to.`,
|
|
32
33
|
);
|
package/src/entity.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { type SchemaConstructor } from './SchemaConstructor.js';
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The one call everything derives from: the fields, and what the entity states about them.
|
|
8
|
-
* FR : l'appel dont tout dérive : les champs, et ce que l'entité en dit.
|
|
9
8
|
* `class Post extends entity({ id: primary(), title: text() }, { unique: [['title']] }) {}`
|
|
10
9
|
*
|
|
11
10
|
* Documented: [entities](https://fougere.dev/docs/schema/entities).
|
package/src/field/Field.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { Axis } from '../axis/Axis.js';
|
|
|
7
7
|
import { FieldDeclarationValidator } from '../validator/FieldDeclarationValidator.js';
|
|
8
8
|
import { FieldValueValidator } from '../validator/FieldValueValidator.js';
|
|
9
9
|
import { dotted } from '../lib/ValidationResult.js';
|
|
10
|
+
import { SchemaError } from '../SchemaError.js';
|
|
10
11
|
|
|
11
12
|
type FieldDeclaration = Pick<Field, 'shape' | Axis['slot'] | 'meta'>;
|
|
12
13
|
|
|
@@ -23,7 +24,7 @@ export class Field<T = unknown> {
|
|
|
23
24
|
const verdict = FieldDeclarationValidator.of(init).verdict;
|
|
24
25
|
|
|
25
26
|
if (!verdict.success) {
|
|
26
|
-
throw new
|
|
27
|
+
throw new SchemaError(
|
|
27
28
|
`${key ? `Field '${key}': ` : ''}` +
|
|
28
29
|
verdict.errors.map((e) => `${dotted(e.path)}: ${e.message}`).join('; '),
|
|
29
30
|
);
|
|
@@ -38,10 +39,10 @@ export class Field<T = unknown> {
|
|
|
38
39
|
const create = this.lifecycle?.create;
|
|
39
40
|
if (typeof create === 'object' && create !== null && 'value' in create) {
|
|
40
41
|
const checked = FieldValueValidator.of(this).validate(create.value);
|
|
41
|
-
if ('
|
|
42
|
-
throw new
|
|
42
|
+
if ('message' in checked)
|
|
43
|
+
throw new SchemaError(
|
|
43
44
|
`${key ? `Field '${key}': ` : ''}the declared default ${JSON.stringify(create.value)} ` +
|
|
44
|
-
`is not a legal value for it — ${checked.
|
|
45
|
+
`is not a legal value for it — ${checked.message}.`,
|
|
45
46
|
);
|
|
46
47
|
}
|
|
47
48
|
}
|
package/src/field/FieldSet.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { CompositeUnique } from '../entity/CompositeUnique.js';
|
|
|
3
3
|
import { Field } from './Field.js';
|
|
4
4
|
import { type FieldName } from './FieldName.js';
|
|
5
5
|
import { type Fields } from './Fields.js';
|
|
6
|
+
import { SchemaError } from '../SchemaError.js';
|
|
6
7
|
|
|
7
8
|
export class FieldSet<TFields extends Fields = Fields> {
|
|
8
9
|
private constructor(private readonly fields: TFields) {}
|
|
@@ -28,14 +29,15 @@ export class FieldSet<TFields extends Fields = Fields> {
|
|
|
28
29
|
for (const group of unique ?? []) {
|
|
29
30
|
const missing = group.filter((key) => !Object.hasOwn(fields, key));
|
|
30
31
|
if (missing.length)
|
|
31
|
-
throw new
|
|
32
|
+
throw new SchemaError(
|
|
32
33
|
`unique: [${group.join(', ')}] names ` +
|
|
33
34
|
`${missing.map((key) => `'${key}'`).join(', ')}, which the entity does not declare.`,
|
|
34
35
|
);
|
|
35
36
|
|
|
36
37
|
if (group.length === 1) {
|
|
37
38
|
const key = group[0]!;
|
|
38
|
-
|
|
39
|
+
const alone = fields[key]!;
|
|
40
|
+
fields[key] = alone.with({ role: { ...alone.role, unique: true } });
|
|
39
41
|
continue;
|
|
40
42
|
}
|
|
41
43
|
composite.push([...group]);
|
|
@@ -51,7 +53,7 @@ export class FieldSet<TFields extends Fields = Fields> {
|
|
|
51
53
|
.map(([name]) => name);
|
|
52
54
|
|
|
53
55
|
if (primaries.length > 1) {
|
|
54
|
-
throw new
|
|
56
|
+
throw new SchemaError(
|
|
55
57
|
`FieldSet.primary: ${primaries.map((name) => JSON.stringify(name)).join(', ')} all declare ` +
|
|
56
58
|
'`primary`; a field set can have only one primary field.',
|
|
57
59
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Field } from './
|
|
2
|
-
import type { Fields } from './
|
|
1
|
+
import type { Field } from './Field.js';
|
|
2
|
+
import type { Fields } from './Fields.js';
|
|
3
3
|
|
|
4
4
|
export type Values<TFields extends Fields> = {
|
|
5
5
|
[K in keyof TFields]: TFields[K] extends Field<infer T> ? T : never;
|
package/src/index.ts
CHANGED
|
@@ -12,12 +12,15 @@ export { type Shape } from './axis/shape/Shape.js';
|
|
|
12
12
|
export { type ShapeType } from './axis/shape/ShapeType.js';
|
|
13
13
|
export { Shapes } from './axis/shape/Shape.js';
|
|
14
14
|
export { Role } from './axis/role/Role.js';
|
|
15
|
+
export { ON_DELETE, type OnDelete } from './axis/role/Relation.js';
|
|
15
16
|
export { type EntityConstructor } from './axis/role/EntityConstructor.js';
|
|
16
17
|
export { Lifecycle } from './axis/lifecycle/Lifecycle.js';
|
|
17
18
|
export { Boundary } from './axis/boundary/Boundary.js';
|
|
18
19
|
export { applyCreate, applyUpdate } from './axis/lifecycle/apply.js';
|
|
19
20
|
export { InputRefusal } from './validator/InputRefusal.js';
|
|
21
|
+
export { SchemaError } from './SchemaError.js';
|
|
20
22
|
export { type ValidationError } from './lib/ValidationError.js';
|
|
23
|
+
export { type Verdict } from './lib/Verdict.js';
|
|
21
24
|
export { dotted, type ValidationResult } from './lib/ValidationResult.js';
|
|
22
25
|
|
|
23
26
|
export { Card } from './projection/card/Card.js';
|
package/src/lib/Registry.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { SchemaError } from '../SchemaError.js';
|
|
2
|
+
|
|
1
3
|
export class Registry<T> {
|
|
2
4
|
private readonly entries: Map<string, T>;
|
|
3
5
|
|
|
4
|
-
/** `Unknown boundary decoder 'celsius' — call Boundaries.decoders.register(name, fn).` */
|
|
5
6
|
constructor(
|
|
6
7
|
private readonly label: string,
|
|
7
8
|
private readonly hint?: string,
|
|
8
|
-
|
|
9
|
+
entries?: Iterable<readonly [string, T]>,
|
|
9
10
|
) {
|
|
10
|
-
this.entries = new Map(
|
|
11
|
+
this.entries = new Map(entries);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
register(name: string, value: T): T {
|
|
@@ -25,7 +26,7 @@ export class Registry<T> {
|
|
|
25
26
|
|
|
26
27
|
if (found !== undefined) return found;
|
|
27
28
|
|
|
28
|
-
throw new
|
|
29
|
+
throw new SchemaError(
|
|
29
30
|
`${path ? `${path}: ` : ''}Unknown ${this.label} '${name}'${this.hint ? ` — ${this.hint}` : ''}. ` +
|
|
30
31
|
`This process answers ${this.names.join(', ') || 'nothing yet'}.`,
|
|
31
32
|
);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ValidationError } from './ValidationError.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A field's verdict — the value it admitted, or a refusal whose `path` says where INSIDE the
|
|
5
|
+
* value it happened. That path is optional here and required on a `ValidationError`: a field
|
|
6
|
+
* does not know its own name, so the key is prefixed by whoever iterates the fields.
|
|
7
|
+
*/
|
|
8
|
+
export type Verdict =
|
|
9
|
+
| { value: unknown }
|
|
10
|
+
| (Omit<ValidationError, 'path'> & { path?: ValidationError['path'] });
|