@aventara/core 1.0.0-rc.2 → 1.0.0-rc.4
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/CHANGELOG.md +175 -0
- package/ERRORS.md +9 -9
- package/README.md +72 -12
- package/dist/adapters/adapter-operation.d.ts +7 -0
- package/dist/compiler/contract.compiler.d.ts +6 -0
- package/dist/compiler/contract.compiler.js +73 -12
- package/dist/config/computed-fields.js +2 -9
- package/dist/config/config-diagnostic.guidance.js +3 -3
- package/dist/config/config.resolver.d.ts +14 -0
- package/dist/config/config.resolver.js +31 -0
- package/dist/config/config.validator.js +82 -0
- package/dist/config/exact-framework-config.d.ts +8 -1
- package/dist/config/framework-config.d.ts +26 -1
- package/dist/contracts/client-contract-structure.validator.d.ts +8 -0
- package/dist/contracts/client-contract-structure.validator.js +6 -1
- package/dist/contracts/field.d.ts +11 -0
- package/dist/framework/contract-view.d.ts +41 -10
- package/dist/framework/framework.factory.js +39 -11
- package/dist/framework/operation-facade.d.ts +24 -5
- package/dist/framework/operation-facade.js +2 -1
- package/dist/index.d.ts +4 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/operations/database-refusal.d.ts +1 -1
- package/dist/operations/database-refusal.js +1 -1
- package/dist/operations/operation-arguments.d.ts +1 -1
- package/dist/operations/operation-call.d.ts +9 -5
- package/dist/operations/operation-result.d.ts +5 -3
- package/dist/operations/operation-validator-core.js +66 -33
- package/dist/operations/validation-code.guidance.js +2 -2
- package/dist/policy/policy-admission.d.ts +22 -0
- package/dist/policy/policy-admission.js +400 -0
- package/dist/policy/policy-after-update.runtime.d.ts +29 -0
- package/dist/policy/policy-after-update.runtime.js +131 -0
- package/dist/policy/policy-condition.d.ts +15 -0
- package/dist/policy/policy-condition.js +36 -0
- package/dist/policy/policy-engine.d.ts +111 -0
- package/dist/policy/policy-engine.js +1 -0
- package/dist/policy/policy-nested-write.runtime.d.ts +38 -0
- package/dist/policy/policy-nested-write.runtime.js +449 -0
- package/dist/policy/policy-read.runtime.d.ts +83 -0
- package/dist/policy/policy-read.runtime.js +494 -0
- package/dist/policy/policy-request.d.ts +37 -0
- package/dist/policy/policy-request.js +52 -0
- package/dist/policy/policy-session.d.ts +83 -0
- package/dist/policy/policy-session.js +234 -0
- package/dist/policy/policy-startup.validator.d.ts +8 -0
- package/dist/policy/policy-startup.validator.js +106 -0
- package/dist/policy/policy-write.runtime.d.ts +30 -0
- package/dist/policy/policy-write.runtime.js +394 -0
- package/dist/prebuilt/core-version.generated.d.ts +2 -0
- package/dist/prebuilt/core-version.generated.js +1 -0
- package/dist/prebuilt/prebuilt-contracts.d.ts +40 -0
- package/dist/prebuilt/prebuilt-contracts.emitter.d.ts +16 -0
- package/dist/prebuilt/prebuilt-contracts.emitter.js +50 -0
- package/dist/prebuilt/prebuilt-contracts.js +1 -0
- package/dist/prebuilt/prebuilt-contracts.loader.d.ts +21 -0
- package/dist/prebuilt/prebuilt-contracts.loader.js +122 -0
- package/dist/prebuilt/prebuilt-inputs.fingerprint.d.ts +16 -0
- package/dist/prebuilt/prebuilt-inputs.fingerprint.js +192 -0
- package/dist/runtime/computed-read-runtime.d.ts +6 -1
- package/dist/runtime/computed-read-runtime.js +224 -45
- package/dist/runtime/context-executor.runtime.js +4 -2
- package/dist/runtime/execution-options.d.ts +8 -0
- package/dist/runtime/field-validator.runtime.d.ts +9 -0
- package/dist/runtime/field-validator.runtime.js +36 -2
- package/dist/runtime/framework-warning.emitter.d.ts +6 -0
- package/dist/runtime/framework-warning.emitter.js +8 -0
- package/dist/runtime/level-lifecycle.runtime.js +3 -2
- package/dist/runtime/operation-code.guidance.js +4 -4
- package/dist/runtime/operation-request.d.ts +7 -0
- package/dist/runtime/operation-runtime.d.ts +9 -1
- package/dist/runtime/operation-runtime.js +119 -11
- package/dist/runtime/readonly-value.d.ts +5 -1
- package/dist/runtime/readonly-value.js +67 -28
- package/dist/transactions/transaction-runtime.d.ts +4 -1
- package/dist/transactions/transaction-runtime.js +27 -5
- package/package.json +2 -2
|
@@ -335,7 +335,7 @@ type IncludeRelationProjectionEntry<C extends AvContract, K extends AvResourceKe
|
|
|
335
335
|
* One entry of a `select` argument, at one projection position. The default is
|
|
336
336
|
* the unrestricted position.
|
|
337
337
|
*/
|
|
338
|
-
export type AvProjectionEntry<C extends AvContract, K extends AvResourceKey<C>, Scope extends AvProjectionScope = UnrestrictedProjection, AllowCount extends boolean = false, Forms extends ScalarForms = UnstatedForms> = SelectableScalarKey<C, K, Scope> | SelectableRelationKey<C, K, Scope> | SelectRelationProjectionEntry<C, K, Scope, Forms> | (AllowCount extends true ? "$count" : never);
|
|
338
|
+
export type AvProjectionEntry<C extends AvContract, K extends AvResourceKey<C>, Scope extends AvProjectionScope = UnrestrictedProjection, AllowCount extends boolean = false, Forms extends ScalarForms = UnstatedForms> = SelectableScalarKey<C, K, Scope> | SelectableRelationKey<C, K, Scope> | SelectRelationProjectionEntry<C, K, Scope, Forms> | "$defaults" | (AllowCount extends true ? "$count" : never);
|
|
339
339
|
/** A `select` argument: the fields to return. */
|
|
340
340
|
export type AvSelectInput<C extends AvContract, K extends AvResourceKey<C>, Scope extends AvProjectionScope = UnrestrictedProjection, AllowCount extends boolean = false, Forms extends ScalarForms = UnstatedForms> = readonly AvProjectionEntry<C, K, Scope, AllowCount, Forms>[];
|
|
341
341
|
/** An `include` argument: the relations to return with each row. */
|
|
@@ -55,8 +55,10 @@ export type AvOperationGrammar<C extends AvContract> = {
|
|
|
55
55
|
export type AvOperationSlot<I extends AvOperationIdentity, Result> = I["variant"] extends "first" ? Result | null : Result;
|
|
56
56
|
/** What a direct call resolves: the slot, every failure thrown. */
|
|
57
57
|
type Settled<I extends AvOperationIdentity, Result> = AvOperationSlot<I, Result>;
|
|
58
|
-
|
|
59
|
-
type
|
|
58
|
+
/** A direct call that projects: generic in its `select`/`include` literal. */
|
|
59
|
+
export type ProjectingCall<C extends AvContract, R extends AvResourceKey<C>, I extends AvOperationIdentity, Args, ResultForms extends ScalarForms, Options> = <const Select extends SelectOf<Args> = never, const Include extends IncludeOf<Args> = never>(...args: CallArgumentList<Omit<Args, ProjectionKey>, Omit<Args, ProjectionKey> & ProjectionArgument<Select, Include>, Options>) => Promise<Settled<I, AdmittedOperationResult<C, R, I, InferredProjection<Select, Include>, ResultForms>>>;
|
|
60
|
+
/** A direct call that does not project. */
|
|
61
|
+
export type PlainCall<C extends AvContract, R extends AvResourceKey<C>, I extends AvOperationIdentity, Args, ResultForms extends ScalarForms, Options> = (...args: CallArgumentList<Args, Args, Options>) => Promise<Settled<I, AdmittedOperationResult<C, R, I, unknown, ResultForms>>>;
|
|
60
62
|
/**
|
|
61
63
|
* One direct call: its arguments in `Forms`, its result in `ResultForms`,
|
|
62
64
|
* resolving the slot and throwing every failure, taking `Options` per call. It is generic in its
|
|
@@ -68,9 +70,11 @@ export type AvOperationCall<C extends AvContract, R extends AvResourceKey<C>, I
|
|
|
68
70
|
* `$ref` typed in `ResultForms` — the forms the source's final data is read in, so a
|
|
69
71
|
* reference carries what its source resolves.
|
|
70
72
|
*/
|
|
71
|
-
type DeferredArguments<C extends AvContract, R extends AvResourceKey<C>, I extends AvOperationIdentity, Forms extends ScalarForms, ResultForms extends ScalarForms> = ReferenceableArgumentsFor<C, R, I, Forms, ResultForms>;
|
|
72
|
-
|
|
73
|
-
type
|
|
73
|
+
export type DeferredArguments<C extends AvContract, R extends AvResourceKey<C>, I extends AvOperationIdentity, Forms extends ScalarForms, ResultForms extends ScalarForms> = ReferenceableArgumentsFor<C, R, I, Forms, ResultForms>;
|
|
74
|
+
/** A deferred operation that projects: generic in its `select`/`include` literal. */
|
|
75
|
+
export type DeferredProjectingCall<C extends AvContract, R extends AvResourceKey<C>, I extends AvOperationIdentity, Args, ResultForms extends ScalarForms, Origin extends HandleOrigin> = <const Select extends SelectOf<Args> = never, const Include extends IncludeOf<Args> = never>(...args: ArgumentList<Omit<Args, ProjectionKey>, Omit<Args, ProjectionKey> & ProjectionArgument<Select, Include>>) => AvOperation<AvOperationSlot<I, AdmittedOperationResult<C, R, I, InferredProjection<Select, Include>, ResultForms>>, Origin>;
|
|
76
|
+
/** A deferred operation that does not project. */
|
|
77
|
+
export type DeferredPlainCall<C extends AvContract, R extends AvResourceKey<C>, I extends AvOperationIdentity, Args, ResultForms extends ScalarForms, Origin extends HandleOrigin> = (...args: ArgumentList<Args, Args>) => AvOperation<AvOperationSlot<I, AdmittedOperationResult<C, R, I, unknown, ResultForms>>, Origin>;
|
|
74
78
|
/**
|
|
75
79
|
* One deferred operation: a handle typed `AvOperation<AvOperationSlot<…>, Origin>`,
|
|
76
80
|
* with `$ref` present exactly where the result is one row. A handle is not a
|
|
@@ -6,7 +6,8 @@ import type { AvOperationIdentity } from "./operation-identity.js";
|
|
|
6
6
|
type FieldOf<C extends AvContract, K extends AvResourceKey<C>, F extends AvFieldKey<C, K>> = C["resources"][K]["fields"][F];
|
|
7
7
|
/**
|
|
8
8
|
* The fields returned when no projection is given: the scalars whose `select`
|
|
9
|
-
* is exactly `true
|
|
9
|
+
* is exactly `true` and that the policy does not govern. A write-only field never
|
|
10
|
+
* appears, and a governed one appears only where a projection names it.
|
|
10
11
|
*/
|
|
11
12
|
type DefaultKey<C extends AvContract, K extends AvResourceKey<C>> = {
|
|
12
13
|
[F in AvFieldKey<C, K>]: FieldOf<C, K, F> extends {
|
|
@@ -14,6 +15,7 @@ type DefaultKey<C extends AvContract, K extends AvResourceKey<C>> = {
|
|
|
14
15
|
readonly capabilities: {
|
|
15
16
|
readonly select: true;
|
|
16
17
|
};
|
|
18
|
+
readonly governed?: never;
|
|
17
19
|
} ? F : never;
|
|
18
20
|
}[AvFieldKey<C, K>];
|
|
19
21
|
/** The field one projection entry names: a bare key, or a relation entry's key. */
|
|
@@ -40,7 +42,7 @@ type ProjectedFields<C extends AvContract, K extends AvResourceKey<C>, Keys exte
|
|
|
40
42
|
*/
|
|
41
43
|
type ProjectedRecord<C extends AvContract, K extends AvResourceKey<C>, Arguments, Forms extends ScalarForms> = [Arguments] extends [never] ? ProjectedFields<C, K, DefaultKey<C, K>, never, Forms> : [Arguments] extends [{
|
|
42
44
|
readonly select: readonly (infer Entry)[];
|
|
43
|
-
}] ? ProjectedFields<C, K, Extract<EntryKey<Entry>, AvFieldKey<C, K
|
|
45
|
+
}] ? ProjectedFields<C, K, Extract<EntryKey<Entry>, AvFieldKey<C, K>> | (Entry extends "$defaults" ? DefaultKey<C, K> : never), Entry, Forms> : [Arguments] extends [{
|
|
44
46
|
readonly include: readonly (infer Entry)[];
|
|
45
47
|
}] ? ProjectedFields<C, K, DefaultKey<C, K> | Extract<EntryKey<Entry>, AvFieldKey<C, K>>, Entry, Forms> : ProjectedFields<C, K, DefaultKey<C, K>, never, Forms>;
|
|
46
48
|
/**
|
|
@@ -50,7 +52,7 @@ type ProjectedRecord<C extends AvContract, K extends AvResourceKey<C>, Arguments
|
|
|
50
52
|
type CountEnvelope<C extends AvContract, Target extends AvResourceKey<C>, Entry, Forms extends ScalarForms> = [Exclude<Entry, "$count">] extends [never] ? {
|
|
51
53
|
readonly count: number;
|
|
52
54
|
} : {
|
|
53
|
-
readonly data: readonly ProjectedFields<C, Target, Extract<EntryKey<Entry>, AvFieldKey<C, Target
|
|
55
|
+
readonly data: readonly ProjectedFields<C, Target, Extract<EntryKey<Entry>, AvFieldKey<C, Target>> | (Entry extends "$defaults" ? DefaultKey<C, Target> : never), Exclude<Entry, "$count">, Forms>[];
|
|
54
56
|
readonly count: number;
|
|
55
57
|
};
|
|
56
58
|
/** The entries of an argument's own `select`, or `never` when it has none. */
|
|
@@ -304,6 +304,12 @@ const UNDEFINED_ARGUMENT_MESSAGES = {
|
|
|
304
304
|
offset: "offset must not be undefined: leave the key out to mean no offset.",
|
|
305
305
|
cursor: "cursor must not be undefined: leave the key out to mean no cursor.",
|
|
306
306
|
};
|
|
307
|
+
const EMPTY_BOOLEAN_NODE_MESSAGE = "A boolean node must hold a condition here: an empty one matches every row, and the write would reach more rows.";
|
|
308
|
+
function isEmptyBooleanNode(value) {
|
|
309
|
+
return Array.isArray(value)
|
|
310
|
+
? value.length === 0
|
|
311
|
+
: isRecord(value) && Object.keys(value).length === 0;
|
|
312
|
+
}
|
|
307
313
|
function validateWhere(contract, resourceName, where, path, state, depth) {
|
|
308
314
|
if (!checkDepth(contract, state, depth)) {
|
|
309
315
|
return;
|
|
@@ -322,6 +328,13 @@ function validateWhere(contract, resourceName, where, path, state, depth) {
|
|
|
322
328
|
return;
|
|
323
329
|
}
|
|
324
330
|
for (const [key, value] of Object.entries(where)) {
|
|
331
|
+
if (state.refuseEmptyNodes === true &&
|
|
332
|
+
(key === "AND" || key === "OR" || key === "NOT") &&
|
|
333
|
+
isEmptyBooleanNode(value)) {
|
|
334
|
+
countFilterNode(contract, state);
|
|
335
|
+
addIssue(state.issues, "V1001", [...path, key], EMPTY_BOOLEAN_NODE_MESSAGE);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
325
338
|
if (key === "AND" || key === "OR") {
|
|
326
339
|
countFilterNode(contract, state);
|
|
327
340
|
if (!Array.isArray(value)) {
|
|
@@ -329,6 +342,10 @@ function validateWhere(contract, resourceName, where, path, state, depth) {
|
|
|
329
342
|
continue;
|
|
330
343
|
}
|
|
331
344
|
value.forEach((entry, index) => {
|
|
345
|
+
if (state.refuseEmptyNodes === true && isEmptyBooleanNode(entry)) {
|
|
346
|
+
addIssue(state.issues, "V1001", [...path, key, index], EMPTY_BOOLEAN_NODE_MESSAGE);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
332
349
|
validateWhere(contract, resourceName, entry, [...path, key, index], state, depth + 1);
|
|
333
350
|
});
|
|
334
351
|
continue;
|
|
@@ -507,6 +524,12 @@ function validateProjection(contract, resourceName, mode, value, scope, path, is
|
|
|
507
524
|
}
|
|
508
525
|
value.forEach((entry, index) => {
|
|
509
526
|
const entryPath = [...path, index];
|
|
527
|
+
if (entry === "$defaults") {
|
|
528
|
+
if (mode !== "select") {
|
|
529
|
+
addIssue(issues, "V1008", entryPath, "$defaults is a select entry: include already returns the default fields.");
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
510
533
|
if (entry === "$count") {
|
|
511
534
|
if (mode !== "select" ||
|
|
512
535
|
reducerOwner?.edge.cardinality !== "many" ||
|
|
@@ -687,8 +710,8 @@ function validateRelationMutationEntry(contract, edge, directive, value, path, i
|
|
|
687
710
|
addIssue(issues, "V1012", path, "To-many $disconnect requires one or more unique identifiers.");
|
|
688
711
|
return;
|
|
689
712
|
}
|
|
690
|
-
for (const selector of
|
|
691
|
-
validateUniqueSelector(contract, target, selector,
|
|
713
|
+
for (const { entry: selector, at } of entriesOf(edge, value, path)) {
|
|
714
|
+
validateUniqueSelector(contract, target, selector, at, issues);
|
|
692
715
|
}
|
|
693
716
|
return;
|
|
694
717
|
}
|
|
@@ -703,84 +726,81 @@ function validateRelationMutationEntry(contract, edge, directive, value, path, i
|
|
|
703
726
|
addIssue(issues, "V1012", path, "To-many $set requires an identifier array.");
|
|
704
727
|
return;
|
|
705
728
|
}
|
|
706
|
-
for (const selector of value) {
|
|
707
|
-
validateUniqueSelector(contract, target, selector,
|
|
729
|
+
for (const { entry: selector, at } of entriesOf(edge, value, path)) {
|
|
730
|
+
validateUniqueSelector(contract, target, selector, at, issues);
|
|
708
731
|
}
|
|
709
732
|
return;
|
|
710
733
|
}
|
|
711
734
|
if (directive === "$connect") {
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
validateUniqueSelector(contract, target, selector, path, issues);
|
|
735
|
+
for (const { entry: selector, at } of entriesOf(edge, value, path)) {
|
|
736
|
+
validateUniqueSelector(contract, target, selector, at, issues);
|
|
715
737
|
}
|
|
716
738
|
return;
|
|
717
739
|
}
|
|
718
740
|
if (directive === "$create") {
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
validateMutationData(contract, target, entry, "create", NESTED_WRITE_SCOPE, path, issues, transactionReferences);
|
|
741
|
+
for (const { entry, at } of entriesOf(edge, value, path)) {
|
|
742
|
+
validateMutationData(contract, target, entry, "create", NESTED_WRITE_SCOPE, at, issues, transactionReferences);
|
|
722
743
|
}
|
|
723
744
|
return;
|
|
724
745
|
}
|
|
725
746
|
if (directive === "$connectOrCreate") {
|
|
726
|
-
const
|
|
727
|
-
for (const entry of entries) {
|
|
747
|
+
for (const { entry, at } of entriesOf(edge, value, path)) {
|
|
728
748
|
if (!isRecord(entry) || !("where" in entry) || !("create" in entry)) {
|
|
729
|
-
addIssue(issues, "V1012",
|
|
749
|
+
addIssue(issues, "V1012", at, "$connectOrCreate requires where and create.");
|
|
730
750
|
continue;
|
|
731
751
|
}
|
|
732
|
-
validateUniqueSelector(contract, target, entry.where, [...
|
|
733
|
-
validateMutationData(contract, target, entry.create, "create", NESTED_WRITE_SCOPE, [...
|
|
752
|
+
validateUniqueSelector(contract, target, entry.where, [...at, "where"], issues);
|
|
753
|
+
validateMutationData(contract, target, entry.create, "create", NESTED_WRITE_SCOPE, [...at, "create"], issues, transactionReferences);
|
|
734
754
|
}
|
|
735
755
|
return;
|
|
736
756
|
}
|
|
737
757
|
if (directive === "$update") {
|
|
738
|
-
const
|
|
739
|
-
for (const entry of entries) {
|
|
758
|
+
for (const { entry, at } of entriesOf(edge, value, path)) {
|
|
740
759
|
if (!isRecord(entry) || !("data" in entry)) {
|
|
741
|
-
addIssue(issues, "V1012",
|
|
760
|
+
addIssue(issues, "V1012", at, "$update requires a data object.");
|
|
742
761
|
continue;
|
|
743
762
|
}
|
|
744
763
|
if (edge.cardinality === "many" && !("where" in entry)) {
|
|
745
|
-
addIssue(issues, "V1012",
|
|
764
|
+
addIssue(issues, "V1012", at, "To-many $update requires where.");
|
|
746
765
|
continue;
|
|
747
766
|
}
|
|
748
767
|
if ("where" in entry) {
|
|
749
768
|
const state = {
|
|
750
769
|
issues,
|
|
751
|
-
structuralRoot: [...
|
|
770
|
+
structuralRoot: [...at, "where"],
|
|
752
771
|
transactionReferences,
|
|
753
772
|
scalarOnly: edge.capabilities.nestedWriteFilter === "scalar",
|
|
773
|
+
...(edge.cardinality === "many" ? { refuseEmptyNodes: true } : {}),
|
|
754
774
|
booleanFilterNodes: 0,
|
|
755
775
|
nestingExceeded: false,
|
|
756
776
|
booleanNodesExceeded: false,
|
|
757
777
|
};
|
|
758
|
-
validateWhere(contract, target, entry.where, [...
|
|
778
|
+
validateWhere(contract, target, entry.where, [...at, "where"], state, 1);
|
|
759
779
|
}
|
|
760
780
|
validateMutationData(contract, target, entry.data, "update", edge.cardinality === "many"
|
|
761
781
|
? TO_MANY_UPDATE_WRITE_SCOPE
|
|
762
|
-
: NESTED_WRITE_SCOPE, [...
|
|
782
|
+
: NESTED_WRITE_SCOPE, [...at, "data"], issues, transactionReferences);
|
|
763
783
|
}
|
|
764
784
|
return;
|
|
765
785
|
}
|
|
766
786
|
if (directive === "$delete") {
|
|
767
|
-
const
|
|
768
|
-
for (const entry of entries) {
|
|
787
|
+
for (const { entry, at } of entriesOf(edge, value, path)) {
|
|
769
788
|
if (edge.cardinality === "many") {
|
|
770
789
|
if (!isRecord(entry) || !("where" in entry)) {
|
|
771
|
-
addIssue(issues, "V1012",
|
|
790
|
+
addIssue(issues, "V1012", at, "To-many $delete requires where.");
|
|
772
791
|
continue;
|
|
773
792
|
}
|
|
774
793
|
const state = {
|
|
775
794
|
issues,
|
|
776
|
-
structuralRoot: [...
|
|
795
|
+
structuralRoot: [...at, "where"],
|
|
777
796
|
transactionReferences,
|
|
778
797
|
scalarOnly: edge.capabilities.nestedWriteFilter === "scalar",
|
|
798
|
+
refuseEmptyNodes: true,
|
|
779
799
|
booleanFilterNodes: 0,
|
|
780
800
|
nestingExceeded: false,
|
|
781
801
|
booleanNodesExceeded: false,
|
|
782
802
|
};
|
|
783
|
-
validateWhere(contract, target, entry.where, [...
|
|
803
|
+
validateWhere(contract, target, entry.where, [...at, "where"], state, 1);
|
|
784
804
|
}
|
|
785
805
|
else if (entry !== true &&
|
|
786
806
|
(!isRecord(entry) || Object.keys(entry).some((key) => key !== "where"))) {
|
|
@@ -802,23 +822,27 @@ function validateRelationMutationEntry(contract, edge, directive, value, path, i
|
|
|
802
822
|
return;
|
|
803
823
|
}
|
|
804
824
|
if (directive === "$upsert") {
|
|
805
|
-
const
|
|
806
|
-
for (const entry of entries) {
|
|
825
|
+
for (const { entry, at } of entriesOf(edge, value, path)) {
|
|
807
826
|
if (!isRecord(entry) ||
|
|
808
827
|
!("create" in entry) ||
|
|
809
828
|
!("update" in entry) ||
|
|
810
829
|
(edge.cardinality === "many" && !("where" in entry))) {
|
|
811
|
-
addIssue(issues, "V1012",
|
|
830
|
+
addIssue(issues, "V1012", at, "$upsert requires create/update and requires where on to-many relations.");
|
|
812
831
|
continue;
|
|
813
832
|
}
|
|
814
833
|
if ("where" in entry) {
|
|
815
|
-
validateUniqueSelector(contract, target, entry.where, [...
|
|
834
|
+
validateUniqueSelector(contract, target, entry.where, [...at, "where"], issues);
|
|
816
835
|
}
|
|
817
|
-
validateMutationData(contract, target, entry.create, "create", NESTED_WRITE_SCOPE, [...
|
|
818
|
-
validateMutationData(contract, target, entry.update, "update", NESTED_WRITE_SCOPE, [...
|
|
836
|
+
validateMutationData(contract, target, entry.create, "create", NESTED_WRITE_SCOPE, [...at, "create"], issues, transactionReferences);
|
|
837
|
+
validateMutationData(contract, target, entry.update, "update", NESTED_WRITE_SCOPE, [...at, "update"], issues, transactionReferences);
|
|
819
838
|
}
|
|
820
839
|
}
|
|
821
840
|
}
|
|
841
|
+
function entriesOf(edge, value, path) {
|
|
842
|
+
return edge.cardinality === "many" && Array.isArray(value)
|
|
843
|
+
? value.map((entry, index) => ({ entry, at: [...path, index] }))
|
|
844
|
+
: [{ entry: value, at: path }];
|
|
845
|
+
}
|
|
822
846
|
function validateRelationMutation(contract, fieldName, field, value, stage, scope, path, issues, transactionReferences) {
|
|
823
847
|
const directives = advertisedDirectivesAt(field.capabilities[stage], scope);
|
|
824
848
|
if (directives === undefined) {
|
|
@@ -896,6 +920,14 @@ function validateMutationArguments(contract, resourceName, request, args, issues
|
|
|
896
920
|
}
|
|
897
921
|
}
|
|
898
922
|
}
|
|
923
|
+
function emptyNodeRefusalOf(request) {
|
|
924
|
+
return (request.family === "update" || request.family === "delete") &&
|
|
925
|
+
(request.variant === "many" ||
|
|
926
|
+
request.variant === "count" ||
|
|
927
|
+
request.variant === "first")
|
|
928
|
+
? { refuseEmptyNodes: true }
|
|
929
|
+
: {};
|
|
930
|
+
}
|
|
899
931
|
function isUniqueOperation(request) {
|
|
900
932
|
return (request.variant === "unique" &&
|
|
901
933
|
(request.family === "find" ||
|
|
@@ -941,6 +973,7 @@ export function validateOperationArguments(contract, request, options) {
|
|
|
941
973
|
issues,
|
|
942
974
|
structuralRoot: ["arguments", "where"],
|
|
943
975
|
transactionReferences: options?.transactionReferences === true,
|
|
976
|
+
...emptyNodeRefusalOf(request),
|
|
944
977
|
booleanFilterNodes: 0,
|
|
945
978
|
nestingExceeded: false,
|
|
946
979
|
booleanNodesExceeded: false,
|
|
@@ -4,7 +4,7 @@ export const VALIDATION_CODE_GUIDANCE = {
|
|
|
4
4
|
fix: "Send the value at the issue's path. When the server must fill it, give the field a compute.create stage or a database default; a pipe cannot fill it.",
|
|
5
5
|
},
|
|
6
6
|
V1001: {
|
|
7
|
-
cause: "A value of the wrong type: a string where a number is expected, a negative limit, a non-integer id, null given to an operator with no null meaning (gt, contains, in), two directives in one scalar action object (`{ $increment: 1, $decrement: 2 }`), or a server-side value that is not plain data.",
|
|
7
|
+
cause: "A value of the wrong type: a string where a number is expected, a negative limit, a non-integer id, null given to an operator with no null meaning (gt, contains, in), two directives in one scalar action object (`{ $increment: 1, $decrement: 2 }`), or a server-side value that is not plain data. Also a value that would make the call reach more rows: `undefined` in a filter, a unique selector or a cursor, an empty boolean node (`NOT: {}`, `AND: []`, `OR: []`) in the where of a write (update or delete many, count or first, a to-many $update or $delete).",
|
|
8
8
|
fix: "Send the type the contract states at that path (the generated client's types say it).",
|
|
9
9
|
},
|
|
10
10
|
V1002: {
|
|
@@ -16,7 +16,7 @@ export const VALIDATION_CODE_GUIDANCE = {
|
|
|
16
16
|
fix: "Send the canonical encoding (ISO-8601 UTC datetime, decimal string, base64 bytes) and satisfy the validator that check and params name.",
|
|
17
17
|
},
|
|
18
18
|
V1004: {
|
|
19
|
-
cause: "A number, length or size outside its range: a failed bound, length or size validator, a BigInt outside signed 64 bits, an AvZ.int() outside the safe integer range, or a transaction with no operations.",
|
|
19
|
+
cause: "A number, length or size outside its range: a failed bound, length or size validator, a BigInt outside signed 64 bits, an AvZ.int() outside the safe integer range, or a transaction with no operations. Under A2004 it is also a value the database refused as too long or out of its range, a BigInt beyond what the data source's driver can bind among them (some bind at most ±(2^53 − 1)).",
|
|
20
20
|
fix: "Send a value inside the range; check and params name the validator that failed.",
|
|
21
21
|
},
|
|
22
22
|
V1005: {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AvContract, AvContractCore } from "../contracts/contract.js";
|
|
2
|
+
import type { AvOperationIdentity } from "../operations/operation-identity.js";
|
|
3
|
+
import type { AvOperationFailureResponse } from "../runtime/operation-response.js";
|
|
4
|
+
import { type PolicyAction, type PolicySession } from "./policy-session.js";
|
|
5
|
+
/** The refusal every policy decision answers: `A4002`. */
|
|
6
|
+
export declare function policyRefusal(message: string): AvOperationFailureResponse;
|
|
7
|
+
/** The actions an operation family asks the engine about. */
|
|
8
|
+
export declare function policyActionsOf(family: AvOperationIdentity["family"]): readonly PolicyAction[];
|
|
9
|
+
/**
|
|
10
|
+
* Policy admission for one request (see the module comment): the refusal, or
|
|
11
|
+
* `undefined` when the request may go on to its guards.
|
|
12
|
+
*/
|
|
13
|
+
export declare function admitByPolicy(session: PolicySession, core: AvContractCore, active: AvContract, request: AvOperationIdentity & {
|
|
14
|
+
readonly resource: string;
|
|
15
|
+
}, args: unknown): Promise<AvOperationFailureResponse | undefined>;
|
|
16
|
+
/**
|
|
17
|
+
* The write half of policy admission: every field the write sends against
|
|
18
|
+
* `fieldsOf(write)`, nested records included, the refusal or `undefined`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function admitWriteByPolicy(session: PolicySession, core: AvContractCore, request: AvOperationIdentity & {
|
|
21
|
+
readonly resource: string;
|
|
22
|
+
}, args: unknown): Promise<AvOperationFailureResponse | undefined>;
|