@aventara/core 0.1.0-pilot.4 → 1.0.0-rc.0
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 +338 -0
- package/README.md +100 -3
- package/dist/adapters/adapter-model.d.ts +8 -0
- package/dist/adapters/adapter.interface.d.ts +6 -2
- package/dist/compiler/contract-compiler-diagnostic.d.ts +6 -0
- package/dist/compiler/contract.compiler.js +67 -0
- package/dist/config/config.resolver.d.ts +3 -0
- package/dist/config/config.resolver.js +1 -0
- package/dist/config/config.validator.js +6 -0
- package/dist/config/define-fields.d.ts +10 -3
- package/dist/config/framework-config.d.ts +4 -1
- package/dist/config/framework-fields-config.d.ts +6 -0
- package/dist/contracts/client-contract-structure.validator.d.ts +21 -1
- package/dist/contracts/client-contract-structure.validator.js +99 -2
- package/dist/contracts/contract-canonicalizer.js +32 -0
- package/dist/contracts/contract.d.ts +7 -0
- package/dist/contracts/field.d.ts +10 -0
- package/dist/contracts/json-shape.d.ts +65 -0
- package/dist/contracts/json-shape.js +1 -0
- package/dist/contracts/reference-write-limit.d.ts +7 -0
- package/dist/contracts/reference-write-limit.js +1 -0
- package/dist/contracts/scalar-value-type.d.ts +12 -1
- package/dist/framework/contract-view.d.ts +126 -21
- package/dist/framework/framework.d.ts +5 -3
- package/dist/framework/framework.factory.js +56 -13
- package/dist/index.d.ts +8 -40
- package/dist/index.js +0 -20
- package/dist/internal.d.ts +35 -0
- package/dist/internal.js +16 -0
- package/dist/json/avz.d.ts +96 -0
- package/dist/json/avz.js +212 -0
- package/dist/json/index.d.ts +6 -0
- package/dist/json/index.js +1 -0
- package/dist/json/json-shape.registry.d.ts +22 -0
- package/dist/json/json-shape.registry.js +16 -0
- package/dist/json/zod-issue.translator.d.ts +51 -0
- package/dist/json/zod-issue.translator.js +142 -0
- package/dist/operations/operation-arguments.d.ts +151 -101
- package/dist/operations/operation-identity.d.ts +2 -0
- package/dist/operations/operation-identity.js +3 -0
- package/dist/operations/operation-validator-core.d.ts +1 -1
- package/dist/operations/operation-validator-core.js +28 -10
- package/dist/operations/operation-validator.d.ts +20 -5
- package/dist/operations/operation-validator.js +26 -10
- package/dist/operations/projection-scope.resolver.d.ts +1 -1
- package/dist/operations/structural-limits.walker.d.ts +19 -0
- package/dist/operations/structural-limits.walker.js +344 -0
- package/dist/protocol/absent-route.responder.d.ts +2 -1
- package/dist/protocol/absent-route.responder.js +32 -6
- package/dist/protocol/http-request.decoder.d.ts +6 -0
- package/dist/protocol/http-request.decoder.js +11 -3
- package/dist/protocol/protocol.binder.d.ts +4 -2
- package/dist/protocol/protocol.binder.js +3 -1
- package/dist/protocol/protocol.handler.d.ts +6 -2
- package/dist/protocol/protocol.handler.js +14 -4
- package/dist/protocol/unmounted-route.error.d.ts +19 -0
- package/dist/protocol/unmounted-route.error.js +10 -0
- package/dist/runtime/caller-data.copier.d.ts +59 -0
- package/dist/runtime/caller-data.copier.js +470 -0
- package/dist/runtime/caller-data.door.d.ts +56 -0
- package/dist/runtime/caller-data.door.js +65 -0
- package/dist/runtime/computed-read-runtime.js +3 -10
- package/dist/runtime/computed-value.error.d.ts +13 -0
- package/dist/runtime/computed-value.error.js +9 -0
- package/dist/runtime/create-requirement.d.ts +43 -7
- package/dist/runtime/create-requirement.js +89 -9
- package/dist/runtime/diagnostics-reporter.d.ts +20 -4
- package/dist/runtime/diagnostics-reporter.js +11 -4
- package/dist/runtime/error-boundary.d.ts +8 -0
- package/dist/runtime/error-boundary.js +65 -35
- package/dist/runtime/field-behavior.d.ts +2 -2
- package/dist/runtime/framework-error.d.ts +12 -0
- package/dist/runtime/misuse-reporter.d.ts +32 -0
- package/dist/runtime/misuse-reporter.js +19 -0
- package/dist/runtime/mutation-planner.js +13 -11
- package/dist/runtime/operation-request.d.ts +4 -11
- package/dist/runtime/operation-runtime.d.ts +14 -7
- package/dist/runtime/operation-runtime.js +37 -18
- package/dist/runtime/outcome-mapper.js +12 -2
- package/dist/runtime/output-validator.d.ts +10 -0
- package/dist/runtime/output-validator.js +11 -0
- package/dist/runtime/owned-execution.registry.d.ts +13 -0
- package/dist/runtime/owned-execution.registry.js +7 -0
- package/dist/runtime/pipeline.d.ts +19 -7
- package/dist/runtime/referential-action.d.ts +7 -0
- package/dist/runtime/request-envelope.validator.d.ts +5 -0
- package/dist/runtime/request-envelope.validator.js +118 -0
- package/dist/runtime/scalar-runtime.d.ts +30 -2
- package/dist/runtime/scalar-runtime.js +162 -79
- package/dist/transactions/transaction-plan-assembler.d.ts +5 -1
- package/dist/transactions/transaction-plan-assembler.js +74 -75
- package/dist/transactions/transaction-plan-validator.d.ts +30 -7
- package/dist/transactions/transaction-plan-validator.js +204 -153
- package/dist/transactions/transaction-reference-collector.d.ts +30 -0
- package/dist/transactions/transaction-reference-collector.js +204 -0
- package/dist/transactions/transaction-reference-resolver.js +10 -5
- package/dist/transactions/transaction-reference-validator.js +24 -17
- package/dist/transactions/transaction-reference.d.ts +23 -51
- package/dist/transactions/transaction-request.d.ts +3 -3
- package/dist/transactions/transaction-response.d.ts +6 -1
- package/dist/transactions/transaction-response.js +24 -1
- package/dist/transactions/transaction-runtime.d.ts +4 -2
- package/dist/transactions/transaction-runtime.js +4 -3
- package/package.json +17 -3
- package/dist/operations/operation-boolean-nodes.d.ts +0 -11
- package/dist/operations/operation-boolean-nodes.js +0 -283
- package/dist/operations/operation-nesting.d.ts +0 -8
- package/dist/operations/operation-nesting.js +0 -307
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { parseOperationIdentity } from "../operations/operation-identity.js";
|
|
2
|
+
import { validationIssue } from "../operations/validation-code.js";
|
|
3
|
+
import { CallerDataError, copyCallerData, } from "./caller-data.copier.js";
|
|
4
|
+
export function misuseIdentityOf(request) {
|
|
5
|
+
const origin = request.origin === "application" || request.origin === "client"
|
|
6
|
+
? request.origin
|
|
7
|
+
: undefined;
|
|
8
|
+
const identity = typeof request.family === "string" && typeof request.variant === "string"
|
|
9
|
+
? parseOperationIdentity(request.family, request.variant)
|
|
10
|
+
: undefined;
|
|
11
|
+
return {
|
|
12
|
+
...(origin === undefined ? {} : { origin }),
|
|
13
|
+
...(typeof request.resource === "string"
|
|
14
|
+
? { resource: request.resource }
|
|
15
|
+
: {}),
|
|
16
|
+
...(identity === undefined
|
|
17
|
+
? {}
|
|
18
|
+
: { family: identity.family, variant: identity.variant }),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function refuseCallerData(error, address, identity, misuse) {
|
|
22
|
+
const path = [...address, ...error.path];
|
|
23
|
+
misuse({ kind: error.kind, path, ...identity });
|
|
24
|
+
return validationIssue("V1001", path, error.message);
|
|
25
|
+
}
|
|
26
|
+
export function copyAtDoor(value, policy) {
|
|
27
|
+
try {
|
|
28
|
+
return { ok: true, value: copyCallerData(value, policy) };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof CallerDataError) {
|
|
32
|
+
return { ok: false, error };
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function copyEnvelopeAtDoor(request, members) {
|
|
38
|
+
const shallow = copyAtDoor(request, {
|
|
39
|
+
keep: (value) => value !== request,
|
|
40
|
+
});
|
|
41
|
+
if (!shallow.ok ||
|
|
42
|
+
typeof shallow.value !== "object" ||
|
|
43
|
+
shallow.value === null ||
|
|
44
|
+
Array.isArray(shallow.value)) {
|
|
45
|
+
return shallow;
|
|
46
|
+
}
|
|
47
|
+
const envelope = shallow.value;
|
|
48
|
+
for (const member of members) {
|
|
49
|
+
if (!Object.hasOwn(envelope, member)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const copy = copyAtDoor(envelope[member]);
|
|
53
|
+
if (!copy.ok) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: new CallerDataError(copy.error.kind, [
|
|
57
|
+
member,
|
|
58
|
+
...copy.error.path,
|
|
59
|
+
]),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
envelope[member] = copy.value;
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, value: envelope };
|
|
65
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
|
|
2
|
-
import {
|
|
3
|
-
import { INTERNAL_FAILURE_MESSAGE } from "./operation-response.js";
|
|
2
|
+
import { ComputedValueError } from "./computed-value.error.js";
|
|
4
3
|
import { normalizeScalarFieldValue } from "./scalar-runtime.js";
|
|
5
4
|
function isRecord(value) {
|
|
6
5
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -274,12 +273,6 @@ export function planReadOperation(core, active, behaviors, request, args) {
|
|
|
274
273
|
materialize: defaults.plan.computed.length > 0,
|
|
275
274
|
};
|
|
276
275
|
}
|
|
277
|
-
function behaviorFailure(issues) {
|
|
278
|
-
throw new FrameworkError("A3000", {
|
|
279
|
-
message: INTERNAL_FAILURE_MESSAGE,
|
|
280
|
-
issues,
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
276
|
async function materializeRelation(active, behaviors, request, relation, value, contextFactory) {
|
|
284
277
|
if (value === null || value === undefined) {
|
|
285
278
|
return value;
|
|
@@ -308,7 +301,7 @@ async function materializeRecord(active, behaviors, request, plan, value, contex
|
|
|
308
301
|
continue;
|
|
309
302
|
}
|
|
310
303
|
const computedValue = await behavior.compute({
|
|
311
|
-
|
|
304
|
+
origin: request.origin,
|
|
312
305
|
resource: plan.resource,
|
|
313
306
|
field: fieldName,
|
|
314
307
|
value: record[fieldName],
|
|
@@ -320,7 +313,7 @@ async function materializeRecord(active, behaviors, request, plan, value, contex
|
|
|
320
313
|
}
|
|
321
314
|
const validation = normalizeScalarFieldValue(active, field, computedValue, "runtime", ["behaviors", plan.resource, fieldName, "read"]);
|
|
322
315
|
if (!validation.ok) {
|
|
323
|
-
|
|
316
|
+
throw new ComputedValueError(plan.resource, fieldName, "read", validation.issues);
|
|
324
317
|
}
|
|
325
318
|
record[fieldName] = validation.value;
|
|
326
319
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ValidationIssue } from "../operations/validation-code.js";
|
|
2
|
+
/**
|
|
3
|
+
* What the diagnostics channel receives when a computed value — a read or write
|
|
4
|
+
* behavior's return — is one its Contract refuses: the developer's broken code,
|
|
5
|
+
* never the caller's. It is thrown, not a deliberate outcome, so the framework's
|
|
6
|
+
* boundary answers the caller the generic `A3000` and reports this error, with the
|
|
7
|
+
* issues, exactly once.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ComputedValueError extends Error {
|
|
10
|
+
readonly issues: readonly ValidationIssue[];
|
|
11
|
+
readonly name = "ComputedValueError";
|
|
12
|
+
constructor(resource: string, field: string, stage: "read" | "create" | "update", issues: readonly ValidationIssue[]);
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export class ComputedValueError extends Error {
|
|
2
|
+
issues;
|
|
3
|
+
name = "ComputedValueError";
|
|
4
|
+
constructor(resource, field, stage, issues) {
|
|
5
|
+
const [first] = issues;
|
|
6
|
+
super(`The ${stage} behavior of ${resource}.${field} returned a value its Contract refuses${first === undefined ? "." : ` (${first.code}: ${first.message})`}`);
|
|
7
|
+
this.issues = issues;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Contract } from "../contracts/contract.js";
|
|
2
2
|
import type { OperationIdentity } from "../operations/operation-identity.js";
|
|
3
3
|
import type { ValidationIssue } from "../operations/validation-code.js";
|
|
4
|
+
import type { ReferentialActionTable } from "./referential-action.js";
|
|
4
5
|
/**
|
|
5
6
|
* Core's create-requirement vocabulary, and the check that reads it.
|
|
6
7
|
*
|
|
@@ -22,6 +23,24 @@ import type { ValidationIssue } from "../operations/validation-code.js";
|
|
|
22
23
|
* which reaches the caller as an internal failure. A missing value is the
|
|
23
24
|
* caller's to fix, so it answers `A2004` with a `V1000` issue at the path where
|
|
24
25
|
* the value belongs.
|
|
26
|
+
*
|
|
27
|
+
* **One value, two spellings.** A relation that holds a reference has scalar
|
|
28
|
+
* fields that hold it too — the Adapter states which, on the relation's
|
|
29
|
+
* referential edge (`ReferentialEdge.references`), for every such relation,
|
|
30
|
+
* required or optional. A write that sets both — `authorId` and
|
|
31
|
+
* `author: { $connect: … }` in one record, at create or update — names the
|
|
32
|
+
* reference twice. No store can honour both, and which one the caller meant is
|
|
33
|
+
* not the framework's to guess, so it is the caller's `A2004` too: a `V1012`
|
|
34
|
+
* issue at the relation, in every record the check walks, before any Adapter
|
|
35
|
+
* work. The same edge is what a required relation may instead be written
|
|
36
|
+
* through, so the pairing has one source.
|
|
37
|
+
*
|
|
38
|
+
* **One store, one spelling per record.** A store may take a record's
|
|
39
|
+
* references only one way at a time — every relation that holds one written as
|
|
40
|
+
* a relation, or every one through its fields — and state it as the Contract's
|
|
41
|
+
* `referenceWrites: "uniform"`. Under it, a record that writes a relation
|
|
42
|
+
* holding a reference beside another relation's reference field is the caller's
|
|
43
|
+
* `A2004` as well: a `V1012` issue at the relation write.
|
|
25
44
|
*/
|
|
26
45
|
/** A relation edge, named by the Resource that holds it and its field there. */
|
|
27
46
|
export type CreateRequirementEdge = {
|
|
@@ -34,9 +53,11 @@ export type CreateRequirementEdge = {
|
|
|
34
53
|
* - `field` — a scalar with no default, generated value or other source: it
|
|
35
54
|
* must be written.
|
|
36
55
|
* - `relation` — a required to-one relation: it must be written (connected or
|
|
37
|
-
* created), or every
|
|
38
|
-
*
|
|
39
|
-
* (`
|
|
56
|
+
* created), or every field that holds its reference must be — the
|
|
57
|
+
* `references` of the relation's referential edge, less any the store fills
|
|
58
|
+
* itself (`DEFAULTED` or `GENERATED_ON_CREATE`). When the row is created
|
|
59
|
+
* **through** the relation's other end (`inverse`), the parent supplies it,
|
|
60
|
+
* and it is not required.
|
|
40
61
|
*/
|
|
41
62
|
export type CreateRequirement = {
|
|
42
63
|
readonly kind: "field";
|
|
@@ -44,7 +65,6 @@ export type CreateRequirement = {
|
|
|
44
65
|
} | {
|
|
45
66
|
readonly kind: "relation";
|
|
46
67
|
readonly relation: string;
|
|
47
|
-
readonly orFields: readonly string[];
|
|
48
68
|
readonly inverse?: CreateRequirementEdge;
|
|
49
69
|
};
|
|
50
70
|
/**
|
|
@@ -53,13 +73,29 @@ export type CreateRequirement = {
|
|
|
53
73
|
*/
|
|
54
74
|
export type CreateRequirementTable = Readonly<Record<string, readonly CreateRequirement[]>>;
|
|
55
75
|
type Path = readonly (string | number)[];
|
|
76
|
+
/**
|
|
77
|
+
* What the Adapter stated that the check reads: its create requirements, and
|
|
78
|
+
* its referential edges for the fields that hold each reference. Read once when
|
|
79
|
+
* the Framework is constructed.
|
|
80
|
+
*/
|
|
81
|
+
export type RecordWriteFacts = {
|
|
82
|
+
readonly requirements: CreateRequirementTable;
|
|
83
|
+
readonly references: ReferentialActionTable;
|
|
84
|
+
};
|
|
85
|
+
/** The facts an Adapter stated, read off it once; an absent table states nothing. */
|
|
86
|
+
export declare function recordWriteFactsOf(adapter: {
|
|
87
|
+
readonly createRequirements?: CreateRequirementTable;
|
|
88
|
+
readonly referentialActions?: ReferentialActionTable;
|
|
89
|
+
}): RecordWriteFacts;
|
|
56
90
|
/**
|
|
57
91
|
* The `V1000` issues for every create position in one operation's arguments
|
|
58
92
|
* that lacks a value the Adapter requires — `create.*`'s `data`, `upsert`'s
|
|
59
|
-
* `create`, and every nested create under any write —
|
|
60
|
-
*
|
|
93
|
+
* `create`, and every nested create under any write — and the `V1012` issues
|
|
94
|
+
* for every write record, create or update, that sets a relation and its
|
|
95
|
+
* reference both, in argument order. Empty when there are none. Total over
|
|
96
|
+
* malformed input.
|
|
61
97
|
*/
|
|
62
|
-
export declare function
|
|
98
|
+
export declare function createRequirementIssues(contract: Contract, facts: RecordWriteFacts, request: OperationIdentity & {
|
|
63
99
|
readonly resource: string;
|
|
64
100
|
}, args: unknown, argumentsPath: Path): readonly ValidationIssue[];
|
|
65
101
|
export {};
|
|
@@ -12,6 +12,23 @@ function requirementsOf(table, resource) {
|
|
|
12
12
|
const listed = Object.hasOwn(table, resource) ? table[resource] : undefined;
|
|
13
13
|
return Array.isArray(listed) ? listed : [];
|
|
14
14
|
}
|
|
15
|
+
function edgesOf(table, resource) {
|
|
16
|
+
const listed = Object.hasOwn(table, resource) ? table[resource] : undefined;
|
|
17
|
+
return Array.isArray(listed) ? listed : [];
|
|
18
|
+
}
|
|
19
|
+
function referencesOf(edge) {
|
|
20
|
+
return Array.isArray(edge.references) ? edge.references : [];
|
|
21
|
+
}
|
|
22
|
+
function referenceFieldsOf(table, resource, relation) {
|
|
23
|
+
const edge = edgesOf(table, resource).find((candidate) => candidate.field === relation);
|
|
24
|
+
return edge === undefined ? [] : referencesOf(edge);
|
|
25
|
+
}
|
|
26
|
+
function storeSupplied(contract, resource, field) {
|
|
27
|
+
const found = lookupField(lookupResource(contract, resource), field);
|
|
28
|
+
return (found?.kind === "scalar" &&
|
|
29
|
+
(found.lifecycle.includes("DEFAULTED") ||
|
|
30
|
+
found.lifecycle.includes("GENERATED_ON_CREATE")));
|
|
31
|
+
}
|
|
15
32
|
function creatableScalar(contract, resource, field) {
|
|
16
33
|
const found = lookupField(lookupResource(contract, resource), field);
|
|
17
34
|
return found?.kind === "scalar" && found.capabilities.create !== undefined;
|
|
@@ -26,7 +43,9 @@ function checkCreateRecord(context, resource, record, path, scope, via) {
|
|
|
26
43
|
return;
|
|
27
44
|
}
|
|
28
45
|
const { contract, issues } = context;
|
|
29
|
-
|
|
46
|
+
const conflicted = referenceConflicts(context, resource, record, path);
|
|
47
|
+
mixedReferenceWrites(context, resource, record, path, conflicted);
|
|
48
|
+
for (const requirement of requirementsOf(context.requirements, resource)) {
|
|
30
49
|
if (requirement.kind === "field") {
|
|
31
50
|
if (!written(record, requirement.field) &&
|
|
32
51
|
creatableScalar(contract, resource, requirement.field)) {
|
|
@@ -38,14 +57,15 @@ function checkCreateRecord(context, resource, record, path, scope, via) {
|
|
|
38
57
|
}
|
|
39
58
|
continue;
|
|
40
59
|
}
|
|
60
|
+
if (conflicted.has(requirement.relation)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
41
63
|
if (via !== undefined &&
|
|
42
64
|
requirement.inverse?.resource === via.resource &&
|
|
43
65
|
requirement.inverse.relation === via.relation) {
|
|
44
66
|
continue;
|
|
45
67
|
}
|
|
46
|
-
const orFields =
|
|
47
|
-
? requirement.orFields
|
|
48
|
-
: [];
|
|
68
|
+
const orFields = referenceFieldsOf(context.references, resource, requirement.relation).filter((field) => !storeSupplied(contract, resource, field));
|
|
49
69
|
if (written(record, requirement.relation) ||
|
|
50
70
|
(orFields.length > 0 && orFields.every((field) => written(record, field)))) {
|
|
51
71
|
continue;
|
|
@@ -69,6 +89,55 @@ function checkCreateRecord(context, resource, record, path, scope, via) {
|
|
|
69
89
|
}
|
|
70
90
|
walkNestedCreates(context, resource, record, path);
|
|
71
91
|
}
|
|
92
|
+
function referenceConflicts(context, resource, record, path) {
|
|
93
|
+
const conflicted = new Set();
|
|
94
|
+
for (const edge of edgesOf(context.references, resource)) {
|
|
95
|
+
if (!written(record, edge.field)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const fields = referencesOf(edge).filter((field) => written(record, field));
|
|
99
|
+
if (fields.length === 0) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
conflicted.add(edge.field);
|
|
103
|
+
context.issues.push({
|
|
104
|
+
code: "V1012",
|
|
105
|
+
path: [...path, edge.field],
|
|
106
|
+
message: `Set ${fields.map((field) => `"${field}"`).join(" and ")} or "${edge.field}", not both.`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return conflicted;
|
|
110
|
+
}
|
|
111
|
+
function mixedReferenceWrites(context, resource, record, path, conflicted) {
|
|
112
|
+
if (context.contract.referenceWrites !== "uniform") {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const edges = edgesOf(context.references, resource);
|
|
116
|
+
const referenceFields = [...new Set(edges.flatMap(referencesOf))];
|
|
117
|
+
for (const edge of edges) {
|
|
118
|
+
if (!written(record, edge.field) || conflicted.has(edge.field)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const fields = referenceFields.filter((field) => written(record, field));
|
|
122
|
+
if (fields.length === 0) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const named = fields.map((field) => `"${field}"`).join(" and ");
|
|
126
|
+
context.issues.push({
|
|
127
|
+
code: "V1012",
|
|
128
|
+
path: [...path, edge.field],
|
|
129
|
+
message: `Use relation fields only, or reference fields only, in one record: ${named} ${fields.length === 1 ? "is a reference field" : "are reference fields"} and "${edge.field}" is a relation.`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function checkUpdateRecord(context, resource, record, path) {
|
|
134
|
+
if (!isRecord(record)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const conflicted = referenceConflicts(context, resource, record, path);
|
|
138
|
+
mixedReferenceWrites(context, resource, record, path, conflicted);
|
|
139
|
+
walkNestedCreates(context, resource, record, path);
|
|
140
|
+
}
|
|
72
141
|
function walkNestedCreates(context, resource, record, path) {
|
|
73
142
|
if (!isRecord(record)) {
|
|
74
143
|
return;
|
|
@@ -100,18 +169,29 @@ function walkNestedCreates(context, resource, record, path) {
|
|
|
100
169
|
checkCreateRecord(context, field.target, siteRecord, sitePath, NESTED_WRITE_SCOPE, via);
|
|
101
170
|
}
|
|
102
171
|
else {
|
|
103
|
-
|
|
172
|
+
checkUpdateRecord(context, field.target, siteRecord, sitePath);
|
|
104
173
|
}
|
|
105
174
|
}
|
|
106
175
|
}
|
|
107
176
|
}
|
|
108
177
|
}
|
|
109
178
|
}
|
|
110
|
-
export function
|
|
179
|
+
export function recordWriteFactsOf(adapter) {
|
|
180
|
+
return {
|
|
181
|
+
requirements: adapter.createRequirements ?? {},
|
|
182
|
+
references: adapter.referentialActions ?? {},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export function createRequirementIssues(contract, facts, request, args, argumentsPath) {
|
|
111
186
|
if (!isRecord(args)) {
|
|
112
187
|
return [];
|
|
113
188
|
}
|
|
114
|
-
const context = {
|
|
189
|
+
const context = {
|
|
190
|
+
contract,
|
|
191
|
+
requirements: facts.requirements,
|
|
192
|
+
references: facts.references,
|
|
193
|
+
issues: [],
|
|
194
|
+
};
|
|
115
195
|
const scope = resolveWriteScope(lookupResource(contract, request.resource), request);
|
|
116
196
|
if (request.family === "create" && written(args, "data")) {
|
|
117
197
|
const data = args.data;
|
|
@@ -126,13 +206,13 @@ export function missingCreateValues(contract, table, request, args, argumentsPat
|
|
|
126
206
|
}
|
|
127
207
|
if (request.family === "upsert") {
|
|
128
208
|
checkCreateRecord(context, request.resource, args.create, [...argumentsPath, "create"], scope, undefined);
|
|
129
|
-
|
|
209
|
+
checkUpdateRecord(context, request.resource, args.update, [
|
|
130
210
|
...argumentsPath,
|
|
131
211
|
"update",
|
|
132
212
|
]);
|
|
133
213
|
}
|
|
134
214
|
if (request.family === "update") {
|
|
135
|
-
|
|
215
|
+
checkUpdateRecord(context, request.resource, args.data, [
|
|
136
216
|
...argumentsPath,
|
|
137
217
|
"data",
|
|
138
218
|
]);
|
|
@@ -5,19 +5,28 @@ import type { FrameworkOperationRequest } from "./operation-request.js";
|
|
|
5
5
|
import type { OperationErrorCode } from "./operation-response.js";
|
|
6
6
|
/**
|
|
7
7
|
* One unplanned failure of an operation: the raw error, the code the caller
|
|
8
|
-
* received, and the operation and request it happened in
|
|
8
|
+
* received, and the operation and request it happened in — or a developer's
|
|
9
|
+
* deliberate internal failure, marked `deliberate`. Server-side only:
|
|
9
10
|
* diagnostics are never sent to the caller.
|
|
10
11
|
*/
|
|
11
12
|
export type OperationDiagnostic = {
|
|
12
13
|
readonly kind: "operation";
|
|
13
14
|
readonly error: unknown;
|
|
14
15
|
readonly code: OperationErrorCode;
|
|
15
|
-
readonly
|
|
16
|
+
readonly origin: RequestOrigin;
|
|
16
17
|
readonly resource: string;
|
|
17
18
|
readonly family: OperationFamily;
|
|
18
19
|
readonly variant: string;
|
|
19
20
|
readonly requestId: string;
|
|
20
21
|
readonly operation?: number;
|
|
22
|
+
/**
|
|
23
|
+
* `true` when `error` is a `FrameworkError` its thrower — a pipeline
|
|
24
|
+
* callback, a behavior, a filter or an Adapter — raised deliberately with an
|
|
25
|
+
* internal (`A3xxx`) code: the developer's own failure, whose message and
|
|
26
|
+
* issues the caller never receives. Absent for a failure the framework did not
|
|
27
|
+
* expect.
|
|
28
|
+
*/
|
|
29
|
+
readonly deliberate?: true;
|
|
21
30
|
};
|
|
22
31
|
/**
|
|
23
32
|
* One unplanned failure of a transaction: the raw error, the code the caller
|
|
@@ -27,9 +36,14 @@ export type TransactionDiagnostic = {
|
|
|
27
36
|
readonly kind: "transaction";
|
|
28
37
|
readonly error: unknown;
|
|
29
38
|
readonly code: OperationErrorCode;
|
|
30
|
-
readonly
|
|
39
|
+
readonly origin: RequestOrigin;
|
|
31
40
|
readonly requestId: string;
|
|
32
41
|
readonly operation?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Never present: only an operation diagnostic is marked `deliberate`. Declared
|
|
44
|
+
* so the mark can be read on any {@link FrameworkDiagnostic}.
|
|
45
|
+
*/
|
|
46
|
+
readonly deliberate?: never;
|
|
33
47
|
};
|
|
34
48
|
/** One diagnostic: of an operation, or of a transaction as a whole. */
|
|
35
49
|
export type FrameworkDiagnostic = OperationDiagnostic | TransactionDiagnostic;
|
|
@@ -43,7 +57,9 @@ export type FrameworkDiagnosticsSink = (diagnostic: FrameworkDiagnostic) => void
|
|
|
43
57
|
* error and the code being emitted. Total by construction — it returns
|
|
44
58
|
* before the sink has finished and swallows everything the sink does.
|
|
45
59
|
*/
|
|
46
|
-
export type DiagnosticsReporter = (error: unknown, code: OperationErrorCode) => void;
|
|
60
|
+
export type DiagnosticsReporter = (error: unknown, code: OperationErrorCode, marks?: DiagnosticMarks) => void;
|
|
61
|
+
/** What a report states about its error beyond the code: today, whether it was deliberate. */
|
|
62
|
+
export type DiagnosticMarks = Pick<OperationDiagnostic, "deliberate">;
|
|
47
63
|
/** The identifying half of a diagnostic, captured once per request. */
|
|
48
64
|
type OperationDiagnosticIdentity = Omit<OperationDiagnostic, "error" | "code">;
|
|
49
65
|
type TransactionDiagnosticIdentity = Omit<TransactionDiagnostic, "error" | "code">;
|
|
@@ -10,9 +10,16 @@ export function createReporter(sink, identity) {
|
|
|
10
10
|
if (sink === undefined) {
|
|
11
11
|
return IGNORE_DIAGNOSTIC;
|
|
12
12
|
}
|
|
13
|
-
return (error, code) => {
|
|
13
|
+
return (error, code, marks) => {
|
|
14
14
|
try {
|
|
15
|
-
const settled = sink(
|
|
15
|
+
const settled = sink(identity.kind === "operation"
|
|
16
|
+
? {
|
|
17
|
+
...identity,
|
|
18
|
+
error,
|
|
19
|
+
code,
|
|
20
|
+
...(marks?.deliberate === true ? { deliberate: true } : {}),
|
|
21
|
+
}
|
|
22
|
+
: { ...identity, error, code });
|
|
16
23
|
if (isThenable(settled)) {
|
|
17
24
|
settled.then(undefined, () => undefined);
|
|
18
25
|
}
|
|
@@ -36,7 +43,7 @@ export function createDiagnosticsReporter(sink, request, operation) {
|
|
|
36
43
|
try {
|
|
37
44
|
identity = {
|
|
38
45
|
kind: "operation",
|
|
39
|
-
|
|
46
|
+
origin: request.origin,
|
|
40
47
|
resource: request.resource,
|
|
41
48
|
family: request.family,
|
|
42
49
|
variant: request.variant,
|
|
@@ -54,7 +61,7 @@ export function createTransactionDiagnosticsChannel(sink, request) {
|
|
|
54
61
|
try {
|
|
55
62
|
identity = {
|
|
56
63
|
kind: "transaction",
|
|
57
|
-
|
|
64
|
+
origin: request.origin,
|
|
58
65
|
requestId: request.requestId,
|
|
59
66
|
};
|
|
60
67
|
}
|
|
@@ -20,6 +20,8 @@ export declare class AdapterBoundary {
|
|
|
20
20
|
export declare const SERIALIZATION_FAILURE_MESSAGE = "Result serialization failed.";
|
|
21
21
|
/** Where a standalone operation's arguments sit in a framework issue path. */
|
|
22
22
|
export declare const OPERATION_ARGUMENTS_PATH: readonly (string | number)[];
|
|
23
|
+
/** Where one plan step sits in a framework issue path: its node in the request. */
|
|
24
|
+
export declare function planStepPath(index: number): readonly (string | number)[];
|
|
23
25
|
/** Where one plan operation's arguments sit in a framework issue path. */
|
|
24
26
|
export declare function planOperationArgumentsPath(index: number): readonly (string | number)[];
|
|
25
27
|
/**
|
|
@@ -31,6 +33,12 @@ export type CallerArgumentsView = {
|
|
|
31
33
|
readonly path: readonly (string | number)[];
|
|
32
34
|
readonly arguments: unknown;
|
|
33
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* The longest issue message a developer's code may send to the caller, in UTF-16
|
|
38
|
+
* code units. A longer one, or one with no visible character, is replaced by the
|
|
39
|
+
* framework's text for its code and reported to the diagnostics channel.
|
|
40
|
+
*/
|
|
41
|
+
export declare const MAX_DEVELOPER_ISSUE_MESSAGE_LENGTH = 1000;
|
|
34
42
|
/**
|
|
35
43
|
* Final framework sanitization, shared by every boundary. A deliberate
|
|
36
44
|
* {@link FrameworkError} carries its own stable outcome; anything else is
|