@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/ERRORS.md +9 -9
  3. package/README.md +72 -12
  4. package/dist/adapters/adapter-operation.d.ts +7 -0
  5. package/dist/compiler/contract.compiler.d.ts +6 -0
  6. package/dist/compiler/contract.compiler.js +73 -12
  7. package/dist/config/computed-fields.js +2 -9
  8. package/dist/config/config-diagnostic.guidance.js +3 -3
  9. package/dist/config/config.resolver.d.ts +14 -0
  10. package/dist/config/config.resolver.js +31 -0
  11. package/dist/config/config.validator.js +82 -0
  12. package/dist/config/exact-framework-config.d.ts +8 -1
  13. package/dist/config/framework-config.d.ts +26 -1
  14. package/dist/contracts/client-contract-structure.validator.d.ts +8 -0
  15. package/dist/contracts/client-contract-structure.validator.js +6 -1
  16. package/dist/contracts/field.d.ts +11 -0
  17. package/dist/framework/contract-view.d.ts +41 -10
  18. package/dist/framework/framework.factory.js +39 -11
  19. package/dist/framework/operation-facade.d.ts +24 -5
  20. package/dist/framework/operation-facade.js +2 -1
  21. package/dist/index.d.ts +4 -1
  22. package/dist/internal.d.ts +1 -0
  23. package/dist/internal.js +1 -0
  24. package/dist/operations/database-refusal.d.ts +1 -1
  25. package/dist/operations/database-refusal.js +1 -1
  26. package/dist/operations/operation-arguments.d.ts +1 -1
  27. package/dist/operations/operation-call.d.ts +9 -5
  28. package/dist/operations/operation-result.d.ts +5 -3
  29. package/dist/operations/operation-validator-core.js +66 -33
  30. package/dist/operations/validation-code.guidance.js +2 -2
  31. package/dist/policy/policy-admission.d.ts +22 -0
  32. package/dist/policy/policy-admission.js +400 -0
  33. package/dist/policy/policy-after-update.runtime.d.ts +29 -0
  34. package/dist/policy/policy-after-update.runtime.js +131 -0
  35. package/dist/policy/policy-condition.d.ts +15 -0
  36. package/dist/policy/policy-condition.js +36 -0
  37. package/dist/policy/policy-engine.d.ts +111 -0
  38. package/dist/policy/policy-engine.js +1 -0
  39. package/dist/policy/policy-nested-write.runtime.d.ts +38 -0
  40. package/dist/policy/policy-nested-write.runtime.js +449 -0
  41. package/dist/policy/policy-read.runtime.d.ts +83 -0
  42. package/dist/policy/policy-read.runtime.js +494 -0
  43. package/dist/policy/policy-request.d.ts +37 -0
  44. package/dist/policy/policy-request.js +52 -0
  45. package/dist/policy/policy-session.d.ts +83 -0
  46. package/dist/policy/policy-session.js +234 -0
  47. package/dist/policy/policy-startup.validator.d.ts +8 -0
  48. package/dist/policy/policy-startup.validator.js +106 -0
  49. package/dist/policy/policy-write.runtime.d.ts +30 -0
  50. package/dist/policy/policy-write.runtime.js +394 -0
  51. package/dist/prebuilt/core-version.generated.d.ts +2 -0
  52. package/dist/prebuilt/core-version.generated.js +1 -0
  53. package/dist/prebuilt/prebuilt-contracts.d.ts +40 -0
  54. package/dist/prebuilt/prebuilt-contracts.emitter.d.ts +16 -0
  55. package/dist/prebuilt/prebuilt-contracts.emitter.js +50 -0
  56. package/dist/prebuilt/prebuilt-contracts.js +1 -0
  57. package/dist/prebuilt/prebuilt-contracts.loader.d.ts +21 -0
  58. package/dist/prebuilt/prebuilt-contracts.loader.js +122 -0
  59. package/dist/prebuilt/prebuilt-inputs.fingerprint.d.ts +16 -0
  60. package/dist/prebuilt/prebuilt-inputs.fingerprint.js +192 -0
  61. package/dist/runtime/computed-read-runtime.d.ts +6 -1
  62. package/dist/runtime/computed-read-runtime.js +224 -45
  63. package/dist/runtime/context-executor.runtime.js +4 -2
  64. package/dist/runtime/execution-options.d.ts +8 -0
  65. package/dist/runtime/field-validator.runtime.d.ts +9 -0
  66. package/dist/runtime/field-validator.runtime.js +36 -2
  67. package/dist/runtime/framework-warning.emitter.d.ts +6 -0
  68. package/dist/runtime/framework-warning.emitter.js +8 -0
  69. package/dist/runtime/level-lifecycle.runtime.js +3 -2
  70. package/dist/runtime/operation-code.guidance.js +4 -4
  71. package/dist/runtime/operation-request.d.ts +7 -0
  72. package/dist/runtime/operation-runtime.d.ts +9 -1
  73. package/dist/runtime/operation-runtime.js +119 -11
  74. package/dist/runtime/readonly-value.d.ts +5 -1
  75. package/dist/runtime/readonly-value.js +67 -28
  76. package/dist/transactions/transaction-runtime.d.ts +4 -1
  77. package/dist/transactions/transaction-runtime.js +27 -5
  78. 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
- 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>>>;
59
- 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>>>;
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
- 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>;
73
- 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>;
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`. A write-only field never appears.
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>>, Entry, Forms> : [Arguments] extends [{
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>>, Exclude<Entry, "$count">, Forms>[];
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 selectors) {
691
- validateUniqueSelector(contract, target, selector, path, issues);
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, path, issues);
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 selectors = edge.cardinality === "many" && Array.isArray(value) ? value : [value];
713
- for (const selector of selectors) {
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 entries = edge.cardinality === "many" && Array.isArray(value) ? value : [value];
720
- for (const entry of entries) {
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 entries = edge.cardinality === "many" && Array.isArray(value) ? value : [value];
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", path, "$connectOrCreate requires where and create.");
749
+ addIssue(issues, "V1012", at, "$connectOrCreate requires where and create.");
730
750
  continue;
731
751
  }
732
- validateUniqueSelector(contract, target, entry.where, [...path, "where"], issues);
733
- validateMutationData(contract, target, entry.create, "create", NESTED_WRITE_SCOPE, [...path, "create"], issues, transactionReferences);
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 entries = edge.cardinality === "many" && Array.isArray(value) ? value : [value];
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", path, "$update requires a data object.");
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", path, "To-many $update requires where.");
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: [...path, "where"],
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, [...path, "where"], state, 1);
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, [...path, "data"], issues, transactionReferences);
782
+ : NESTED_WRITE_SCOPE, [...at, "data"], issues, transactionReferences);
763
783
  }
764
784
  return;
765
785
  }
766
786
  if (directive === "$delete") {
767
- const entries = edge.cardinality === "many" && Array.isArray(value) ? value : [value];
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", path, "To-many $delete requires where.");
790
+ addIssue(issues, "V1012", at, "To-many $delete requires where.");
772
791
  continue;
773
792
  }
774
793
  const state = {
775
794
  issues,
776
- structuralRoot: [...path, "where"],
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, [...path, "where"], state, 1);
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 entries = edge.cardinality === "many" && Array.isArray(value) ? value : [value];
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", path, "$upsert requires create/update and requires where on to-many relations.");
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, [...path, "where"], issues);
834
+ validateUniqueSelector(contract, target, entry.where, [...at, "where"], issues);
816
835
  }
817
- validateMutationData(contract, target, entry.create, "create", NESTED_WRITE_SCOPE, [...path, "create"], issues, transactionReferences);
818
- validateMutationData(contract, target, entry.update, "update", NESTED_WRITE_SCOPE, [...path, "update"], issues, transactionReferences);
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>;