@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
|
@@ -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 {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
|
|
2
2
|
import { advertisedDirectivesAt } from "../contracts/field.js";
|
|
3
|
-
import { NESTED_WRITE_SCOPE, resolveWriteScope, } from "../operations/write-scope.resolver.js";
|
|
3
|
+
import { LOOPBACK_WRITE_SCOPE, NESTED_WRITE_SCOPE, resolveWriteScope, } from "../operations/write-scope.resolver.js";
|
|
4
4
|
import { isRelationMutationDirective, RELATION_MUTATION_RECORD_SITES, } from "./mutation-planner.js";
|
|
5
5
|
function isRecord(value) {
|
|
6
6
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -63,9 +63,19 @@ function checkCreateRecord(context, resource, record, path, scope, via) {
|
|
|
63
63
|
if (!isRecord(record)) {
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
|
+
if (context.computed !== undefined) {
|
|
67
|
+
computedRequirementGaps(context, resource, record, path, via);
|
|
68
|
+
walkNestedCreates(context, resource, record, path);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
66
71
|
const { contract, issues } = context;
|
|
67
|
-
const
|
|
68
|
-
|
|
72
|
+
const looped = loopbackWrites(context, resource, record, path, via, "create");
|
|
73
|
+
const conflicted = looped.size > 0
|
|
74
|
+
? looped
|
|
75
|
+
: referenceConflicts(context, resource, record, path);
|
|
76
|
+
if (looped.size === 0) {
|
|
77
|
+
mixedReferenceWrites(context, resource, record, path, conflicted);
|
|
78
|
+
}
|
|
69
79
|
const requirements = requirementsOf(contract, resource);
|
|
70
80
|
if (requirements !== undefined) {
|
|
71
81
|
const supplied = suppliedThrough(requirements, via);
|
|
@@ -157,14 +167,93 @@ function mixedReferenceWrites(context, resource, record, path, conflicted) {
|
|
|
157
167
|
});
|
|
158
168
|
}
|
|
159
169
|
}
|
|
160
|
-
function checkUpdateRecord(context, resource, record, path) {
|
|
170
|
+
function checkUpdateRecord(context, resource, record, path, via) {
|
|
161
171
|
if (!isRecord(record)) {
|
|
162
172
|
return;
|
|
163
173
|
}
|
|
164
|
-
|
|
165
|
-
|
|
174
|
+
if (context.computed === undefined &&
|
|
175
|
+
loopbackWrites(context, resource, record, path, via, "update").size === 0) {
|
|
176
|
+
const conflicted = referenceConflicts(context, resource, record, path);
|
|
177
|
+
mixedReferenceWrites(context, resource, record, path, conflicted);
|
|
178
|
+
}
|
|
166
179
|
walkNestedCreates(context, resource, record, path);
|
|
167
180
|
}
|
|
181
|
+
function loopbackWrites(context, resource, record, path, via, stage) {
|
|
182
|
+
const refused = new Set();
|
|
183
|
+
if (via === undefined) {
|
|
184
|
+
return refused;
|
|
185
|
+
}
|
|
186
|
+
const { contract } = context;
|
|
187
|
+
const edge = lookupField(lookupResource(contract, via.resource), via.relation);
|
|
188
|
+
if (edge?.kind !== "relation" || typeof edge.inverse !== "string") {
|
|
189
|
+
return refused;
|
|
190
|
+
}
|
|
191
|
+
const inverse = edge.inverse;
|
|
192
|
+
const other = lookupField(lookupResource(contract, resource), inverse);
|
|
193
|
+
if (other?.kind === "relation" &&
|
|
194
|
+
advertisedDirectivesAt(other.capabilities[stage], LOOPBACK_WRITE_SCOPE) !==
|
|
195
|
+
undefined) {
|
|
196
|
+
return refused;
|
|
197
|
+
}
|
|
198
|
+
const references = edgesOf(context.references, resource).find((candidate) => candidate.field === inverse) ?? undefined;
|
|
199
|
+
for (const field of [
|
|
200
|
+
inverse,
|
|
201
|
+
...(references === undefined ? [] : referencesOf(references)),
|
|
202
|
+
]) {
|
|
203
|
+
if (!written(record, field)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
refused.add(field);
|
|
207
|
+
context.issues.push({
|
|
208
|
+
code: "V1012",
|
|
209
|
+
path: [...path, field],
|
|
210
|
+
message: field === inverse
|
|
211
|
+
? `Relation "${field}" is the relation this ${resource} is written through, from its other end ("${via.resource}.${via.relation}"): the write it sits in supplies it.`
|
|
212
|
+
: `Field "${field}" holds the reference of "${inverse}", the relation this ${resource} is written through ("${via.resource}.${via.relation}"): the write it sits in supplies it.`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return refused;
|
|
216
|
+
}
|
|
217
|
+
function computedRequirementGaps(context, resource, record, path, via) {
|
|
218
|
+
const reader = context.computed;
|
|
219
|
+
const computed = reader?.computed(resource);
|
|
220
|
+
const stated = reader?.stated(resource);
|
|
221
|
+
if (computed === undefined || computed.size === 0 || stated === undefined) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const supplied = suppliedThrough(stated, via);
|
|
225
|
+
const gap = (field) => {
|
|
226
|
+
context.gaps?.push({
|
|
227
|
+
resource,
|
|
228
|
+
field,
|
|
229
|
+
issue: {
|
|
230
|
+
code: "V1000",
|
|
231
|
+
path: [...path, field],
|
|
232
|
+
message: `compute.create on ${resource}.${field} returned undefined for a required field.`,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
for (const field of namesOf(stated.required)) {
|
|
237
|
+
if (computed.has(field) &&
|
|
238
|
+
!supplied.has(field) &&
|
|
239
|
+
!written(record, field)) {
|
|
240
|
+
gap(field);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const groups = Array.isArray(stated.oneOf) ? stated.oneOf : [];
|
|
244
|
+
for (const group of groups) {
|
|
245
|
+
const [relation, ...references] = namesOf(group);
|
|
246
|
+
const left = references.find((field) => computed.has(field) && !written(record, field));
|
|
247
|
+
if (relation === undefined ||
|
|
248
|
+
left === undefined ||
|
|
249
|
+
supplied.has(relation) ||
|
|
250
|
+
references.some((field) => supplied.has(field)) ||
|
|
251
|
+
written(record, relation)) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
gap(left);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
168
257
|
function walkNestedCreates(context, resource, record, path) {
|
|
169
258
|
if (!isRecord(record)) {
|
|
170
259
|
return;
|
|
@@ -196,7 +285,7 @@ function walkNestedCreates(context, resource, record, path) {
|
|
|
196
285
|
checkCreateRecord(context, field.target, siteRecord, sitePath, NESTED_WRITE_SCOPE, via);
|
|
197
286
|
}
|
|
198
287
|
else {
|
|
199
|
-
checkUpdateRecord(context, field.target, siteRecord, sitePath);
|
|
288
|
+
checkUpdateRecord(context, field.target, siteRecord, sitePath, via);
|
|
200
289
|
}
|
|
201
290
|
}
|
|
202
291
|
}
|
|
@@ -204,17 +293,52 @@ function walkNestedCreates(context, resource, record, path) {
|
|
|
204
293
|
}
|
|
205
294
|
}
|
|
206
295
|
export function recordWriteFactsOf(adapter) {
|
|
207
|
-
|
|
296
|
+
const resources = adapter.model?.resources ?? {};
|
|
297
|
+
return {
|
|
298
|
+
references: adapter.referentialActions ?? {},
|
|
299
|
+
stated: (resource) => {
|
|
300
|
+
const found = Object.hasOwn(resources, resource)
|
|
301
|
+
? resources[resource]
|
|
302
|
+
: undefined;
|
|
303
|
+
const stated = isRecord(found) ? found.createRequirements : undefined;
|
|
304
|
+
return isRecord(stated) ? stated : undefined;
|
|
305
|
+
},
|
|
306
|
+
};
|
|
208
307
|
}
|
|
209
308
|
export function createRequirementIssues(contract, facts, request, args, argumentsPath) {
|
|
210
|
-
if (!isRecord(args)) {
|
|
211
|
-
return [];
|
|
212
|
-
}
|
|
213
309
|
const context = {
|
|
214
310
|
contract,
|
|
215
311
|
references: facts.references,
|
|
216
312
|
issues: [],
|
|
217
313
|
};
|
|
314
|
+
walkOperation(context, request, args, argumentsPath);
|
|
315
|
+
return context.issues;
|
|
316
|
+
}
|
|
317
|
+
export function preliminaryRecordIssues(contract, facts, request, args, argumentsPath) {
|
|
318
|
+
const context = {
|
|
319
|
+
contract,
|
|
320
|
+
references: facts.references,
|
|
321
|
+
issues: [],
|
|
322
|
+
};
|
|
323
|
+
walkOperation(context, request, args, argumentsPath);
|
|
324
|
+
return context.issues;
|
|
325
|
+
}
|
|
326
|
+
export function computedRequirementGapsOf(contract, facts, request, args, argumentsPath, computed) {
|
|
327
|
+
const gaps = [];
|
|
328
|
+
walkOperation({
|
|
329
|
+
contract,
|
|
330
|
+
references: facts.references,
|
|
331
|
+
issues: [],
|
|
332
|
+
computed: { stated: facts.stated, computed },
|
|
333
|
+
gaps,
|
|
334
|
+
}, request, args, argumentsPath);
|
|
335
|
+
return gaps;
|
|
336
|
+
}
|
|
337
|
+
function walkOperation(context, request, args, argumentsPath) {
|
|
338
|
+
if (!isRecord(args)) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const contract = context.contract;
|
|
218
342
|
const scope = resolveWriteScope(lookupResource(contract, request.resource), request);
|
|
219
343
|
if (request.family === "create" && written(args, "data")) {
|
|
220
344
|
const data = args.data;
|
|
@@ -229,16 +353,9 @@ export function createRequirementIssues(contract, facts, request, args, argument
|
|
|
229
353
|
}
|
|
230
354
|
if (request.family === "upsert") {
|
|
231
355
|
checkCreateRecord(context, request.resource, args.create, [...argumentsPath, "create"], scope, undefined);
|
|
232
|
-
checkUpdateRecord(context, request.resource, args.update, [
|
|
233
|
-
...argumentsPath,
|
|
234
|
-
"update",
|
|
235
|
-
]);
|
|
356
|
+
checkUpdateRecord(context, request.resource, args.update, [...argumentsPath, "update"], undefined);
|
|
236
357
|
}
|
|
237
358
|
if (request.family === "update") {
|
|
238
|
-
checkUpdateRecord(context, request.resource, args.data, [
|
|
239
|
-
...argumentsPath,
|
|
240
|
-
"data",
|
|
241
|
-
]);
|
|
359
|
+
checkUpdateRecord(context, request.resource, args.data, [...argumentsPath, "data"], undefined);
|
|
242
360
|
}
|
|
243
|
-
return context.issues;
|
|
244
361
|
}
|
|
@@ -47,7 +47,8 @@ export type AvTransactionDiagnostic = {
|
|
|
47
47
|
};
|
|
48
48
|
/**
|
|
49
49
|
* One unplanned failure of the MCP endpoint itself, outside any tool call's
|
|
50
|
-
* operation: the raw error, answered `500` with no detail
|
|
50
|
+
* operation: the raw error, answered `500` with no detail, or `401` when it is
|
|
51
|
+
* the throw of `mcp.authorization.verify`.
|
|
51
52
|
*/
|
|
52
53
|
export type AvMcpDiagnostic = {
|
|
53
54
|
readonly kind: "mcp";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isDatabaseRefusalMessage } from "../operations/database-refusal.js";
|
|
1
2
|
import { VALIDATION_CODE_MESSAGES, VALIDATION_CODES, } from "../operations/validation-code.js";
|
|
2
3
|
import { AvError } from "./framework-error.js";
|
|
3
4
|
import { INTERNAL_FAILURE_MESSAGE, internalFailure, OPERATION_CODES, operationFailure, } from "./operation-response.js";
|
|
@@ -164,6 +165,9 @@ function projectIssue(value, unvouched, view, author) {
|
|
|
164
165
|
unvouched.push(supplied);
|
|
165
166
|
}
|
|
166
167
|
}
|
|
168
|
+
else if (isDatabaseRefusalMessage(code, supplied)) {
|
|
169
|
+
message = supplied;
|
|
170
|
+
}
|
|
167
171
|
if (rawPath === undefined) {
|
|
168
172
|
return { code, message };
|
|
169
173
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Why a code is answered, and what the one who meets it should do. */
|
|
2
|
+
export type ErrorCodeGuidance = {
|
|
3
|
+
/** What typically produced the code, in a sentence or two. */
|
|
4
|
+
readonly cause: string;
|
|
5
|
+
/** What to change so it does not happen again. */
|
|
6
|
+
readonly fix: string;
|
|
7
|
+
};
|
|
8
|
+
/** Guidance for a code whose table holds no message of its own. */
|
|
9
|
+
export type ErrorCodeDescription = ErrorCodeGuidance & {
|
|
10
|
+
/** The code's meaning, in one short sentence. */
|
|
11
|
+
readonly message: string;
|
|
12
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AvAdapterDiscoveryDiagnosticCode } from "../adapters/adapter-discovery-diagnostic.js";
|
|
2
|
+
import type { AvContractCompilerDiagnosticCode } from "../compiler/contract-compiler-diagnostic.js";
|
|
3
|
+
import type { AvConfigDiagnosticCode } from "../config/config-diagnostic.js";
|
|
4
|
+
import type { AvConstructionStage } from "../framework/framework-construction.error.js";
|
|
5
|
+
import { type AvValidationCode } from "../operations/validation-code.js";
|
|
6
|
+
import { type AvOperationCode } from "./operation-response.js";
|
|
7
|
+
/**
|
|
8
|
+
* The documentation page every catalog entry links into. Each code has its own
|
|
9
|
+
* anchor there: the code in lower case (`#a2004`, `#config_required`).
|
|
10
|
+
*/
|
|
11
|
+
export declare const ERROR_CODES_DOCS_URL = "https://aventara-framework.vercel.app/docs/error-codes";
|
|
12
|
+
/**
|
|
13
|
+
* What kind of code an entry is: an outcome code's family (`success` `A1xxx`,
|
|
14
|
+
* `request` `A2xxx`, `internal` `A3xxx`, `auth` `A4xxx`), a `validation` issue
|
|
15
|
+
* code (`V1xxx`), or a startup code, by the construction stage that reports it.
|
|
16
|
+
*/
|
|
17
|
+
export type AvErrorKind = "success" | "request" | "internal" | "auth" | "validation" | AvConstructionStage;
|
|
18
|
+
/** Every code the catalog describes: outcome, validation and startup codes. */
|
|
19
|
+
export type AvErrorCatalogCode = AvOperationCode | AvValidationCode | AvConfigDiagnosticCode | AvAdapterDiscoveryDiagnosticCode | AvContractCompilerDiagnosticCode;
|
|
20
|
+
/** One code: what it means, why it happens, what to do, and where it is documented. */
|
|
21
|
+
export type AvErrorCatalogEntry<C extends AvErrorCatalogCode = AvErrorCatalogCode> = {
|
|
22
|
+
readonly code: C;
|
|
23
|
+
readonly kind: AvErrorKind;
|
|
24
|
+
/** Its meaning in one sentence; for a V-code, the framework's message for it. */
|
|
25
|
+
readonly message: string;
|
|
26
|
+
/** What typically produces it. */
|
|
27
|
+
readonly cause: string;
|
|
28
|
+
/** What to change. */
|
|
29
|
+
readonly fix: string;
|
|
30
|
+
/** Its entry on the error-codes page. */
|
|
31
|
+
readonly docs: string;
|
|
32
|
+
};
|
|
33
|
+
/** The error catalog's type: one entry per code. */
|
|
34
|
+
export type AvErrorCatalog = {
|
|
35
|
+
readonly [C in AvErrorCatalogCode]: AvErrorCatalogEntry<C>;
|
|
36
|
+
};
|
|
37
|
+
/** The documentation URL of one code: the error-codes page at its anchor. */
|
|
38
|
+
export declare function errorDocsUrl(code: AvErrorCatalogCode): string;
|
|
39
|
+
/**
|
|
40
|
+
* Every code Aventara answers or refuses with, by code: its `kind`, `message`,
|
|
41
|
+
* `cause`, `fix` and `docs` URL. An operation's `code`, a validation issue's
|
|
42
|
+
* `code` and a startup diagnostic's `code` are all keys of it:
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* AvErrorCatalog[error.code].fix;
|
|
46
|
+
* AvErrorCatalog.V1000.docs; // https://aventara-framework.vercel.app/docs/error-codes#v1000
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare const AvErrorCatalog: AvErrorCatalog;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ADAPTER_DISCOVERY_DIAGNOSTIC_GUIDANCE } from "../adapters/adapter-discovery-diagnostic.guidance.js";
|
|
2
|
+
import { CONTRACT_COMPILER_DIAGNOSTIC_GUIDANCE } from "../compiler/contract-compiler-diagnostic.guidance.js";
|
|
3
|
+
import { CONFIG_DIAGNOSTIC_GUIDANCE } from "../config/config-diagnostic.guidance.js";
|
|
4
|
+
import { VALIDATION_CODE_GUIDANCE } from "../operations/validation-code.guidance.js";
|
|
5
|
+
import { VALIDATION_CODE_MESSAGES, VALIDATION_CODES, } from "../operations/validation-code.js";
|
|
6
|
+
import { OPERATION_CODE_GUIDANCE } from "./operation-code.guidance.js";
|
|
7
|
+
import { OPERATION_CODES } from "./operation-response.js";
|
|
8
|
+
export const ERROR_CODES_DOCS_URL = "https://aventara-framework.vercel.app/docs/error-codes";
|
|
9
|
+
export function errorDocsUrl(code) {
|
|
10
|
+
return `${ERROR_CODES_DOCS_URL}#${code.toLowerCase()}`;
|
|
11
|
+
}
|
|
12
|
+
const OUTCOME_KIND = {
|
|
13
|
+
"1": "success",
|
|
14
|
+
"2": "request",
|
|
15
|
+
"3": "internal",
|
|
16
|
+
"4": "auth",
|
|
17
|
+
};
|
|
18
|
+
function entry(code, kind, description) {
|
|
19
|
+
return Object.freeze({
|
|
20
|
+
code,
|
|
21
|
+
kind,
|
|
22
|
+
message: description.message,
|
|
23
|
+
cause: description.cause,
|
|
24
|
+
fix: description.fix,
|
|
25
|
+
docs: errorDocsUrl(code),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function entriesOf(table, kind) {
|
|
29
|
+
return Object.keys(table).map((code) => [
|
|
30
|
+
code,
|
|
31
|
+
entry(code, kind, table[code]),
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
export const AvErrorCatalog = Object.freeze(Object.fromEntries([
|
|
35
|
+
...OPERATION_CODES.map((code) => [
|
|
36
|
+
code,
|
|
37
|
+
entry(code, OUTCOME_KIND[code[1]], OPERATION_CODE_GUIDANCE[code]),
|
|
38
|
+
]),
|
|
39
|
+
...VALIDATION_CODES.map((code) => [
|
|
40
|
+
code,
|
|
41
|
+
entry(code, "validation", {
|
|
42
|
+
message: VALIDATION_CODE_MESSAGES[code],
|
|
43
|
+
...VALIDATION_CODE_GUIDANCE[code],
|
|
44
|
+
}),
|
|
45
|
+
]),
|
|
46
|
+
...entriesOf(CONFIG_DIAGNOSTIC_GUIDANCE, "configuration"),
|
|
47
|
+
...entriesOf(ADAPTER_DISCOVERY_DIAGNOSTIC_GUIDANCE, "discovery"),
|
|
48
|
+
...entriesOf(CONTRACT_COMPILER_DIAGNOSTIC_GUIDANCE, "contract-compilation"),
|
|
49
|
+
]));
|
|
@@ -6,6 +6,8 @@ export type Path = readonly (string | number)[];
|
|
|
6
6
|
type UnknownRecord = Readonly<Record<string, unknown>>;
|
|
7
7
|
/** A plain object: what a write record, or a directive's operand, is. */
|
|
8
8
|
export declare function isPlainRecord(value: unknown): value is UnknownRecord;
|
|
9
|
+
/** The containers a family writes, by argument key. */
|
|
10
|
+
export declare const WRITE_KEYS: Readonly<Partial<Record<AvOperationFamily, readonly string[]>>>;
|
|
9
11
|
/** The fields of `resource` with stages, in the Resource's field order. */
|
|
10
12
|
export declare function stagedFieldsOf(contract: AvContract, resource: string, stages: Readonly<Record<string, ResolvedFieldStages>>, kind: "pipes" | "guards"): readonly string[];
|
|
11
13
|
/** The value at `path` in `args`, or `undefined` when nothing is there. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { frozenCopy } from "./readonly-value.js";
|
|
1
2
|
export function isPlainRecord(value) {
|
|
2
3
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
3
4
|
return false;
|
|
@@ -5,7 +6,7 @@ export function isPlainRecord(value) {
|
|
|
5
6
|
const prototype = Object.getPrototypeOf(value);
|
|
6
7
|
return prototype === Object.prototype || prototype === null;
|
|
7
8
|
}
|
|
8
|
-
const WRITE_KEYS = {
|
|
9
|
+
export const WRITE_KEYS = {
|
|
9
10
|
create: ["data"],
|
|
10
11
|
update: ["data"],
|
|
11
12
|
upsert: ["create", "update"],
|
|
@@ -104,9 +105,12 @@ export async function pipeSentFields(stages, fields, records, args, select) {
|
|
|
104
105
|
}
|
|
105
106
|
let value = record[field];
|
|
106
107
|
for (const pipe of stages[field]?.pipes ?? []) {
|
|
107
|
-
const
|
|
108
|
+
const told = frozenCopy(value);
|
|
109
|
+
const next = await pipe(told);
|
|
108
110
|
if (next !== undefined) {
|
|
109
|
-
|
|
111
|
+
if (next !== told) {
|
|
112
|
+
value = next;
|
|
113
|
+
}
|
|
110
114
|
changed = true;
|
|
111
115
|
}
|
|
112
116
|
}
|
|
@@ -133,7 +137,7 @@ export async function guardSentFields(stages, fields, records, args, context) {
|
|
|
133
137
|
}
|
|
134
138
|
for (const guard of stages[field]?.guards ?? []) {
|
|
135
139
|
const told = context();
|
|
136
|
-
if ((await guard(record[field], told)) === false) {
|
|
140
|
+
if ((await guard(frozenCopy(record[field]), told)) === false) {
|
|
137
141
|
return false;
|
|
138
142
|
}
|
|
139
143
|
}
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
import type { AvContract } from "../contracts/contract.js";
|
|
2
2
|
import type { AvValidationIssue } from "../operations/validation-code.js";
|
|
3
|
+
import { AvComputedValueError } from "./computed-value.error.js";
|
|
3
4
|
import { type Path } from "./field-stage.runtime.js";
|
|
4
5
|
import type { OperationExecutionRequest } from "./operation-request.js";
|
|
5
6
|
import type { ReferentialActionTable } from "./referential-action.js";
|
|
7
|
+
/** The write phase of a record: a created row (an upsert's `create` branch too) or an updated one. */
|
|
8
|
+
export type WriteStage = "create" | "update";
|
|
9
|
+
/**
|
|
10
|
+
* The fields a judgment leaves for later: a plan step's (or a forced call's)
|
|
11
|
+
* literal values are judged before BEGIN, except a field a write compute stage
|
|
12
|
+
* of that phase may still change, which is judged after write compute.
|
|
13
|
+
*/
|
|
14
|
+
export type DeferredField = (resource: string, stage: WriteStage, field: string) => boolean;
|
|
6
15
|
/**
|
|
7
16
|
* Every way the sent fields `select` admits break their validators: the
|
|
8
17
|
* operation's own records, then each nested write target's. Paths are the
|
|
9
18
|
* request's: `argumentsPath` and then the value's place in the arguments.
|
|
10
19
|
*/
|
|
11
|
-
export declare function fieldValidatorIssues(contract: AvContract, references: ReferentialActionTable, request: OperationExecutionRequest, args: unknown, argumentsPath: Path, select: (path: Path) => boolean): readonly AvValidationIssue[];
|
|
20
|
+
export declare function fieldValidatorIssues(contract: AvContract, references: ReferentialActionTable, request: OperationExecutionRequest, args: unknown, argumentsPath: Path, select: (path: Path) => boolean, deferred?: DeferredField): readonly AvValidationIssue[];
|
|
21
|
+
/**
|
|
22
|
+
* The server's defect when a value written to a field the active layer hides
|
|
23
|
+
* breaks its validators or its Json shape, or `undefined`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function hiddenFieldRefusal(contracts: {
|
|
26
|
+
readonly core: AvContract;
|
|
27
|
+
readonly active: AvContract;
|
|
28
|
+
}, references: ReferentialActionTable, request: OperationExecutionRequest, args: unknown, argumentsPath: Path): AvComputedValueError | undefined;
|