@aventara/core 1.0.0-rc.1 → 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 +341 -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 +50 -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 +145 -36
- 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 +18 -1
- package/dist/runtime/field-validator.runtime.js +47 -8
- 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 +82 -36
- 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,26 @@ 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
|
+
};
|
|
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
|
+
}
|
|
252
313
|
function validateWhere(contract, resourceName, where, path, state, depth) {
|
|
253
314
|
if (!checkDepth(contract, state, depth)) {
|
|
254
315
|
return;
|
|
255
316
|
}
|
|
317
|
+
if (where === undefined) {
|
|
318
|
+
state.issues.push(unsentReadValueIssue(path));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
256
321
|
if (!isRecord(where)) {
|
|
257
322
|
addIssue(state.issues, "V1001", path, "where must be an object.");
|
|
258
323
|
return;
|
|
@@ -263,6 +328,13 @@ function validateWhere(contract, resourceName, where, path, state, depth) {
|
|
|
263
328
|
return;
|
|
264
329
|
}
|
|
265
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
|
+
}
|
|
266
338
|
if (key === "AND" || key === "OR") {
|
|
267
339
|
countFilterNode(contract, state);
|
|
268
340
|
if (!Array.isArray(value)) {
|
|
@@ -270,6 +342,10 @@ function validateWhere(contract, resourceName, where, path, state, depth) {
|
|
|
270
342
|
continue;
|
|
271
343
|
}
|
|
272
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
|
+
}
|
|
273
349
|
validateWhere(contract, resourceName, entry, [...path, key, index], state, depth + 1);
|
|
274
350
|
});
|
|
275
351
|
continue;
|
|
@@ -366,7 +442,9 @@ function validateOrderBy(contract, resourceName, orderBy, path, issues) {
|
|
|
366
442
|
valueKeys[1] !== "sort" ||
|
|
367
443
|
(value.sort !== "asc" && value.sort !== "desc") ||
|
|
368
444
|
(value.nulls !== "first" && value.nulls !== "last")) {
|
|
369
|
-
addIssue(issues, "V1001", fieldPath,
|
|
445
|
+
addIssue(issues, "V1001", fieldPath, Object.hasOwn(value, "$ref")
|
|
446
|
+
? `A transaction reference cannot stand in orderBy: order "${fieldName}" by "asc", "desc" or { sort, nulls }.`
|
|
447
|
+
: `Null ordering for "${fieldName}" must be exactly { sort: "asc" | "desc", nulls: "first" | "last" }.`);
|
|
370
448
|
continue;
|
|
371
449
|
}
|
|
372
450
|
if (!field.nullable || field.capabilities.orderNulls !== true) {
|
|
@@ -393,6 +471,10 @@ function validateOrderBy(contract, resourceName, orderBy, path, issues) {
|
|
|
393
471
|
return analysis;
|
|
394
472
|
}
|
|
395
473
|
function validatePaginationValue(contract, name, value, path, issues) {
|
|
474
|
+
if (value === undefined) {
|
|
475
|
+
addIssue(issues, "V1001", path, UNDEFINED_ARGUMENT_MESSAGES[name]);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
396
478
|
if (!Number.isInteger(value) || typeof value !== "number" || value < 0) {
|
|
397
479
|
addIssue(issues, "V1001", path, `${name} must be a non-negative integer.`);
|
|
398
480
|
return;
|
|
@@ -402,6 +484,10 @@ function validatePaginationValue(contract, name, value, path, issues) {
|
|
|
402
484
|
}
|
|
403
485
|
}
|
|
404
486
|
function validateCursor(contract, resourceName, cursor, orderBy, path, issues) {
|
|
487
|
+
if (cursor === undefined) {
|
|
488
|
+
addIssue(issues, "V1009", path, UNDEFINED_ARGUMENT_MESSAGES.cursor);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
405
491
|
if (!isRecord(cursor) || orderBy === undefined) {
|
|
406
492
|
addIssue(issues, "V1009", path, "Cursor pagination requires an object cursor and explicit deterministic orderBy.");
|
|
407
493
|
return;
|
|
@@ -513,6 +599,9 @@ function validateRelationProjection(contract, edge, scope, value, path, issues)
|
|
|
513
599
|
if (!includes(edge.capabilities.filter, "nested")) {
|
|
514
600
|
addIssue(issues, "V1006", [...path, "where"], "Nested returned-relation filtering is not available on this edge.");
|
|
515
601
|
}
|
|
602
|
+
else if (value.where === undefined) {
|
|
603
|
+
addIssue(issues, "V1001", [...path, "where"], UNDEFINED_ARGUMENT_MESSAGES.where);
|
|
604
|
+
}
|
|
516
605
|
else {
|
|
517
606
|
const state = {
|
|
518
607
|
issues,
|
|
@@ -586,11 +675,16 @@ function validateScalarMutation(fieldName, field, value, stage, path, issues, tr
|
|
|
586
675
|
addIssue(issues, "V1001", path, "Scalar mutation action objects may contain only $-prefixed directive keys.");
|
|
587
676
|
return;
|
|
588
677
|
}
|
|
678
|
+
let unavailable = false;
|
|
589
679
|
for (const directive of directiveKeys) {
|
|
590
680
|
if (!includes(directives, directive)) {
|
|
681
|
+
unavailable = true;
|
|
591
682
|
addIssue(issues, "V1006", [...path, directive], `Mutation directive "${directive}" is not available on field "${fieldName}".`);
|
|
592
683
|
}
|
|
593
684
|
}
|
|
685
|
+
if (!unavailable && directiveKeys.length > 1) {
|
|
686
|
+
addIssue(issues, "V1001", path, "Scalar mutation action objects may contain only one directive.");
|
|
687
|
+
}
|
|
594
688
|
}
|
|
595
689
|
function validateRelationMutationEntry(contract, edge, directive, value, path, issues, transactionReferences) {
|
|
596
690
|
const target = edge.target;
|
|
@@ -610,8 +704,8 @@ function validateRelationMutationEntry(contract, edge, directive, value, path, i
|
|
|
610
704
|
addIssue(issues, "V1012", path, "To-many $disconnect requires one or more unique identifiers.");
|
|
611
705
|
return;
|
|
612
706
|
}
|
|
613
|
-
for (const selector of
|
|
614
|
-
validateUniqueSelector(contract, target, selector,
|
|
707
|
+
for (const { entry: selector, at } of entriesOf(edge, value, path)) {
|
|
708
|
+
validateUniqueSelector(contract, target, selector, at, issues);
|
|
615
709
|
}
|
|
616
710
|
return;
|
|
617
711
|
}
|
|
@@ -626,82 +720,81 @@ function validateRelationMutationEntry(contract, edge, directive, value, path, i
|
|
|
626
720
|
addIssue(issues, "V1012", path, "To-many $set requires an identifier array.");
|
|
627
721
|
return;
|
|
628
722
|
}
|
|
629
|
-
for (const selector of value) {
|
|
630
|
-
validateUniqueSelector(contract, target, selector,
|
|
723
|
+
for (const { entry: selector, at } of entriesOf(edge, value, path)) {
|
|
724
|
+
validateUniqueSelector(contract, target, selector, at, issues);
|
|
631
725
|
}
|
|
632
726
|
return;
|
|
633
727
|
}
|
|
634
728
|
if (directive === "$connect") {
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
validateUniqueSelector(contract, target, selector, path, issues);
|
|
729
|
+
for (const { entry: selector, at } of entriesOf(edge, value, path)) {
|
|
730
|
+
validateUniqueSelector(contract, target, selector, at, issues);
|
|
638
731
|
}
|
|
639
732
|
return;
|
|
640
733
|
}
|
|
641
734
|
if (directive === "$create") {
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
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);
|
|
645
737
|
}
|
|
646
738
|
return;
|
|
647
739
|
}
|
|
648
740
|
if (directive === "$connectOrCreate") {
|
|
649
|
-
const
|
|
650
|
-
for (const entry of entries) {
|
|
741
|
+
for (const { entry, at } of entriesOf(edge, value, path)) {
|
|
651
742
|
if (!isRecord(entry) || !("where" in entry) || !("create" in entry)) {
|
|
652
|
-
addIssue(issues, "V1012",
|
|
743
|
+
addIssue(issues, "V1012", at, "$connectOrCreate requires where and create.");
|
|
653
744
|
continue;
|
|
654
745
|
}
|
|
655
|
-
validateUniqueSelector(contract, target, entry.where, [...
|
|
656
|
-
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);
|
|
657
748
|
}
|
|
658
749
|
return;
|
|
659
750
|
}
|
|
660
751
|
if (directive === "$update") {
|
|
661
|
-
const
|
|
662
|
-
for (const entry of entries) {
|
|
752
|
+
for (const { entry, at } of entriesOf(edge, value, path)) {
|
|
663
753
|
if (!isRecord(entry) || !("data" in entry)) {
|
|
664
|
-
addIssue(issues, "V1012",
|
|
754
|
+
addIssue(issues, "V1012", at, "$update requires a data object.");
|
|
665
755
|
continue;
|
|
666
756
|
}
|
|
667
757
|
if (edge.cardinality === "many" && !("where" in entry)) {
|
|
668
|
-
addIssue(issues, "V1012",
|
|
758
|
+
addIssue(issues, "V1012", at, "To-many $update requires where.");
|
|
669
759
|
continue;
|
|
670
760
|
}
|
|
671
761
|
if ("where" in entry) {
|
|
672
762
|
const state = {
|
|
673
763
|
issues,
|
|
674
|
-
structuralRoot: [...
|
|
764
|
+
structuralRoot: [...at, "where"],
|
|
675
765
|
transactionReferences,
|
|
676
766
|
scalarOnly: edge.capabilities.nestedWriteFilter === "scalar",
|
|
767
|
+
...(edge.cardinality === "many" ? { refuseEmptyNodes: true } : {}),
|
|
677
768
|
booleanFilterNodes: 0,
|
|
678
769
|
nestingExceeded: false,
|
|
679
770
|
booleanNodesExceeded: false,
|
|
680
771
|
};
|
|
681
|
-
validateWhere(contract, target, entry.where, [...
|
|
772
|
+
validateWhere(contract, target, entry.where, [...at, "where"], state, 1);
|
|
682
773
|
}
|
|
683
|
-
validateMutationData(contract, target, entry.data, "update",
|
|
774
|
+
validateMutationData(contract, target, entry.data, "update", edge.cardinality === "many"
|
|
775
|
+
? TO_MANY_UPDATE_WRITE_SCOPE
|
|
776
|
+
: NESTED_WRITE_SCOPE, [...at, "data"], issues, transactionReferences);
|
|
684
777
|
}
|
|
685
778
|
return;
|
|
686
779
|
}
|
|
687
780
|
if (directive === "$delete") {
|
|
688
|
-
const
|
|
689
|
-
for (const entry of entries) {
|
|
781
|
+
for (const { entry, at } of entriesOf(edge, value, path)) {
|
|
690
782
|
if (edge.cardinality === "many") {
|
|
691
783
|
if (!isRecord(entry) || !("where" in entry)) {
|
|
692
|
-
addIssue(issues, "V1012",
|
|
784
|
+
addIssue(issues, "V1012", at, "To-many $delete requires where.");
|
|
693
785
|
continue;
|
|
694
786
|
}
|
|
695
787
|
const state = {
|
|
696
788
|
issues,
|
|
697
|
-
structuralRoot: [...
|
|
789
|
+
structuralRoot: [...at, "where"],
|
|
698
790
|
transactionReferences,
|
|
699
791
|
scalarOnly: edge.capabilities.nestedWriteFilter === "scalar",
|
|
792
|
+
refuseEmptyNodes: true,
|
|
700
793
|
booleanFilterNodes: 0,
|
|
701
794
|
nestingExceeded: false,
|
|
702
795
|
booleanNodesExceeded: false,
|
|
703
796
|
};
|
|
704
|
-
validateWhere(contract, target, entry.where, [...
|
|
797
|
+
validateWhere(contract, target, entry.where, [...at, "where"], state, 1);
|
|
705
798
|
}
|
|
706
799
|
else if (entry !== true &&
|
|
707
800
|
(!isRecord(entry) || Object.keys(entry).some((key) => key !== "where"))) {
|
|
@@ -723,23 +816,27 @@ function validateRelationMutationEntry(contract, edge, directive, value, path, i
|
|
|
723
816
|
return;
|
|
724
817
|
}
|
|
725
818
|
if (directive === "$upsert") {
|
|
726
|
-
const
|
|
727
|
-
for (const entry of entries) {
|
|
819
|
+
for (const { entry, at } of entriesOf(edge, value, path)) {
|
|
728
820
|
if (!isRecord(entry) ||
|
|
729
821
|
!("create" in entry) ||
|
|
730
822
|
!("update" in entry) ||
|
|
731
823
|
(edge.cardinality === "many" && !("where" in entry))) {
|
|
732
|
-
addIssue(issues, "V1012",
|
|
824
|
+
addIssue(issues, "V1012", at, "$upsert requires create/update and requires where on to-many relations.");
|
|
733
825
|
continue;
|
|
734
826
|
}
|
|
735
827
|
if ("where" in entry) {
|
|
736
|
-
validateUniqueSelector(contract, target, entry.where, [...
|
|
828
|
+
validateUniqueSelector(contract, target, entry.where, [...at, "where"], issues);
|
|
737
829
|
}
|
|
738
|
-
validateMutationData(contract, target, entry.create, "create", NESTED_WRITE_SCOPE, [...
|
|
739
|
-
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);
|
|
740
832
|
}
|
|
741
833
|
}
|
|
742
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
|
+
}
|
|
743
840
|
function validateRelationMutation(contract, fieldName, field, value, stage, scope, path, issues, transactionReferences) {
|
|
744
841
|
const directives = advertisedDirectivesAt(field.capabilities[stage], scope);
|
|
745
842
|
if (directives === undefined) {
|
|
@@ -817,6 +914,14 @@ function validateMutationArguments(contract, resourceName, request, args, issues
|
|
|
817
914
|
}
|
|
818
915
|
}
|
|
819
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
|
+
}
|
|
820
925
|
function isUniqueOperation(request) {
|
|
821
926
|
return (request.variant === "unique" &&
|
|
822
927
|
(request.family === "find" ||
|
|
@@ -854,11 +959,15 @@ export function validateOperationArguments(contract, request, options) {
|
|
|
854
959
|
if (isUniqueOperation(request)) {
|
|
855
960
|
validateUniqueSelector(contract, request.resource, request.arguments.where, ["arguments", "where"], issues);
|
|
856
961
|
}
|
|
962
|
+
else if (request.arguments.where === undefined) {
|
|
963
|
+
addIssue(issues, "V1001", ["arguments", "where"], UNDEFINED_ARGUMENT_MESSAGES.where);
|
|
964
|
+
}
|
|
857
965
|
else {
|
|
858
966
|
const state = {
|
|
859
967
|
issues,
|
|
860
968
|
structuralRoot: ["arguments", "where"],
|
|
861
969
|
transactionReferences: options?.transactionReferences === true,
|
|
970
|
+
...emptyNodeRefusalOf(request),
|
|
862
971
|
booleanFilterNodes: 0,
|
|
863
972
|
nestingExceeded: false,
|
|
864
973
|
booleanNodesExceeded: false,
|
|
@@ -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. 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
|
+
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. 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
|
+
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 {};
|