@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 +46 -0
- package/ERRORS.md +4 -4
- package/dist/config/exact-framework-config.d.ts +3 -1
- package/dist/operations/database-refusal.d.ts +1 -1
- package/dist/operations/database-refusal.js +1 -1
- package/dist/operations/operation-validator-core.js +60 -33
- package/dist/operations/validation-code.guidance.js +2 -2
- package/dist/runtime/field-validator.runtime.d.ts +9 -0
- package/dist/runtime/field-validator.runtime.js +36 -2
- package/dist/runtime/operation-code.guidance.js +1 -1
- package/dist/runtime/operation-runtime.js +5 -1
- package/package.json +1 -1
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.
|
|
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"]
|
|
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
|
|
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
|
|
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
|
|
691
|
-
validateUniqueSelector(contract, target, selector,
|
|
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,
|
|
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
|
|
713
|
-
|
|
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
|
|
720
|
-
|
|
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
|
|
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",
|
|
743
|
+
addIssue(issues, "V1012", at, "$connectOrCreate requires where and create.");
|
|
730
744
|
continue;
|
|
731
745
|
}
|
|
732
|
-
validateUniqueSelector(contract, target, entry.where, [...
|
|
733
|
-
validateMutationData(contract, target, entry.create, "create", NESTED_WRITE_SCOPE, [...
|
|
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
|
|
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",
|
|
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",
|
|
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: [...
|
|
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, [...
|
|
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, [...
|
|
776
|
+
: NESTED_WRITE_SCOPE, [...at, "data"], issues, transactionReferences);
|
|
763
777
|
}
|
|
764
778
|
return;
|
|
765
779
|
}
|
|
766
780
|
if (directive === "$delete") {
|
|
767
|
-
const
|
|
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",
|
|
784
|
+
addIssue(issues, "V1012", at, "To-many $delete requires where.");
|
|
772
785
|
continue;
|
|
773
786
|
}
|
|
774
787
|
const state = {
|
|
775
788
|
issues,
|
|
776
|
-
structuralRoot: [...
|
|
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, [...
|
|
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
|
|
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",
|
|
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, [...
|
|
828
|
+
validateUniqueSelector(contract, target, entry.where, [...at, "where"], issues);
|
|
816
829
|
}
|
|
817
|
-
validateMutationData(contract, target, entry.create, "create", NESTED_WRITE_SCOPE, [...
|
|
818
|
-
validateMutationData(contract, target, entry.update, "update", NESTED_WRITE_SCOPE, [...
|
|
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
|
|
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"]
|
|
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;
|