@aventara/core 1.0.0-rc.1 → 1.0.0-rc.2
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 +295 -1
- package/ERRORS.md +142 -0
- package/README.md +24 -1
- package/dist/adapters/adapter-discovery-diagnostic.guidance.d.ts +6 -0
- package/dist/adapters/adapter-discovery-diagnostic.guidance.js +7 -0
- package/dist/adapters/adapter-field.d.ts +14 -0
- package/dist/adapters/adapter-model.d.ts +9 -0
- package/dist/adapters/adapter.interface.d.ts +10 -0
- package/dist/compiler/contract-compiler-diagnostic.guidance.d.ts +6 -0
- package/dist/compiler/contract-compiler-diagnostic.guidance.js +97 -0
- package/dist/compiler/contract.compiler.js +94 -6
- package/dist/config/computed-fields.d.ts +62 -0
- package/dist/config/computed-fields.js +80 -0
- package/dist/config/config-diagnostic.guidance.d.ts +6 -0
- package/dist/config/config-diagnostic.guidance.js +32 -0
- package/dist/config/define-resource.d.ts +1 -1
- package/dist/config/exact-framework-config.d.ts +48 -20
- package/dist/config/field-compute.resolver.d.ts +2 -0
- package/dist/config/field-compute.resolver.js +8 -0
- package/dist/config/framework-config.d.ts +7 -6
- package/dist/config/framework-resources-config.d.ts +18 -12
- package/dist/config/pipeline-config.d.ts +4 -4
- package/dist/contracts/capability.d.ts +10 -1
- package/dist/contracts/capability.js +2 -0
- package/dist/contracts/client-contract-structure.validator.d.ts +65 -35
- package/dist/contracts/client-contract-structure.validator.js +4 -2
- package/dist/contracts/contract-canonicalizer.d.ts +7 -0
- package/dist/contracts/contract-canonicalizer.js +18 -4
- package/dist/contracts/field.d.ts +30 -1
- package/dist/framework/contract-view.d.ts +4 -4
- package/dist/framework/framework-construction.error.d.ts +12 -3
- package/dist/framework/framework-construction.error.js +5 -1
- package/dist/framework/framework.factory.d.ts +1 -1
- package/dist/framework/framework.factory.js +9 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.js +1 -0
- package/dist/operations/database-refusal.d.ts +51 -0
- package/dist/operations/database-refusal.js +45 -0
- package/dist/operations/operation-arguments.d.ts +102 -31
- package/dist/operations/operation-validator-core.js +86 -4
- package/dist/operations/validation-code.guidance.d.ts +6 -0
- package/dist/operations/validation-code.guidance.js +90 -0
- package/dist/operations/write-scope.resolver.d.ts +4 -0
- package/dist/operations/write-scope.resolver.js +2 -0
- package/dist/protocol/protocol.handler.js +55 -16
- package/dist/runtime/caller-data.copier.d.ts +17 -0
- package/dist/runtime/caller-data.copier.js +18 -1
- package/dist/runtime/caller-data.door.d.ts +11 -5
- package/dist/runtime/computed-read-runtime.js +15 -2
- package/dist/runtime/computed-value.error.d.ts +3 -2
- package/dist/runtime/computed-value.error.js +3 -2
- package/dist/runtime/create-requirement.d.ts +41 -5
- package/dist/runtime/create-requirement.js +137 -20
- package/dist/runtime/diagnostics-reporter.d.ts +2 -1
- package/dist/runtime/error-boundary.js +4 -0
- package/dist/runtime/error-guidance.d.ts +12 -0
- package/dist/runtime/error-guidance.js +1 -0
- package/dist/runtime/error.catalog.d.ts +49 -0
- package/dist/runtime/error.catalog.js +49 -0
- package/dist/runtime/field-stage.runtime.d.ts +2 -0
- package/dist/runtime/field-stage.runtime.js +8 -4
- package/dist/runtime/field-validator.runtime.d.ts +9 -1
- package/dist/runtime/field-validator.runtime.js +11 -6
- package/dist/runtime/json-value.d.ts +10 -5
- package/dist/runtime/level-lifecycle.runtime.d.ts +2 -2
- package/dist/runtime/level-lifecycle.runtime.js +2 -5
- package/dist/runtime/mutation-planner.js +3 -14
- package/dist/runtime/operation-code.guidance.d.ts +6 -0
- package/dist/runtime/operation-code.guidance.js +167 -0
- package/dist/runtime/operation-runtime.d.ts +20 -6
- package/dist/runtime/operation-runtime.js +77 -35
- package/dist/runtime/owned-execution.registry.d.ts +12 -2
- package/dist/runtime/pipe-output.validator.d.ts +10 -0
- package/dist/runtime/pipe-output.validator.js +4 -2
- package/dist/runtime/pipeline.d.ts +35 -8
- package/dist/runtime/readonly-value.d.ts +14 -3
- package/dist/runtime/readonly-value.js +10 -1
- package/dist/runtime/scalar-runtime.d.ts +5 -1
- package/dist/runtime/scalar-runtime.js +86 -16
- package/dist/runtime/unsent-value.normalizer.d.ts +15 -0
- package/dist/runtime/unsent-value.normalizer.js +128 -0
- package/dist/transactions/operation-fingerprint.js +2 -2
- package/dist/transactions/sql-statement.d.ts +8 -0
- package/dist/transactions/sql-statement.validator.js +36 -3
- package/dist/transactions/transaction-plan-assembler.d.ts +1 -1
- package/dist/transactions/transaction-plan-assembler.js +33 -10
- package/dist/transactions/transaction-plan-validator.d.ts +16 -0
- package/dist/transactions/transaction-plan-validator.js +27 -1
- package/dist/transactions/transaction-reference-validator.d.ts +42 -0
- package/dist/transactions/transaction-reference-validator.js +82 -56
- package/dist/transactions/transaction-reference.js +5 -0
- package/dist/transactions/transaction-runtime.js +47 -22
- package/dist/transactions/transaction-stage.runtime.d.ts +2 -1
- package/dist/transactions/transaction-stage.runtime.js +7 -4
- package/package.json +4 -3
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { FILTER_MODES, } from "../contracts/capability.js";
|
|
1
2
|
import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
|
|
2
3
|
import { advertisedDirectivesAt, advertisedReducers, advertisedReducersAt, holdsAt, } from "../contracts/field.js";
|
|
3
|
-
import { isPlainRecord } from "../runtime/scalar-runtime.js";
|
|
4
|
+
import { isPlainRecord, unsentReadValueIssue, } from "../runtime/scalar-runtime.js";
|
|
4
5
|
import { routeTransactionReferenceCandidate } from "../transactions/transaction-reference.js";
|
|
5
6
|
import { isOperationFamily, } from "./operation-identity.js";
|
|
6
7
|
import { NESTED_PROJECTION_SCOPE, resolveProjectionScope, } from "./projection-scope.resolver.js";
|
|
7
|
-
import { NESTED_WRITE_SCOPE, resolveWriteScope, } from "./write-scope.resolver.js";
|
|
8
|
+
import { NESTED_WRITE_SCOPE, resolveWriteScope, TO_MANY_UPDATE_WRITE_SCOPE, } from "./write-scope.resolver.js";
|
|
8
9
|
const OPERATION_ARGUMENT_RULES = {
|
|
9
10
|
"find.first": {
|
|
10
11
|
allowed: ["where", "orderBy", "select", "include"],
|
|
@@ -190,11 +191,16 @@ function validateScalarCondition(contract, fieldName, field, supported, value, p
|
|
|
190
191
|
addIssue(state.issues, "V1001", path, `Filter object for field "${fieldName}" must contain an operator.`);
|
|
191
192
|
return;
|
|
192
193
|
}
|
|
194
|
+
refuseDanglingQualifiers(fieldName, value, path, state);
|
|
193
195
|
for (const operator of operators) {
|
|
194
196
|
if (!includes(supported, operator)) {
|
|
195
197
|
addIssue(state.issues, "V1006", [...path, operator], `Filter operator "${operator}" is not available on field "${fieldName}".`);
|
|
196
198
|
continue;
|
|
197
199
|
}
|
|
200
|
+
if (operator === "mode") {
|
|
201
|
+
refuseUnavailableMode(field, fieldName, value, path, state);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
198
204
|
if (operator === "not") {
|
|
199
205
|
const operand = value[operator];
|
|
200
206
|
if (isPlainRecord(operand) && !checkDepth(contract, state, depth + 1)) {
|
|
@@ -204,12 +210,55 @@ function validateScalarCondition(contract, fieldName, field, supported, value, p
|
|
|
204
210
|
}
|
|
205
211
|
}
|
|
206
212
|
}
|
|
213
|
+
function refuseDanglingQualifiers(fieldName, level, path, state) {
|
|
214
|
+
const qualifiers = DANGLING_QUALIFIERS.filter((key) => Object.hasOwn(level, key));
|
|
215
|
+
if (qualifiers.length === 0) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const compares = Object.entries(level).some(([operator, operand]) => !includes(DANGLING_QUALIFIERS, operator) &&
|
|
219
|
+
!(operator === "not" && isNegatedOperatorObject(operand, state)));
|
|
220
|
+
if (compares) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
for (const qualifier of qualifiers) {
|
|
224
|
+
addIssue(state.issues, "V1001", [...path, qualifier], `${qualifier} on field "${fieldName}" qualifies no operator: write it next to the operator it applies to (inside "not" to apply to a negated one).`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const DANGLING_QUALIFIERS = ["mode", "path"];
|
|
228
|
+
function isNegatedOperatorObject(operand, state) {
|
|
229
|
+
return (isPlainRecord(operand) &&
|
|
230
|
+
!(state.transactionReferences === true &&
|
|
231
|
+
routeTransactionReferenceCandidate(operand).kind !== "ordinary"));
|
|
232
|
+
}
|
|
233
|
+
function refuseUnavailableMode(field, fieldName, level, path, state) {
|
|
234
|
+
const mode = level.mode;
|
|
235
|
+
if (mode === "default" ||
|
|
236
|
+
typeof mode !== "string" ||
|
|
237
|
+
!includes(FILTER_MODES, mode)) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const { filterModes = [], pathFilterModes } = field.capabilities;
|
|
241
|
+
const atPath = Object.hasOwn(level, "path") && pathFilterModes !== undefined;
|
|
242
|
+
if (includes(atPath ? pathFilterModes : filterModes, mode)) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const where = atPath
|
|
246
|
+
? " at a path"
|
|
247
|
+
: pathFilterModes !== undefined && includes(pathFilterModes, mode)
|
|
248
|
+
? " without a path"
|
|
249
|
+
: "";
|
|
250
|
+
addIssue(state.issues, "V1006", [...path, "mode"], `Filter mode "${mode}" is not available on field "${fieldName}"${where}.`);
|
|
251
|
+
}
|
|
207
252
|
function validateRelationFilter(contract, fieldName, field, value, path, state, depth) {
|
|
208
253
|
const supported = field.capabilities.filter;
|
|
209
254
|
if (supported === undefined) {
|
|
210
255
|
addIssue(state.issues, "V1006", path, `Filtering is not available on relation "${fieldName}".`);
|
|
211
256
|
return;
|
|
212
257
|
}
|
|
258
|
+
if (value === undefined) {
|
|
259
|
+
state.issues.push(unsentReadValueIssue(path));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
213
262
|
if (field.cardinality === "one") {
|
|
214
263
|
if (value === null) {
|
|
215
264
|
if (!field.nullable || !includes(supported, "null")) {
|
|
@@ -249,10 +298,20 @@ function validateRelationFilter(contract, fieldName, field, value, path, state,
|
|
|
249
298
|
validateWhere(contract, field.target, value[operator], [...path, operator], state, depth + 1);
|
|
250
299
|
}
|
|
251
300
|
}
|
|
301
|
+
const UNDEFINED_ARGUMENT_MESSAGES = {
|
|
302
|
+
where: "where must not be undefined: leave the key out to mean no filter.",
|
|
303
|
+
limit: "limit must not be undefined: leave the key out to mean no limit.",
|
|
304
|
+
offset: "offset must not be undefined: leave the key out to mean no offset.",
|
|
305
|
+
cursor: "cursor must not be undefined: leave the key out to mean no cursor.",
|
|
306
|
+
};
|
|
252
307
|
function validateWhere(contract, resourceName, where, path, state, depth) {
|
|
253
308
|
if (!checkDepth(contract, state, depth)) {
|
|
254
309
|
return;
|
|
255
310
|
}
|
|
311
|
+
if (where === undefined) {
|
|
312
|
+
state.issues.push(unsentReadValueIssue(path));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
256
315
|
if (!isRecord(where)) {
|
|
257
316
|
addIssue(state.issues, "V1001", path, "where must be an object.");
|
|
258
317
|
return;
|
|
@@ -366,7 +425,9 @@ function validateOrderBy(contract, resourceName, orderBy, path, issues) {
|
|
|
366
425
|
valueKeys[1] !== "sort" ||
|
|
367
426
|
(value.sort !== "asc" && value.sort !== "desc") ||
|
|
368
427
|
(value.nulls !== "first" && value.nulls !== "last")) {
|
|
369
|
-
addIssue(issues, "V1001", fieldPath,
|
|
428
|
+
addIssue(issues, "V1001", fieldPath, Object.hasOwn(value, "$ref")
|
|
429
|
+
? `A transaction reference cannot stand in orderBy: order "${fieldName}" by "asc", "desc" or { sort, nulls }.`
|
|
430
|
+
: `Null ordering for "${fieldName}" must be exactly { sort: "asc" | "desc", nulls: "first" | "last" }.`);
|
|
370
431
|
continue;
|
|
371
432
|
}
|
|
372
433
|
if (!field.nullable || field.capabilities.orderNulls !== true) {
|
|
@@ -393,6 +454,10 @@ function validateOrderBy(contract, resourceName, orderBy, path, issues) {
|
|
|
393
454
|
return analysis;
|
|
394
455
|
}
|
|
395
456
|
function validatePaginationValue(contract, name, value, path, issues) {
|
|
457
|
+
if (value === undefined) {
|
|
458
|
+
addIssue(issues, "V1001", path, UNDEFINED_ARGUMENT_MESSAGES[name]);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
396
461
|
if (!Number.isInteger(value) || typeof value !== "number" || value < 0) {
|
|
397
462
|
addIssue(issues, "V1001", path, `${name} must be a non-negative integer.`);
|
|
398
463
|
return;
|
|
@@ -402,6 +467,10 @@ function validatePaginationValue(contract, name, value, path, issues) {
|
|
|
402
467
|
}
|
|
403
468
|
}
|
|
404
469
|
function validateCursor(contract, resourceName, cursor, orderBy, path, issues) {
|
|
470
|
+
if (cursor === undefined) {
|
|
471
|
+
addIssue(issues, "V1009", path, UNDEFINED_ARGUMENT_MESSAGES.cursor);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
405
474
|
if (!isRecord(cursor) || orderBy === undefined) {
|
|
406
475
|
addIssue(issues, "V1009", path, "Cursor pagination requires an object cursor and explicit deterministic orderBy.");
|
|
407
476
|
return;
|
|
@@ -513,6 +582,9 @@ function validateRelationProjection(contract, edge, scope, value, path, issues)
|
|
|
513
582
|
if (!includes(edge.capabilities.filter, "nested")) {
|
|
514
583
|
addIssue(issues, "V1006", [...path, "where"], "Nested returned-relation filtering is not available on this edge.");
|
|
515
584
|
}
|
|
585
|
+
else if (value.where === undefined) {
|
|
586
|
+
addIssue(issues, "V1001", [...path, "where"], UNDEFINED_ARGUMENT_MESSAGES.where);
|
|
587
|
+
}
|
|
516
588
|
else {
|
|
517
589
|
const state = {
|
|
518
590
|
issues,
|
|
@@ -586,11 +658,16 @@ function validateScalarMutation(fieldName, field, value, stage, path, issues, tr
|
|
|
586
658
|
addIssue(issues, "V1001", path, "Scalar mutation action objects may contain only $-prefixed directive keys.");
|
|
587
659
|
return;
|
|
588
660
|
}
|
|
661
|
+
let unavailable = false;
|
|
589
662
|
for (const directive of directiveKeys) {
|
|
590
663
|
if (!includes(directives, directive)) {
|
|
664
|
+
unavailable = true;
|
|
591
665
|
addIssue(issues, "V1006", [...path, directive], `Mutation directive "${directive}" is not available on field "${fieldName}".`);
|
|
592
666
|
}
|
|
593
667
|
}
|
|
668
|
+
if (!unavailable && directiveKeys.length > 1) {
|
|
669
|
+
addIssue(issues, "V1001", path, "Scalar mutation action objects may contain only one directive.");
|
|
670
|
+
}
|
|
594
671
|
}
|
|
595
672
|
function validateRelationMutationEntry(contract, edge, directive, value, path, issues, transactionReferences) {
|
|
596
673
|
const target = edge.target;
|
|
@@ -680,7 +757,9 @@ function validateRelationMutationEntry(contract, edge, directive, value, path, i
|
|
|
680
757
|
};
|
|
681
758
|
validateWhere(contract, target, entry.where, [...path, "where"], state, 1);
|
|
682
759
|
}
|
|
683
|
-
validateMutationData(contract, target, entry.data, "update",
|
|
760
|
+
validateMutationData(contract, target, entry.data, "update", edge.cardinality === "many"
|
|
761
|
+
? TO_MANY_UPDATE_WRITE_SCOPE
|
|
762
|
+
: NESTED_WRITE_SCOPE, [...path, "data"], issues, transactionReferences);
|
|
684
763
|
}
|
|
685
764
|
return;
|
|
686
765
|
}
|
|
@@ -854,6 +933,9 @@ export function validateOperationArguments(contract, request, options) {
|
|
|
854
933
|
if (isUniqueOperation(request)) {
|
|
855
934
|
validateUniqueSelector(contract, request.resource, request.arguments.where, ["arguments", "where"], issues);
|
|
856
935
|
}
|
|
936
|
+
else if (request.arguments.where === undefined) {
|
|
937
|
+
addIssue(issues, "V1001", ["arguments", "where"], UNDEFINED_ARGUMENT_MESSAGES.where);
|
|
938
|
+
}
|
|
857
939
|
else {
|
|
858
940
|
const state = {
|
|
859
941
|
issues,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ErrorCodeGuidance } from "../runtime/error-guidance.js";
|
|
2
|
+
import type { AvValidationCode } from "./validation-code.js";
|
|
3
|
+
/** Cause and fix of every validation code. */
|
|
4
|
+
export declare const VALIDATION_CODE_GUIDANCE: {
|
|
5
|
+
readonly [C in AvValidationCode]: ErrorCodeGuidance;
|
|
6
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export const VALIDATION_CODE_GUIDANCE = {
|
|
2
|
+
V1000: {
|
|
3
|
+
cause: "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.",
|
|
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
|
+
},
|
|
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.",
|
|
8
|
+
fix: "Send the type the contract states at that path (the generated client's types say it).",
|
|
9
|
+
},
|
|
10
|
+
V1002: {
|
|
11
|
+
cause: "A value that is not a member of the enum, or outside a typed Json field's AvZ.enum or AvZ.literal.",
|
|
12
|
+
fix: "Send one of the enum's members.",
|
|
13
|
+
},
|
|
14
|
+
V1003: {
|
|
15
|
+
cause: 'A malformed datetime, decimal, base64 or UUID string, or a failed pattern or format validator (`check: "string_format"`).',
|
|
16
|
+
fix: "Send the canonical encoding (ISO-8601 UTC datetime, decimal string, base64 bytes) and satisfy the validator that check and params name.",
|
|
17
|
+
},
|
|
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.",
|
|
20
|
+
fix: "Send a value inside the range; check and params name the validator that failed.",
|
|
21
|
+
},
|
|
22
|
+
V1005: {
|
|
23
|
+
cause: "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.",
|
|
24
|
+
fix: "Use a field the caller's contract exposes, expose it through exemptions, or make the call from server code.",
|
|
25
|
+
},
|
|
26
|
+
V1006: {
|
|
27
|
+
cause: "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).",
|
|
28
|
+
fix: "Use what the contract lists for that field or Resource, or expose it through exemptions.",
|
|
29
|
+
},
|
|
30
|
+
V1007: {
|
|
31
|
+
cause: "The where of a unique operation is not exactly one complete identifier.",
|
|
32
|
+
fix: "Pass exactly one identifier: the id, or every field of one @unique or @@unique.",
|
|
33
|
+
},
|
|
34
|
+
V1008: {
|
|
35
|
+
cause: "Both select and include, a write-only field selected, or a relation that is not selectable there.",
|
|
36
|
+
fix: "Use select or include, not both, and select only readable fields.",
|
|
37
|
+
},
|
|
38
|
+
V1009: {
|
|
39
|
+
cause: "A cursor without an explicit orderBy, or cursor keys that do not match the ordered fields.",
|
|
40
|
+
fix: "Give an orderBy and a cursor holding exactly its ordered fields.",
|
|
41
|
+
},
|
|
42
|
+
V1010: {
|
|
43
|
+
cause: "A $ref path the earlier step does not return, a $ref to a step that is not in the transaction, or to a step with no single row (a many or count step).",
|
|
44
|
+
fix: "Select the referenced field in the earlier step, and reference a single-row step.",
|
|
45
|
+
},
|
|
46
|
+
V1011: {
|
|
47
|
+
cause: "The referenced value's type does not fit its target, a nullable source where only a non-null value is accepted included.",
|
|
48
|
+
fix: "Reference a field of the target's type, non-null where the target needs it.",
|
|
49
|
+
},
|
|
50
|
+
V1012: {
|
|
51
|
+
cause: "A nested write that does not fit the relation (a list where one record is expected), a reference field set together with its own relation, one record mixing reference fields and relation writes where the adapter cannot run both, or a record written through a relation that writes that relation (or a field holding its reference) back, where the adapter admits no such write.",
|
|
52
|
+
fix: "Write either the relation or its reference field, in the relation's cardinality.",
|
|
53
|
+
},
|
|
54
|
+
V1013: {
|
|
55
|
+
cause: "Nested projections, filters or writes deeper than maxNestingDepth.",
|
|
56
|
+
fix: "Nest less, or raise limits.maxNestingDepth.",
|
|
57
|
+
},
|
|
58
|
+
V1014: {
|
|
59
|
+
cause: "A limit above maxListLimit, or more steps than maxTransactionOperations.",
|
|
60
|
+
fix: "Paginate or split the plan, or raise the limit.",
|
|
61
|
+
},
|
|
62
|
+
V1015: {
|
|
63
|
+
cause: "A where tree with more nodes than maxBooleanNodes.",
|
|
64
|
+
fix: "Simplify the filter (in instead of many OR clauses), or raise limits.maxBooleanNodes.",
|
|
65
|
+
},
|
|
66
|
+
V1016: {
|
|
67
|
+
cause: "A $ref to the referencing step itself or a later one, or to an index no step has.",
|
|
68
|
+
fix: "Reference an earlier step only.",
|
|
69
|
+
},
|
|
70
|
+
V1017: {
|
|
71
|
+
cause: "A hand-written plan whose fingerprint does not match its step.",
|
|
72
|
+
fix: "Let the generated client, framework.appTx or framework.clientTx build the plan.",
|
|
73
|
+
},
|
|
74
|
+
V1018: {
|
|
75
|
+
cause: "The referenced step returned no value at that path, such as a first step that matched no row.",
|
|
76
|
+
fix: "Make sure the earlier step returns a row, or use a unique step, which fails with A2003.",
|
|
77
|
+
},
|
|
78
|
+
V1019: {
|
|
79
|
+
cause: "A later step uses a row that an earlier step's delete removed through a database cascade.",
|
|
80
|
+
fix: "Reorder or drop the dependent step.",
|
|
81
|
+
},
|
|
82
|
+
V1020: {
|
|
83
|
+
cause: "One framework.transaction([...]) list mixes appTx and clientTx steps.",
|
|
84
|
+
fix: "Build every step of a plan from one origin.",
|
|
85
|
+
},
|
|
86
|
+
V1021: {
|
|
87
|
+
cause: "A Json field's value nests deeper than maxJsonDepth.",
|
|
88
|
+
fix: "Flatten the value, or raise limits.maxJsonDepth.",
|
|
89
|
+
},
|
|
90
|
+
};
|
|
@@ -13,3 +13,7 @@ export declare function resolveWriteScope(resource: AvResource<AvContract> | und
|
|
|
13
13
|
* unrestricted one, at every depth, under every top-level scope.
|
|
14
14
|
*/
|
|
15
15
|
export declare const NESTED_WRITE_SCOPE: AvWriteScope;
|
|
16
|
+
/** The position a to-many `$update`'s `data` sits in: the flat-bulk write position. */
|
|
17
|
+
export declare const TO_MANY_UPDATE_WRITE_SCOPE: AvWriteScope;
|
|
18
|
+
/** The position of a relation's other end inside a record written through it. */
|
|
19
|
+
export declare const LOOPBACK_WRITE_SCOPE: AvWriteScope;
|
|
@@ -5,3 +5,5 @@ export function resolveWriteScope(resource, request) {
|
|
|
5
5
|
return writeScopeOf(lookupOwn(advertised, request.variant));
|
|
6
6
|
}
|
|
7
7
|
export const NESTED_WRITE_SCOPE = "nested";
|
|
8
|
+
export const TO_MANY_UPDATE_WRITE_SCOPE = "bulkWrite";
|
|
9
|
+
export const LOOPBACK_WRITE_SCOPE = "loopback";
|
|
@@ -1,16 +1,53 @@
|
|
|
1
|
+
import { copyAtDoor } from "../runtime/caller-data.door.js";
|
|
1
2
|
import { createDiagnosticsReporter, createReporter, } from "../runtime/diagnostics-reporter.js";
|
|
2
3
|
import { sanitizeOperationFailure } from "../runtime/error-boundary.js";
|
|
4
|
+
import { capturePlanAtCall } from "../transactions/transaction-plan-validator.js";
|
|
3
5
|
import { decodeOperationRequest, decodeTransactionRequest, isParsedByDecoder, } from "./http-request.decoder.js";
|
|
4
6
|
import { encodeOperationResponse, encodeTransactionResponse, } from "./http-response.encoder.js";
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
: context.execute;
|
|
7
|
+
function admitOperation(context, request) {
|
|
8
|
+
const owned = context.owned;
|
|
9
|
+
if (isParsedByDecoder(request.arguments)) {
|
|
10
|
+
return { request, run: owned?.execute ?? context.execute };
|
|
11
|
+
}
|
|
12
|
+
const copy = copyAtDoor(request.arguments);
|
|
13
|
+
if (owned === undefined) {
|
|
14
|
+
return {
|
|
15
|
+
request: copy.ok ? { ...request, arguments: copy.value } : request,
|
|
16
|
+
run: context.execute,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (copy.ok) {
|
|
20
|
+
return {
|
|
21
|
+
request: { ...request, arguments: copy.value },
|
|
22
|
+
run: (admitted) => owned.execute(admitted),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
request: { ...request, arguments: {} },
|
|
27
|
+
run: (admitted) => owned.execute(admitted, copy.error),
|
|
28
|
+
};
|
|
9
29
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
30
|
+
function admitTransaction(context, request) {
|
|
31
|
+
const owned = context.owned;
|
|
32
|
+
if (isParsedByDecoder(request.plan)) {
|
|
33
|
+
return {
|
|
34
|
+
request,
|
|
35
|
+
run: owned?.executeTransaction ?? context.executeTransaction,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const captured = capturePlanAtCall(request.plan);
|
|
39
|
+
if (owned === undefined) {
|
|
40
|
+
return {
|
|
41
|
+
request: captured.refused.size === 0
|
|
42
|
+
? { ...request, plan: captured.plan }
|
|
43
|
+
: request,
|
|
44
|
+
run: context.executeTransaction,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
request: { ...request, plan: captured.plan },
|
|
49
|
+
run: (admitted) => owned.executeTransaction(admitted, captured.refused.size === 0 ? undefined : captured.refused),
|
|
50
|
+
};
|
|
14
51
|
}
|
|
15
52
|
async function seededState(options, report, view) {
|
|
16
53
|
if (options?.state === undefined) {
|
|
@@ -48,11 +85,12 @@ export async function handleOperationRequest(context, request, options) {
|
|
|
48
85
|
if (!decoded.ok) {
|
|
49
86
|
return decoded.response;
|
|
50
87
|
}
|
|
51
|
-
const
|
|
88
|
+
const admitted = admitOperation(context, decoded.request);
|
|
89
|
+
const seeded = await seededState(options, createDiagnosticsReporter(context.sink, admitted.request), { path: ["arguments"], arguments: admitted.request.arguments });
|
|
52
90
|
const response = seeded.ok
|
|
53
|
-
? await
|
|
54
|
-
?
|
|
55
|
-
: { ...
|
|
91
|
+
? await admitted.run(seeded.state === undefined
|
|
92
|
+
? admitted.request
|
|
93
|
+
: { ...admitted.request, state: seeded.state })
|
|
56
94
|
: seeded.response;
|
|
57
95
|
return encodeOperationResponse(response, decoded.requestId, operationUnencodableReport(context.sink, decoded.request));
|
|
58
96
|
}
|
|
@@ -61,15 +99,16 @@ export async function handleTransactionRequest(context, request, options) {
|
|
|
61
99
|
if (!decoded.ok) {
|
|
62
100
|
return decoded.response;
|
|
63
101
|
}
|
|
102
|
+
const admitted = admitTransaction(context, decoded.request);
|
|
64
103
|
const seeded = await seededState(options, createReporter(context.sink, {
|
|
65
104
|
kind: "transaction",
|
|
66
105
|
origin: context.origin,
|
|
67
106
|
requestId: decoded.requestId,
|
|
68
|
-
}), { path: ["plan"], arguments:
|
|
107
|
+
}), { path: ["plan"], arguments: admitted.request.plan });
|
|
69
108
|
const response = seeded.ok
|
|
70
|
-
? await
|
|
71
|
-
?
|
|
72
|
-
: { ...
|
|
109
|
+
? await admitted.run(seeded.state === undefined
|
|
110
|
+
? admitted.request
|
|
111
|
+
: { ...admitted.request, state: seeded.state })
|
|
73
112
|
: seeded.response;
|
|
74
113
|
return encodeTransactionResponse(response, decoded.requestId, planUnencodableReport(context.sink, decoded.requestId, context.origin));
|
|
75
114
|
}
|
|
@@ -41,6 +41,22 @@ export type CallerDataPolicy = {
|
|
|
41
41
|
readonly keep?: CallerDataKeep;
|
|
42
42
|
readonly mirror?: CallerDataMirror;
|
|
43
43
|
};
|
|
44
|
+
/** The members a structural mirror copies by value rather than mirroring. */
|
|
45
|
+
type MirrorMembers = {
|
|
46
|
+
/** The policy a member at `path` (from the mirror's root) is copied by, or `undefined`. */
|
|
47
|
+
readonly copyAt: (path: CallerDataPath) => CallerDataPolicy | undefined;
|
|
48
|
+
/** Receives a member whose copy failed; the mirror holds `undefined` for it. */
|
|
49
|
+
readonly refused: (path: CallerDataPath, error: AvCallerDataError) => void;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Mirrors `value` by its descriptors, as a `$ref` control object is mirrored (no
|
|
53
|
+
* getter runs, an accessor stays a tripwire), except the members `members.copyAt`
|
|
54
|
+
* names by their path: each is copied by value under the policy it returns, and one
|
|
55
|
+
* whose copy fails is reported to `members.refused` and held as `undefined`.
|
|
56
|
+
*
|
|
57
|
+
* @throws {@link AvCallerDataError} at `[]` when `value` itself cannot be reflected.
|
|
58
|
+
*/
|
|
59
|
+
export declare function mirrorCallerData(value: object, members: MirrorMembers): unknown;
|
|
44
60
|
/**
|
|
45
61
|
* Places a mirror tripwire's failure in the value the door copied: its path from the
|
|
46
62
|
* copy's root, where `error.path` started from the mirror. A failure that is not a
|
|
@@ -57,3 +73,4 @@ export declare function locateCallerDataError(error: AvCallerDataError, copy: un
|
|
|
57
73
|
export declare function copyCallerData(value: unknown, policy?: CallerDataPolicy): unknown;
|
|
58
74
|
/** The `V1001` message a door answers a {@link AvCallerDataError} with. */
|
|
59
75
|
export declare function callerDataFailureMessage(kind: CallerDataFailureKind): string;
|
|
76
|
+
export {};
|
|
@@ -268,7 +268,7 @@ function pathSegment(array, key) {
|
|
|
268
268
|
}
|
|
269
269
|
return key;
|
|
270
270
|
}
|
|
271
|
-
function mirrorControlData(value) {
|
|
271
|
+
function mirrorControlData(value, members) {
|
|
272
272
|
const shape = reflectNode(value);
|
|
273
273
|
if (shape === undefined) {
|
|
274
274
|
throw new AvCallerDataError("unreadable-value", []);
|
|
@@ -375,6 +375,20 @@ function mirrorControlData(value) {
|
|
|
375
375
|
continue;
|
|
376
376
|
}
|
|
377
377
|
const memberLink = childLink(link, pathSegment(shape.array, key));
|
|
378
|
+
const byValue = members?.copyAt(pathOf(memberLink));
|
|
379
|
+
if (byValue !== undefined) {
|
|
380
|
+
try {
|
|
381
|
+
store(copyCallerData(member, byValue));
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
if (!(error instanceof AvCallerDataError)) {
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
members?.refused(pathOf(memberLink), error);
|
|
388
|
+
store(undefined);
|
|
389
|
+
}
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
378
392
|
const memberShape = reflectNode(member);
|
|
379
393
|
if (memberShape === undefined) {
|
|
380
394
|
store(UNREADABLE);
|
|
@@ -408,6 +422,9 @@ function mirrorControlData(value) {
|
|
|
408
422
|
}
|
|
409
423
|
return root;
|
|
410
424
|
}
|
|
425
|
+
export function mirrorCallerData(value, members) {
|
|
426
|
+
return mirrorControlData(value, members);
|
|
427
|
+
}
|
|
411
428
|
export function locateCallerDataError(error, copy) {
|
|
412
429
|
const within = error.within;
|
|
413
430
|
if (within === undefined) {
|
|
@@ -6,13 +6,19 @@ import type { AvMisuseEvent, MisuseReporter } from "./misuse-reporter.js";
|
|
|
6
6
|
*
|
|
7
7
|
* - `"caller"`: a server-side caller's own objects — copied at the door.
|
|
8
8
|
* - `"owned"`: data no caller holds a reference to — a body the protocol decoder
|
|
9
|
-
* parsed with `JSON.parse` for this request alone,
|
|
10
|
-
*
|
|
9
|
+
* parsed with `JSON.parse` for this request alone, a host's parsed body the
|
|
10
|
+
* protocol's shortcut copied at the call, or a plan the typed runner assembled
|
|
11
|
+
* from copies its builders took. It is never copied again.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* - `{ refused }`: a body the protocol's shortcut copied at the call, whose copy
|
|
14
|
+
* failed — answered at the door as `"caller"` would answer it, never read again.
|
|
15
|
+
*
|
|
16
|
+
* Only core's own modules choose anything but `"caller"`: the public
|
|
17
|
+
* `framework.execute` and `executeTransaction` always pass `"caller"`.
|
|
14
18
|
*/
|
|
15
|
-
export type ArgumentsSource = "caller" | "owned"
|
|
19
|
+
export type ArgumentsSource = "caller" | "owned" | {
|
|
20
|
+
readonly refused: AvCallerDataError;
|
|
21
|
+
};
|
|
16
22
|
/** The operation identity a misuse event may name, as far as it could be read. */
|
|
17
23
|
export type MisuseIdentity = Omit<AvMisuseEvent, "kind" | "path">;
|
|
18
24
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
|
|
2
2
|
import { AvComputedValueError } from "./computed-value.error.js";
|
|
3
|
+
import { frozenCopy } from "./readonly-value.js";
|
|
3
4
|
import { normalizeScalarFieldValue } from "./scalar-runtime.js";
|
|
4
5
|
function isRecord(value) {
|
|
5
6
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -36,6 +37,12 @@ function appendUniqueProjection(values, value) {
|
|
|
36
37
|
values.push(value);
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
function adapterIdentifier(model, resource) {
|
|
41
|
+
const resources = model.resources;
|
|
42
|
+
return ((Object.hasOwn(resources, resource)
|
|
43
|
+
? resources[resource]?.identifiers?.[0]
|
|
44
|
+
: undefined) ?? []);
|
|
45
|
+
}
|
|
39
46
|
function supportsProjection(request) {
|
|
40
47
|
if (request.family === "upsert") {
|
|
41
48
|
return true;
|
|
@@ -51,6 +58,11 @@ function addScalarProjection(core, model, active, compute, resourceName, fieldNa
|
|
|
51
58
|
const stage = readCompute(compute?.[resourceName]?.[fieldName]?.read);
|
|
52
59
|
if (stage !== undefined) {
|
|
53
60
|
appendUnique(computed, fieldName);
|
|
61
|
+
if (stage.dependsOn.length === 0) {
|
|
62
|
+
for (const member of adapterIdentifier(model, resourceName)) {
|
|
63
|
+
appendUniqueProjection(adapterSelect, member);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
54
66
|
for (const dependency of stage.dependsOn) {
|
|
55
67
|
if (storedScalar(model, resourceName, dependency)) {
|
|
56
68
|
appendUniqueProjection(adapterSelect, dependency);
|
|
@@ -307,10 +319,11 @@ async function materializeRecord(active, compute, request, plan, value, contextF
|
|
|
307
319
|
if (field?.kind !== "scalar" || stage === undefined) {
|
|
308
320
|
continue;
|
|
309
321
|
}
|
|
322
|
+
const view = frozenCopy(record);
|
|
310
323
|
const computedValue = await stage.compute({
|
|
311
324
|
field: fieldName,
|
|
312
|
-
value:
|
|
313
|
-
record,
|
|
325
|
+
value: view[fieldName],
|
|
326
|
+
record: view,
|
|
314
327
|
ctx: contextFactory(),
|
|
315
328
|
});
|
|
316
329
|
if (computedValue === undefined) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { AvValidationIssue } from "../operations/validation-code.js";
|
|
2
2
|
/**
|
|
3
3
|
* What the diagnostics channel receives when a computed value — a read or write
|
|
4
|
-
* compute stage's return — is one its Contract refuses
|
|
4
|
+
* compute stage's return — is one its Contract refuses, or a `compute.create`
|
|
5
|
+
* stage returned `undefined` for a field a create cannot do without (rc.2): the developer's broken code,
|
|
5
6
|
* never the caller's. It is thrown, not a deliberate outcome, so the framework's
|
|
6
7
|
* boundary answers the caller the generic `A3000` and reports this error, with the
|
|
7
8
|
* issues, exactly once.
|
|
@@ -9,5 +10,5 @@ import type { AvValidationIssue } from "../operations/validation-code.js";
|
|
|
9
10
|
export declare class AvComputedValueError extends Error {
|
|
10
11
|
readonly issues: readonly AvValidationIssue[];
|
|
11
12
|
readonly name = "AvComputedValueError";
|
|
12
|
-
constructor(resource: string, field: string, stage: "read" | "create" | "update", issues: readonly AvValidationIssue[]);
|
|
13
|
+
constructor(resource: string, field: string, stage: "read" | "create" | "update", issues: readonly AvValidationIssue[], message?: string);
|
|
13
14
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export class AvComputedValueError extends Error {
|
|
2
2
|
issues;
|
|
3
3
|
name = "AvComputedValueError";
|
|
4
|
-
constructor(resource, field, stage, issues) {
|
|
4
|
+
constructor(resource, field, stage, issues, message) {
|
|
5
5
|
const [first] = issues;
|
|
6
|
-
super(
|
|
6
|
+
super(message ??
|
|
7
|
+
`The compute.${stage} stage of ${resource}.${field} returned a value its Contract refuses${first === undefined ? "." : ` (${first.code}: ${first.message})`}`);
|
|
7
8
|
this.issues = issues;
|
|
8
9
|
}
|
|
9
10
|
}
|
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
import type { AvContract } from "../contracts/contract.js";
|
|
2
|
+
import type { AvCreateRequirements } from "../contracts/create-requirements.js";
|
|
2
3
|
import type { AvOperationIdentity } from "../operations/operation-identity.js";
|
|
3
4
|
import type { AvValidationIssue } from "../operations/validation-code.js";
|
|
4
5
|
import type { ReferentialActionTable } from "./referential-action.js";
|
|
5
6
|
type Path = readonly (string | number)[];
|
|
7
|
+
/** One required field a `compute.create` stage was to fill and left missing. */
|
|
8
|
+
export type ComputedRequirementGap = {
|
|
9
|
+
readonly resource: string;
|
|
10
|
+
readonly field: string;
|
|
11
|
+
readonly issue: AvValidationIssue;
|
|
12
|
+
};
|
|
6
13
|
/**
|
|
7
|
-
* What the Adapter stated
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
14
|
+
* What the Adapter stated that the checks read: its referential edges, for the
|
|
15
|
+
* fields that hold each reference, and its own create requirements per
|
|
16
|
+
* Resource, before any layer projected them (what write compute must not leave
|
|
17
|
+
* missing). Read once when the Framework is constructed. The requirements the
|
|
18
|
+
* caller is held to are the active Contract's.
|
|
11
19
|
*/
|
|
12
20
|
export type RecordWriteFacts = {
|
|
13
21
|
readonly references: ReferentialActionTable;
|
|
22
|
+
readonly stated: (resource: string) => AvCreateRequirements | undefined;
|
|
14
23
|
};
|
|
15
24
|
/** The facts an Adapter stated, read off it once; an absent table states nothing. */
|
|
16
25
|
export declare function recordWriteFactsOf(adapter: {
|
|
17
26
|
readonly referentialActions?: ReferentialActionTable;
|
|
27
|
+
readonly model?: {
|
|
28
|
+
readonly resources: Readonly<Record<string, unknown>>;
|
|
29
|
+
};
|
|
18
30
|
}): RecordWriteFacts;
|
|
19
31
|
/**
|
|
20
32
|
* The `V1000` issues for every create position in one operation's arguments
|
|
@@ -24,7 +36,31 @@ export declare function recordWriteFactsOf(adapter: {
|
|
|
24
36
|
* reference both, in argument order. Empty when there are none. Total over
|
|
25
37
|
* malformed input.
|
|
26
38
|
*/
|
|
27
|
-
export declare function createRequirementIssues(contract: AvContract, facts: RecordWriteFacts, request: AvOperationIdentity & {
|
|
39
|
+
export declare function createRequirementIssues(contract: AvContract, facts: Pick<RecordWriteFacts, "references">, request: AvOperationIdentity & {
|
|
28
40
|
readonly resource: string;
|
|
29
41
|
}, args: unknown, argumentsPath: Path): readonly AvValidationIssue[];
|
|
42
|
+
/**
|
|
43
|
+
* The `V1000` issues for every create position of one operation's arguments
|
|
44
|
+
* that lacks a value the active Contract requires, and the `V1012` issues for
|
|
45
|
+
* every write record that sets a relation and its reference both, on the
|
|
46
|
+
* arguments as the caller sent them, before any pipeline stage runs. Empty when
|
|
47
|
+
* there are none.
|
|
48
|
+
*/
|
|
49
|
+
export declare function preliminaryRecordIssues(contract: AvContract, facts: Pick<RecordWriteFacts, "references">, request: AvOperationIdentity & {
|
|
50
|
+
readonly resource: string;
|
|
51
|
+
}, args: unknown, argumentsPath: Path): readonly AvValidationIssue[];
|
|
52
|
+
/**
|
|
53
|
+
* Every required field a `compute.create` stage was to fill and that is still
|
|
54
|
+
* missing after write compute, at every create position of one operation's
|
|
55
|
+
* arguments (`create.*`'s `data`, `upsert`'s `create`, every nested create),
|
|
56
|
+
* in argument order, read against the Adapter's own statement of each
|
|
57
|
+
* Resource's create requirements; `computed` names the fields a Resource's
|
|
58
|
+
* `compute.create` stages fill.
|
|
59
|
+
* A field the parent supplies to a nested create, a group whose relation the
|
|
60
|
+
* record writes, and a value the caller sent are not missing. Empty when
|
|
61
|
+
* nothing is.
|
|
62
|
+
*/
|
|
63
|
+
export declare function computedRequirementGapsOf(contract: AvContract, facts: RecordWriteFacts, request: AvOperationIdentity & {
|
|
64
|
+
readonly resource: string;
|
|
65
|
+
}, args: unknown, argumentsPath: Path, computed: (resource: string) => ReadonlySet<string>): readonly ComputedRequirementGap[];
|
|
30
66
|
export {};
|