@aventara/core 1.0.0-rc.2 → 1.0.0-rc.3

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 CHANGED
@@ -1,5 +1,51 @@
1
1
  # @aventara/core
2
2
 
3
+ ## 1.0.0-rc.3
4
+
5
+ ### Patch Changes
6
+
7
+ - b38034d: An issue inside one entry of a to-many relation directive's array names that entry's index:
8
+ `["arguments", "data", "posts", "$update", 1, "data", "category"]`, where it read `[…, "$update", "data", "category"]`.
9
+ This holds for `$create`, `$connect`, `$connectOrCreate`, `$update`, `$delete`, `$upsert`, `$set` and `$disconnect`,
10
+ as the runtime's own nested-write paths already did. A directive given one object, not an array, keeps its issue at the
11
+ directive.
12
+ - b38034d: An empty boolean node in the `where` of a write is refused, as an `undefined` there is, and nothing runs. In
13
+ `update.many`, `update.count`, `update.first`, `delete.many`, `delete.count`, `delete.first` and a to-many `$update`
14
+ or `$delete`, an empty `NOT: {}`, `AND: {}` or `[]`, `OR: []` or `AND`/`OR` entry `{}`, at any depth, answers
15
+ `422 A2004` with a `V1001` issue at the node:
16
+ `A boolean node must hold a condition here: an empty one matches every row, and the write would reach more rows.`
17
+ The ORM reads these as no condition, so `delete.count({ where: { NOT: {} } })` deleted every row, and `delete.first`
18
+ with one deleted an arbitrary row. A `where` that is `{}` itself still means every row, and reads pass empty nodes through as before. Field filters are unchanged: a scalar's `{}` and a
19
+ to-many relation's `{}` were already refused, and a to-one relation's `{}` matches the rows that have a related row.
20
+ - b38034d: The error catalog (`AvErrorCatalog`, `ERRORS.md`) catches up with the code: `A2004` says a value the database refused
21
+ may stand at the value itself where the adapter can place it (a bigint its driver cannot bind) and may be too long for
22
+ its index; `V1001` lists an `undefined` in a filter, selector or cursor and an empty boolean node in a bulk write's
23
+ `where`; `V1004` lists a value the database refused as too long or out of range, a bigint a driver cannot bind among
24
+ them.
25
+ - b38034d: A value write compute writes to a field the caller's layer hides is judged by that field's validators and Json shape
26
+ (the Core Contract's), and a value they refuse is the server's error: `500 A3000`, the issues to the `diagnostics` sink
27
+ on an `AvComputedValueError`, with nothing of the hidden field in the answer. Before, a shaped Json field hidden from
28
+ the layer was not judged at all, and a hidden field's own validators answered the caller `A2004` with an issue at the
29
+ hidden field's path. A layer that shows the field still answers `A2004` at it.
30
+ - b38034d: On PostgreSQL, a `@unique` value too long for its index (an index row past the B-tree's 8191 bytes, SQLSTATE
31
+ `54000`) is answered `422 A2004`, not `500 A3001`: one `V1004` issue at `["arguments"]`, as a value too long for its
32
+ column already was. The adapter reads the SQLSTATE, never the message. The issue's message for a value too long now
33
+ reads `The database refused this value: it is longer than the database can hold for its column.`
34
+ (it said `longer than its column can hold`, which an index refusal is not).
35
+ - b38034d: A Pick layer's list entry that names what its parent hid is refused with the sentence written inline too. Inline in
36
+ `createFramework` or `AventaraModule.forRoot`, the error is at the key and reads:
37
+
38
+ ```text
39
+ Type '["select"]' is not assignable to type '…'.
40
+ …
41
+ Property '"Error: client cannot expose 'Post.internalNote': hidden by root"' is missing in type '["select"]' but required in type '{ readonly "Error: client cannot expose 'Post.internalNote': hidden by root": true; }'.
42
+ ```
43
+
44
+ where it ended at `Type 'string' is not assignable to type 'never'.` at the list's element. A list entry is now typed as
45
+ an object keyed by the sentence, which no list has, so a configuration that is not inline (an `aventaraConfig()` handed
46
+ to `AventaraModule.forRootAsync`) ends at the same `Property '"Error: …"' is missing` line, where it ended at
47
+ `Type '"select"' is not assignable to type '"Error: …"'`. An entry written `true` or as an object is unchanged.
48
+
3
49
  ## 1.0.0-rc.2
4
50
 
5
51
  ### Patch Changes
package/ERRORS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Aventara error codes
2
2
 
3
- Every code `@aventara/core` 1.0.0-rc.2 answers, reports in a validation issue, or refuses to start with. Generated from `AvErrorCatalog` (exported by `@aventara/core`); do not edit.
3
+ Every code `@aventara/core` 1.0.0-rc.3 answers, reports in a validation issue, or refuses to start with. Generated from `AvErrorCatalog` (exported by `@aventara/core`); do not edit.
4
4
 
5
5
  Branch on the code, never on the message. In code, `AvErrorCatalog[code]` gives the same entry: `{ code, kind, message, cause, fix, docs }`.
6
6
 
@@ -31,7 +31,7 @@ The caller can fix these. The generated client and server-side calls throw the c
31
31
  | [`A2001`](https://aventara-framework.vercel.app/docs/error-codes#a2001) | Unknown Resource. | The Resource is not in the caller's contract: misspelt, hidden by exposure, or added to the schema after aventara:prepare last ran. cause.message is "The requested Resource is not available." and the issue is V1005 at `["resource"]`. | Use the model's exact name. After a schema change run the adapter's generate step (`npm run aventara:prepare` in a scaffolded project), restart, and regenerate the client. A Resource hidden on purpose is reached from server code (framework.application). |
32
32
  | [`A2002`](https://aventara-framework.vercel.app/docs/error-codes#a2002) | Unknown operation. | The operation is not available on that Resource: a typo, removed by exposure, an operation the adapter does not offer (such as delete.many), or a ctx.$sql, appTx.$sql or ctx.$transaction the adapter or the layer cannot run. The issue is V1006 at `["family"]` or `["variant"]`. | Call an operation the contract lists (`GET <api>/_contract`), regenerate a stale client, and check the layer's exemptions and transactionsStyle. |
33
33
  | [`A2003`](https://aventara-framework.vercel.app/docs/error-codes#a2003) | Not found. | A unique operation (find.unique, update.unique, delete.unique) matched no row, or a $connect names a record that does not exist. The operation fails, so no after hook runs. | Catch AvNotFoundError, or use a first-style operation, which answers A1001 with null. Check the $connect identifier. |
34
- | [`A2004`](https://aventara-framework.vercel.app/docs/error-codes#a2004) | Validation failed. | The arguments break a contract rule or a field validator. cause.issues lists each problem by V-code and path; a validator's issue also has check and params. The validators judge the values after the pipes and write compute, so a computed value they refuse is here too, at its field. A required create field left out is here (V1000). So is a value the database itself refused: too long for its column or out of its range (V1004), or holding a character it cannot store or a pattern it cannot read (V1001), with the issue at `["arguments"]` and its message saying what the database refused. So is a record written through a relation that writes that relation, or a field holding its reference, back (V1012, at that field), where the adapter admits no such write. | Fix the argument each issue's path points at, as its V-code says. A field the server must fill takes a compute.create stage. For a value the database refused, send one its column holds. For a nested write that writes its relation back, leave that field out: the parent supplies it. |
34
+ | [`A2004`](https://aventara-framework.vercel.app/docs/error-codes#a2004) | Validation failed. | The arguments break a contract rule or a field validator. cause.issues lists each problem by V-code and path; a validator's issue also has check and params. The validators judge the values after the pipes and write compute, so a computed value they refuse is here too, at its field. A required create field left out is here (V1000). So is a value the database itself refused: too long for its column or its index, or out of its range (V1004), or holding a character it cannot store or a pattern it cannot read (V1001), with the issue at `["arguments"]`, or at the value where the adapter can place it (a bigint its driver cannot bind), and its message saying what the database refused. So is a record written through a relation that writes that relation, or a field holding its reference, back (V1012, at that field), where the adapter admits no such write. | Fix the argument each issue's path points at, as its V-code says. A field the server must fill takes a compute.create stage. For a value the database refused, send one its column holds. For a nested write that writes its relation back, leave that field out: the parent supplies it. |
35
35
  | [`A2005`](https://aventara-framework.vercel.app/docs/error-codes#a2005) | Contract mismatch. | The caller's Aventara-Contract-Hash is not the server's current one: the schema, the configuration, the limits or the exposure changed, or the client targets another deployment. | Regenerate the client against the running server: `npm run avclient:generate`. |
36
36
  | [`A2006`](https://aventara-framework.vercel.app/docs/error-codes#a2006) | Protocol mismatch. | Aventara-Protocol-Version is not 1. | Send `Aventara-Protocol-Version: 1`, or upgrade the client. |
37
37
  | [`A2007`](https://aventara-framework.vercel.app/docs/error-codes#a2007) | Invalid transaction reference. | A transaction $ref or fingerprint is invalid: the issues are V1010, V1011, V1016, V1017 or V1018, and cause.operation is the step. A $ref with no fingerprint is A2004 / V1000. | Let the generated client or framework.appTx build plans; check the $ref path and that the referenced step comes earlier. |
@@ -72,10 +72,10 @@ Each entry of `cause.issues[]` under `A2001`, `A2002`, `A2004`, `A2007` or `A200
72
72
  | Code | Meaning | Cause | Fix |
73
73
  |---|---|---|---|
74
74
  | [`V1000`](https://aventara-framework.vercel.app/docs/error-codes#v1000) | Required value missing. | A required value is absent: a create leaves out a required field that has no database default and no compute.create (the path names the field), a required relation and its reference field are both missing, a typed Json field lacks a required key, or a $ref has no fingerprint. | 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. |
75
- | [`V1001`](https://aventara-framework.vercel.app/docs/error-codes#v1001) | Invalid runtime/wire type. | 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. | Send the type the contract states at that path (the generated client's types say it). |
75
+ | [`V1001`](https://aventara-framework.vercel.app/docs/error-codes#v1001) | Invalid runtime/wire type. | 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). | Send the type the contract states at that path (the generated client's types say it). |
76
76
  | [`V1002`](https://aventara-framework.vercel.app/docs/error-codes#v1002) | Invalid enum value. | A value that is not a member of the enum, or outside a typed Json field's AvZ.enum or AvZ.literal. | Send one of the enum's members. |
77
77
  | [`V1003`](https://aventara-framework.vercel.app/docs/error-codes#v1003) | Invalid format (for example UUID/DateTime/base64). | A malformed datetime, decimal, base64 or UUID string, or a failed pattern or format validator (`check: "string_format"`). | Send the canonical encoding (ISO-8601 UTC datetime, decimal string, base64 bytes) and satisfy the validator that check and params name. |
78
- | [`V1004`](https://aventara-framework.vercel.app/docs/error-codes#v1004) | Value outside accepted range. | 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. | Send a value inside the range; check and params name the validator that failed. |
78
+ | [`V1004`](https://aventara-framework.vercel.app/docs/error-codes#v1004) | Value outside accepted range. | 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)). | Send a value inside the range; check and params name the validator that failed. |
79
79
  | [`V1005`](https://aventara-framework.vercel.app/docs/error-codes#v1005) | Unknown or inaccessible field. | A field that does not exist or is hidden in the caller's layer, an unknown Resource (under A2001), or a key a typed Json field's shape does not declare. | Use a field the caller's contract exposes, expose it through exemptions, or make the call from server code. |
80
80
  | [`V1006`](https://aventara-framework.vercel.app/docs/error-codes#v1006) | Unsupported filter/order/mutation directive. | An operator, directive or nested write the field does not offer (at any depth, inside not too; a to-many $update's data takes only what a bulk write takes), a mode it does not offer, or an operation that is not available (under A2002). | Use what the contract lists for that field or Resource, or expose it through exemptions. |
81
81
  | [`V1007`](https://aventara-framework.vercel.app/docs/error-codes#v1007) | Invalid unique identifier shape. | The where of a unique operation is not exactly one complete identifier. | Pass exactly one identifier: the id, or every field of one @unique or @@unique. |
@@ -147,7 +147,9 @@ type RefusalOf<Info, K, Value> = [Value] extends [false] ? never : Info extends
147
147
  readonly layer: infer Layer extends string;
148
148
  readonly parent: infer Parent extends string;
149
149
  readonly at: infer At extends string;
150
- } ? readonly [
150
+ } ? Value extends readonly unknown[] ? {
151
+ readonly [S in `Error: ${Layer} cannot expose '${At}${K & string}': hidden by ${Parent}`]: true;
152
+ } : readonly [
151
153
  `Error: ${Layer} cannot expose '${At}${K & string}': hidden by ${Parent}`
152
154
  ] : never;
153
155
  /** A Pick layer's operations exemption, a family or variant its parent hid refused by name. */
@@ -6,7 +6,7 @@ import type { AvValidationCode } from "./validation-code.js";
6
6
  export declare const DATABASE_REFUSALS: {
7
7
  readonly tooLong: {
8
8
  readonly code: "V1004";
9
- readonly message: "The database refused this value: it is longer than its column can hold.";
9
+ readonly message: "The database refused this value: it is longer than the database can hold for its column.";
10
10
  };
11
11
  readonly outOfRange: {
12
12
  readonly code: "V1004";
@@ -1,7 +1,7 @@
1
1
  export const DATABASE_REFUSALS = {
2
2
  tooLong: {
3
3
  code: "V1004",
4
- message: "The database refused this value: it is longer than its column can hold.",
4
+ message: "The database refused this value: it is longer than the database can hold for its column.",
5
5
  },
6
6
  outOfRange: {
7
7
  code: "V1004",
@@ -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;
@@ -687,8 +704,8 @@ function validateRelationMutationEntry(contract, edge, directive, value, path, i
687
704
  addIssue(issues, "V1012", path, "To-many $disconnect requires one or more unique identifiers.");
688
705
  return;
689
706
  }
690
- for (const selector of selectors) {
691
- validateUniqueSelector(contract, target, selector, path, issues);
707
+ for (const { entry: selector, at } of entriesOf(edge, value, path)) {
708
+ validateUniqueSelector(contract, target, selector, at, issues);
692
709
  }
693
710
  return;
694
711
  }
@@ -703,84 +720,81 @@ function validateRelationMutationEntry(contract, edge, directive, value, path, i
703
720
  addIssue(issues, "V1012", path, "To-many $set requires an identifier array.");
704
721
  return;
705
722
  }
706
- for (const selector of value) {
707
- validateUniqueSelector(contract, target, selector, path, issues);
723
+ for (const { entry: selector, at } of entriesOf(edge, value, path)) {
724
+ validateUniqueSelector(contract, target, selector, at, issues);
708
725
  }
709
726
  return;
710
727
  }
711
728
  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);
729
+ for (const { entry: selector, at } of entriesOf(edge, value, path)) {
730
+ validateUniqueSelector(contract, target, selector, at, issues);
715
731
  }
716
732
  return;
717
733
  }
718
734
  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);
735
+ for (const { entry, at } of entriesOf(edge, value, path)) {
736
+ validateMutationData(contract, target, entry, "create", NESTED_WRITE_SCOPE, at, issues, transactionReferences);
722
737
  }
723
738
  return;
724
739
  }
725
740
  if (directive === "$connectOrCreate") {
726
- const entries = edge.cardinality === "many" && Array.isArray(value) ? value : [value];
727
- for (const entry of entries) {
741
+ for (const { entry, at } of entriesOf(edge, value, path)) {
728
742
  if (!isRecord(entry) || !("where" in entry) || !("create" in entry)) {
729
- addIssue(issues, "V1012", path, "$connectOrCreate requires where and create.");
743
+ addIssue(issues, "V1012", at, "$connectOrCreate requires where and create.");
730
744
  continue;
731
745
  }
732
- validateUniqueSelector(contract, target, entry.where, [...path, "where"], issues);
733
- validateMutationData(contract, target, entry.create, "create", NESTED_WRITE_SCOPE, [...path, "create"], issues, transactionReferences);
746
+ validateUniqueSelector(contract, target, entry.where, [...at, "where"], issues);
747
+ validateMutationData(contract, target, entry.create, "create", NESTED_WRITE_SCOPE, [...at, "create"], issues, transactionReferences);
734
748
  }
735
749
  return;
736
750
  }
737
751
  if (directive === "$update") {
738
- const entries = edge.cardinality === "many" && Array.isArray(value) ? value : [value];
739
- for (const entry of entries) {
752
+ for (const { entry, at } of entriesOf(edge, value, path)) {
740
753
  if (!isRecord(entry) || !("data" in entry)) {
741
- addIssue(issues, "V1012", path, "$update requires a data object.");
754
+ addIssue(issues, "V1012", at, "$update requires a data object.");
742
755
  continue;
743
756
  }
744
757
  if (edge.cardinality === "many" && !("where" in entry)) {
745
- addIssue(issues, "V1012", path, "To-many $update requires where.");
758
+ addIssue(issues, "V1012", at, "To-many $update requires where.");
746
759
  continue;
747
760
  }
748
761
  if ("where" in entry) {
749
762
  const state = {
750
763
  issues,
751
- structuralRoot: [...path, "where"],
764
+ structuralRoot: [...at, "where"],
752
765
  transactionReferences,
753
766
  scalarOnly: edge.capabilities.nestedWriteFilter === "scalar",
767
+ ...(edge.cardinality === "many" ? { refuseEmptyNodes: true } : {}),
754
768
  booleanFilterNodes: 0,
755
769
  nestingExceeded: false,
756
770
  booleanNodesExceeded: false,
757
771
  };
758
- validateWhere(contract, target, entry.where, [...path, "where"], state, 1);
772
+ validateWhere(contract, target, entry.where, [...at, "where"], state, 1);
759
773
  }
760
774
  validateMutationData(contract, target, entry.data, "update", edge.cardinality === "many"
761
775
  ? TO_MANY_UPDATE_WRITE_SCOPE
762
- : NESTED_WRITE_SCOPE, [...path, "data"], issues, transactionReferences);
776
+ : NESTED_WRITE_SCOPE, [...at, "data"], issues, transactionReferences);
763
777
  }
764
778
  return;
765
779
  }
766
780
  if (directive === "$delete") {
767
- const entries = edge.cardinality === "many" && Array.isArray(value) ? value : [value];
768
- for (const entry of entries) {
781
+ for (const { entry, at } of entriesOf(edge, value, path)) {
769
782
  if (edge.cardinality === "many") {
770
783
  if (!isRecord(entry) || !("where" in entry)) {
771
- addIssue(issues, "V1012", path, "To-many $delete requires where.");
784
+ addIssue(issues, "V1012", at, "To-many $delete requires where.");
772
785
  continue;
773
786
  }
774
787
  const state = {
775
788
  issues,
776
- structuralRoot: [...path, "where"],
789
+ structuralRoot: [...at, "where"],
777
790
  transactionReferences,
778
791
  scalarOnly: edge.capabilities.nestedWriteFilter === "scalar",
792
+ refuseEmptyNodes: true,
779
793
  booleanFilterNodes: 0,
780
794
  nestingExceeded: false,
781
795
  booleanNodesExceeded: false,
782
796
  };
783
- validateWhere(contract, target, entry.where, [...path, "where"], state, 1);
797
+ validateWhere(contract, target, entry.where, [...at, "where"], state, 1);
784
798
  }
785
799
  else if (entry !== true &&
786
800
  (!isRecord(entry) || Object.keys(entry).some((key) => key !== "where"))) {
@@ -802,23 +816,27 @@ function validateRelationMutationEntry(contract, edge, directive, value, path, i
802
816
  return;
803
817
  }
804
818
  if (directive === "$upsert") {
805
- const entries = edge.cardinality === "many" && Array.isArray(value) ? value : [value];
806
- for (const entry of entries) {
819
+ for (const { entry, at } of entriesOf(edge, value, path)) {
807
820
  if (!isRecord(entry) ||
808
821
  !("create" in entry) ||
809
822
  !("update" in entry) ||
810
823
  (edge.cardinality === "many" && !("where" in entry))) {
811
- addIssue(issues, "V1012", path, "$upsert requires create/update and requires where on to-many relations.");
824
+ addIssue(issues, "V1012", at, "$upsert requires create/update and requires where on to-many relations.");
812
825
  continue;
813
826
  }
814
827
  if ("where" in entry) {
815
- validateUniqueSelector(contract, target, entry.where, [...path, "where"], issues);
828
+ validateUniqueSelector(contract, target, entry.where, [...at, "where"], issues);
816
829
  }
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);
830
+ validateMutationData(contract, target, entry.create, "create", NESTED_WRITE_SCOPE, [...at, "create"], issues, transactionReferences);
831
+ validateMutationData(contract, target, entry.update, "update", NESTED_WRITE_SCOPE, [...at, "update"], issues, transactionReferences);
819
832
  }
820
833
  }
821
834
  }
835
+ function entriesOf(edge, value, path) {
836
+ return edge.cardinality === "many" && Array.isArray(value)
837
+ ? value.map((entry, index) => ({ entry, at: [...path, index] }))
838
+ : [{ entry: value, at: path }];
839
+ }
822
840
  function validateRelationMutation(contract, fieldName, field, value, stage, scope, path, issues, transactionReferences) {
823
841
  const directives = advertisedDirectivesAt(field.capabilities[stage], scope);
824
842
  if (directives === undefined) {
@@ -896,6 +914,14 @@ function validateMutationArguments(contract, resourceName, request, args, issues
896
914
  }
897
915
  }
898
916
  }
917
+ function emptyNodeRefusalOf(request) {
918
+ return (request.family === "update" || request.family === "delete") &&
919
+ (request.variant === "many" ||
920
+ request.variant === "count" ||
921
+ request.variant === "first")
922
+ ? { refuseEmptyNodes: true }
923
+ : {};
924
+ }
899
925
  function isUniqueOperation(request) {
900
926
  return (request.variant === "unique" &&
901
927
  (request.family === "find" ||
@@ -941,6 +967,7 @@ export function validateOperationArguments(contract, request, options) {
941
967
  issues,
942
968
  structuralRoot: ["arguments", "where"],
943
969
  transactionReferences: options?.transactionReferences === true,
970
+ ...emptyNodeRefusalOf(request),
944
971
  booleanFilterNodes: 0,
945
972
  nestingExceeded: false,
946
973
  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: {
@@ -1,5 +1,6 @@
1
1
  import type { AvContract } from "../contracts/contract.js";
2
2
  import type { AvValidationIssue } from "../operations/validation-code.js";
3
+ import { AvComputedValueError } from "./computed-value.error.js";
3
4
  import { type Path } from "./field-stage.runtime.js";
4
5
  import type { OperationExecutionRequest } from "./operation-request.js";
5
6
  import type { ReferentialActionTable } from "./referential-action.js";
@@ -17,3 +18,11 @@ export type DeferredField = (resource: string, stage: WriteStage, field: string)
17
18
  * request's: `argumentsPath` and then the value's place in the arguments.
18
19
  */
19
20
  export declare function fieldValidatorIssues(contract: AvContract, references: ReferentialActionTable, request: OperationExecutionRequest, args: unknown, argumentsPath: Path, select: (path: Path) => boolean, deferred?: DeferredField): readonly AvValidationIssue[];
21
+ /**
22
+ * The server's defect when a value written to a field the active layer hides
23
+ * breaks its validators or its Json shape, or `undefined`.
24
+ */
25
+ export declare function hiddenFieldRefusal(contracts: {
26
+ readonly core: AvContract;
27
+ readonly active: AvContract;
28
+ }, references: ReferentialActionTable, request: OperationExecutionRequest, args: unknown, argumentsPath: Path): AvComputedValueError | undefined;
@@ -1,4 +1,5 @@
1
- import { validatedFieldsOf, } from "../compiler/field-validator.compiler.js";
1
+ import { hasFieldValidators, validatedFieldsOf, } from "../compiler/field-validator.compiler.js";
2
+ import { AvComputedValueError } from "./computed-value.error.js";
2
3
  import { entryCheckIssues, fieldCheckIssues } from "./field-check.evaluator.js";
3
4
  import { isPlainRecord, valueAt, writeRecordPathsOf, } from "./field-stage.runtime.js";
4
5
  import { nestedWriteTargetsOf } from "./nested-write.walker.js";
@@ -14,7 +15,10 @@ function rulesOf(contract, compiled, resource, name) {
14
15
  const field = fields !== undefined && Object.hasOwn(fields, name)
15
16
  ? fields[name]
16
17
  : undefined;
17
- if (field?.kind === "scalar" && field.json !== undefined) {
18
+ if (field === undefined) {
19
+ return undefined;
20
+ }
21
+ if (field.kind === "scalar" && field.json !== undefined) {
18
22
  return { kind: "shape", field };
19
23
  }
20
24
  const checks = Object.hasOwn(compiled, name) ? compiled[name] : undefined;
@@ -90,3 +94,33 @@ export function fieldValidatorIssues(contract, references, request, args, argume
90
94
  }
91
95
  return issues;
92
96
  }
97
+ function holdsField(contract, resource, field) {
98
+ const fields = Object.hasOwn(contract.resources, resource)
99
+ ? contract.resources[resource]?.fields
100
+ : undefined;
101
+ return fields !== undefined && Object.hasOwn(fields, field);
102
+ }
103
+ export function hiddenFieldRefusal(contracts, references, request, args, argumentsPath) {
104
+ const { core, active } = contracts;
105
+ if (core === active || !hasFieldValidators(core)) {
106
+ return undefined;
107
+ }
108
+ const judged = [];
109
+ const issues = fieldValidatorIssues(core, references, request, args, argumentsPath, () => true, (resource, stage, field) => {
110
+ if (holdsField(active, resource, field)) {
111
+ return true;
112
+ }
113
+ judged.push({ resource, stage, field });
114
+ return false;
115
+ });
116
+ const [first] = issues;
117
+ if (first === undefined) {
118
+ return undefined;
119
+ }
120
+ const path = first.path ?? [];
121
+ const at = judged.find(({ field }) => path.includes(field)) ?? judged[0];
122
+ if (at === undefined) {
123
+ return undefined;
124
+ }
125
+ return new AvComputedValueError(at.resource, at.field, at.stage, issues, `compute.${at.stage} on ${at.resource}.${at.field}, a field the ${request.origin} layer hides, returned a value its validators refuse (${first.code}: ${first.message})`);
126
+ }
@@ -71,7 +71,7 @@ export const OPERATION_CODE_GUIDANCE = {
71
71
  },
72
72
  A2004: {
73
73
  message: "Validation failed.",
74
- cause: 'The arguments break a contract rule or a field validator. cause.issues lists each problem by V-code and path; a validator\'s issue also has check and params. The validators judge the values after the pipes and write compute, so a computed value they refuse is here too, at its field. A required create field left out is here (V1000). So is a value the database itself refused: too long for its column or out of its range (V1004), or holding a character it cannot store or a pattern it cannot read (V1001), with the issue at `["arguments"]` and its message saying what the database refused. So is a record written through a relation that writes that relation, or a field holding its reference, back (V1012, at that field), where the adapter admits no such write.',
74
+ cause: 'The arguments break a contract rule or a field validator. cause.issues lists each problem by V-code and path; a validator\'s issue also has check and params. The validators judge the values after the pipes and write compute, so a computed value they refuse is here too, at its field. A required create field left out is here (V1000). So is a value the database itself refused: too long for its column or its index, or out of its range (V1004), or holding a character it cannot store or a pattern it cannot read (V1001), with the issue at `["arguments"]`, or at the value where the adapter can place it (a bigint its driver cannot bind), and its message saying what the database refused. So is a record written through a relation that writes that relation, or a field holding its reference, back (V1012, at that field), where the adapter admits no such write.',
75
75
  fix: "Fix the argument each issue's path points at, as its V-code says. A field the server must fill takes a compute.create stage. For a value the database refused, send one its column holds. For a nested write that writes its relation back, leave that field out: the parent supplies it.",
76
76
  },
77
77
  A2005: {
@@ -15,7 +15,7 @@ import { createDiagnosticsReporter, } from "./diagnostics-reporter.js";
15
15
  import { AdapterBoundary, OPERATION_ARGUMENTS_PATH, resolveOperationFailure, sanitizeOperationFailure, } from "./error-boundary.js";
16
16
  import { planFieldStages } from "./field-stage.plan.js";
17
17
  import { guardSentFields, pipeSentFields, valueAt, writeRecordPathsOf, } from "./field-stage.runtime.js";
18
- import { fieldValidatorIssues } from "./field-validator.runtime.js";
18
+ import { fieldValidatorIssues, hiddenFieldRefusal, } from "./field-validator.runtime.js";
19
19
  import { thrownFailure } from "./framework-error.factory.js";
20
20
  import { AvInterceptorNextError } from "./interceptor-next.error.js";
21
21
  import { runLevelLifecycle, } from "./level-lifecycle.runtime.js";
@@ -539,6 +539,10 @@ export async function executeAdmittedOperation(dependencies, request, contract,
539
539
  return mapValidationOutcome(refused);
540
540
  }
541
541
  }
542
+ const hiddenRefusal = hiddenFieldRefusal({ core: dependencies.contracts.core, active: contract }, dependencies.recordWrites.references, request, computed, argumentsPath);
543
+ if (hiddenRefusal !== undefined) {
544
+ throw hiddenRefusal;
545
+ }
542
546
  args = deepFreeze(computed);
543
547
  let readMaterialized = false;
544
548
  let executedReadPlan;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aventara/core",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0-rc.3",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "Core runtime, contract compiler, and extension APIs for Aventara.",
6
6
  "type": "module",