@lovrozagar/oat 0.7.4 → 0.7.6
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/README.md +175 -146
- package/dist/cli.js +26 -1
- package/dist/config/define-config.d.ts +17 -0
- package/dist/config/define-config.d.ts.map +1 -1
- package/dist/config/define-config.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/report/console.d.ts.map +1 -1
- package/dist/report/console.js +1 -0
- package/dist/report/console.js.map +1 -1
- package/dist/report/render.d.ts +11 -0
- package/dist/report/render.d.ts.map +1 -1
- package/dist/report/render.js +9 -0
- package/dist/report/render.js.map +1 -1
- package/dist/runtime/checks.d.ts.map +1 -1
- package/dist/runtime/checks.js +120 -58
- package/dist/runtime/checks.js.map +1 -1
- package/dist/runtime/client.d.ts +22 -2
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +72 -8
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/effects.d.ts +43 -0
- package/dist/runtime/effects.d.ts.map +1 -0
- package/dist/runtime/effects.js +141 -0
- package/dist/runtime/effects.js.map +1 -0
- package/dist/runtime/exchanges.d.ts +37 -0
- package/dist/runtime/exchanges.d.ts.map +1 -0
- package/dist/runtime/exchanges.js +298 -0
- package/dist/runtime/exchanges.js.map +1 -0
- package/dist/runtime/network.d.ts +74 -0
- package/dist/runtime/network.d.ts.map +1 -0
- package/dist/runtime/network.js +270 -0
- package/dist/runtime/network.js.map +1 -0
- package/dist/runtime/progress.d.ts +2 -0
- package/dist/runtime/progress.d.ts.map +1 -1
- package/dist/runtime/progress.js +2 -2
- package/dist/runtime/progress.js.map +1 -1
- package/dist/runtime/run.d.ts +23 -0
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +107 -8
- package/dist/runtime/run.js.map +1 -1
- package/dist/spec/extensions.d.ts +4 -1
- package/dist/spec/extensions.d.ts.map +1 -1
- package/dist/spec/extensions.js +8 -2
- package/dist/spec/extensions.js.map +1 -1
- package/dist/spec/graph.js +1 -1
- package/dist/spec/graph.js.map +1 -1
- package/dist/spec/load.d.ts +2 -1
- package/dist/spec/load.d.ts.map +1 -1
- package/dist/spec/load.js +8 -7
- package/dist/spec/load.js.map +1 -1
- package/package.json +1 -1
package/dist/runtime/checks.js
CHANGED
|
@@ -18,6 +18,7 @@ import { buildCohort } from "./fixture.js";
|
|
|
18
18
|
import { forEachInvocation } from "./upload-each.js";
|
|
19
19
|
import { driveWait } from "./wait.js";
|
|
20
20
|
import { fillPath } from "./world.js";
|
|
21
|
+
import { bindAfterCreateEffects, bindCreatedScope, bindMissingPathParams, canFillPath, describeEffectHold, effectHolds, identityPathParam, } from "./effects.js";
|
|
21
22
|
/* ------------------------------------------------------------------- helpers */
|
|
22
23
|
/**
|
|
23
24
|
* A documented feature-gate 403 is not a 2xx-check failure and not SECURITY.
|
|
@@ -3265,11 +3266,15 @@ function findConstrained(schema, predicate) {
|
|
|
3265
3266
|
* downstream consequence.
|
|
3266
3267
|
*/
|
|
3267
3268
|
/**
|
|
3268
|
-
* An operation declaring `x-effects` must produce
|
|
3269
|
+
* An operation declaring `x-effects` must produce the stated change.
|
|
3270
|
+
*
|
|
3271
|
+
* `count` is an exact cardinality delta (default 1). `min` is at-least. After a `create` on A,
|
|
3272
|
+
* later items in the same array fill a child list under A with the new id — from the write
|
|
3273
|
+
* response (`table_id` or the entity identity) or from A's list delta.
|
|
3269
3274
|
*
|
|
3270
3275
|
* `x-invalidate` says a read route changes; `x-effects` says *how*. That difference is what
|
|
3271
3276
|
* separates "something differed" — which is satisfied by a stray timestamp and missed by a
|
|
3272
|
-
* cache-stale read — from an
|
|
3277
|
+
* cache-stale read — from an assertion on cardinality and membership.
|
|
3273
3278
|
*/
|
|
3274
3279
|
/**
|
|
3275
3280
|
* A numeric field must compare numerically, not lexically.
|
|
@@ -3477,64 +3482,108 @@ const declaredEffectsOccur = {
|
|
|
3477
3482
|
needs: "an operation declaring x-effects",
|
|
3478
3483
|
async run(ctx) {
|
|
3479
3484
|
for (const op of ctx.effectOps) {
|
|
3480
|
-
|
|
3481
|
-
const
|
|
3482
|
-
const
|
|
3483
|
-
if (
|
|
3484
|
-
|
|
3485
|
-
|
|
3485
|
+
await forEachInvocation(op.operationId, ctx.uploads, async (uploads, slot) => {
|
|
3486
|
+
const fixture = slot?.filename;
|
|
3487
|
+
const effects = op.effects;
|
|
3488
|
+
if (effects.length === 0)
|
|
3489
|
+
return;
|
|
3490
|
+
const scope = { ...ctx.scope };
|
|
3491
|
+
const befores = new Map();
|
|
3492
|
+
for (const effect of effects) {
|
|
3493
|
+
const listOp = listOpFor(ctx, effect.entity);
|
|
3494
|
+
if (listOp === undefined)
|
|
3495
|
+
continue;
|
|
3496
|
+
if (befores.has(effect.entity))
|
|
3497
|
+
continue;
|
|
3498
|
+
befores.set(effect.entity, await observe(ctx, listOp, scope));
|
|
3486
3499
|
}
|
|
3487
|
-
const
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
});
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3500
|
+
const body = op.hasRequestBody ? bodyForOp(ctx, op) : undefined;
|
|
3501
|
+
const invoked = await ctx.client.request(op.method, fillPath(op.path, ctx.scope), {
|
|
3502
|
+
headers: ctx.auth(),
|
|
3503
|
+
operationId: op.operationId,
|
|
3504
|
+
...(fixture === undefined ? {} : { fixture }),
|
|
3505
|
+
...(body === undefined ? {} : await encodeOpBody(ctx, op, body, "baseline", 0, uploads)),
|
|
3506
|
+
});
|
|
3507
|
+
if (standDownForFeatureGate(ctx, op, invoked, this.id))
|
|
3508
|
+
return;
|
|
3509
|
+
if (standDownForRateLimit(ctx, invoked, this.id))
|
|
3510
|
+
return;
|
|
3511
|
+
if (invoked.status >= 400) {
|
|
3512
|
+
ctx.findings.gap(this.id, subject(ctx.entityName, op.operationId, fixture), `${op.operationId} could not be invoked`, `returned ${invoked.status}; its declared effects are unverified`, fixture);
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
const deltas = new Map();
|
|
3516
|
+
Object.assign(scope, bindAfterCreateEffects(ctx.model, effects, invoked.responseBody, deltas));
|
|
3517
|
+
for (const effect of effects) {
|
|
3518
|
+
if (effect.op !== "create")
|
|
3519
|
+
continue;
|
|
3520
|
+
const param = identityPathParam(ctx.model, effect.entity);
|
|
3521
|
+
if (scope[param] !== undefined)
|
|
3522
|
+
continue;
|
|
3523
|
+
const listOp = listOpFor(ctx, effect.entity);
|
|
3524
|
+
if (listOp === undefined)
|
|
3525
|
+
continue;
|
|
3526
|
+
const afterParent = await observe(ctx, listOp, scope);
|
|
3527
|
+
if (afterParent.status !== "ok")
|
|
3528
|
+
continue;
|
|
3529
|
+
const before = befores.get(effect.entity);
|
|
3530
|
+
const prior = before?.status === "ok" ? before.ids : [];
|
|
3531
|
+
const added = afterParent.ids.filter((id) => !prior.includes(id));
|
|
3532
|
+
deltas.set(effect.entity, added);
|
|
3533
|
+
Object.assign(scope, bindCreatedScope(ctx.model, effect.entity, invoked.responseBody, added));
|
|
3534
|
+
}
|
|
3535
|
+
for (const effect of effects) {
|
|
3536
|
+
const listOp = listOpFor(ctx, effect.entity);
|
|
3537
|
+
if (listOp === undefined) {
|
|
3538
|
+
ctx.findings.gap(this.id, subject(ctx.entityName, op.operationId, fixture), `${op.operationId} declares an effect on "${effect.entity}", which has no list route`, "the effect cannot be observed, so it is not verified", fixture);
|
|
3539
|
+
continue;
|
|
3540
|
+
}
|
|
3541
|
+
if (!canFillPath(listOp.path, scope)) {
|
|
3542
|
+
const missing = listOp.pathParams.filter((name) => scope[name] === undefined);
|
|
3543
|
+
ctx.findings.gap(this.id, subject(ctx.entityName, op.operationId, fixture), `${op.operationId} declares an effect on "${effect.entity}", but the list path cannot be filled`, `missing ${missing.map((name) => `{${name}}`).join(", ")} after the write; the effect is unverified`, fixture);
|
|
3544
|
+
continue;
|
|
3545
|
+
}
|
|
3546
|
+
const after = await observe(ctx, listOp, scope);
|
|
3547
|
+
if (after.status === "error") {
|
|
3548
|
+
ctx.findings.gap(this.id, subject(ctx.entityName, op.operationId, fixture), `${op.operationId} could not observe "${effect.entity}" after the write`, `list returned ${after.exchange.status}; the declared effect is unverified`, fixture);
|
|
3549
|
+
continue;
|
|
3507
3550
|
}
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
const
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3551
|
+
if (after.status === "unfillable") {
|
|
3552
|
+
ctx.findings.gap(this.id, subject(ctx.entityName, op.operationId, fixture), `${op.operationId} declares an effect on "${effect.entity}", but the list path cannot be filled`, "the child list still lacks a parent id after the write; the effect is unverified", fixture);
|
|
3553
|
+
continue;
|
|
3554
|
+
}
|
|
3555
|
+
const before = befores.get(effect.entity);
|
|
3556
|
+
if (before?.status === "error") {
|
|
3557
|
+
ctx.findings.gap(this.id, subject(ctx.entityName, op.operationId, fixture), `${op.operationId} could not observe "${effect.entity}" before the write`, `list returned ${before.exchange.status}; no baseline, so the effect is unverified`, fixture);
|
|
3558
|
+
continue;
|
|
3559
|
+
}
|
|
3560
|
+
const prior = before?.status === "ok" ? before.ids : [];
|
|
3561
|
+
const delta = after.ids.length - prior.length;
|
|
3562
|
+
const added = after.ids.filter((id) => !prior.includes(id));
|
|
3563
|
+
const removed = prior.filter((id) => !after.ids.includes(id));
|
|
3564
|
+
if (effectHolds(effect, delta, added.length, removed.length)) {
|
|
3565
|
+
if (effect.op === "create") {
|
|
3566
|
+
Object.assign(scope, bindCreatedScope(ctx.model, effect.entity, invoked.responseBody, added));
|
|
3567
|
+
}
|
|
3568
|
+
continue;
|
|
3569
|
+
}
|
|
3570
|
+
const hold = describeEffectHold(effect);
|
|
3516
3571
|
if (effect.op === "create" || effect.op === "append") {
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
ctx.findings.backend(this.id, subject(ctx.entityName, op.operationId, fixture), `${op.operationId} did not produce the "${effect.entity}" records it declares`, `x-effects declares ${effect.op} × ${expected} on "${effect.entity}", but the ` +
|
|
3520
|
-
`collection went from ${before.ids.length} to ${after.ids.length} ` +
|
|
3572
|
+
ctx.findings.backend(this.id, subject(ctx.entityName, op.operationId, fixture), `${op.operationId} did not produce the "${effect.entity}" records it declares`, `x-effects declares ${hold} on "${effect.entity}", but the ` +
|
|
3573
|
+
`collection went from ${prior.length} to ${after.ids.length} ` +
|
|
3521
3574
|
`(${added.length} added, ${removed.length} removed). A declared effect that does ` +
|
|
3522
3575
|
"not occur means callers cannot rely on the operation having done anything.", [invoked, after.exchange], fixture);
|
|
3523
|
-
|
|
3576
|
+
continue;
|
|
3524
3577
|
}
|
|
3525
3578
|
if (effect.op === "delete") {
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
`went from ${before.ids.length} to ${after.ids.length}`, [invoked, after.exchange], fixture);
|
|
3530
|
-
return;
|
|
3531
|
-
}
|
|
3532
|
-
if (delta !== 0) {
|
|
3533
|
-
ctx.findings.backend(this.id, subject(ctx.entityName, op.operationId, fixture), `${op.operationId} changed the size of "${effect.entity}" while declaring ${effect.op}`, `x-effects declares ${effect.op}, which must not add or remove records, but the ` +
|
|
3534
|
-
`collection went from ${before.ids.length} to ${after.ids.length}`, [invoked, after.exchange], fixture);
|
|
3579
|
+
ctx.findings.backend(this.id, subject(ctx.entityName, op.operationId, fixture), `${op.operationId} did not remove the "${effect.entity}" records it declares`, `x-effects declares ${hold} on "${effect.entity}", but the collection ` +
|
|
3580
|
+
`went from ${prior.length} to ${after.ids.length}`, [invoked, after.exchange], fixture);
|
|
3581
|
+
continue;
|
|
3535
3582
|
}
|
|
3536
|
-
|
|
3537
|
-
|
|
3583
|
+
ctx.findings.backend(this.id, subject(ctx.entityName, op.operationId, fixture), `${op.operationId} changed the size of "${effect.entity}" while declaring ${effect.op}`, `x-effects declares ${effect.op}, which must not add or remove records, but the ` +
|
|
3584
|
+
`collection went from ${prior.length} to ${after.ids.length}`, [invoked, after.exchange], fixture);
|
|
3585
|
+
}
|
|
3586
|
+
});
|
|
3538
3587
|
}
|
|
3539
3588
|
},
|
|
3540
3589
|
};
|
|
@@ -3564,6 +3613,7 @@ const sideEffectArrives = {
|
|
|
3564
3613
|
const invoked = await ctx.client.request(op.method, fillPath(op.path, ctx.scope), {
|
|
3565
3614
|
headers: ctx.auth(),
|
|
3566
3615
|
operationId: op.operationId,
|
|
3616
|
+
...(fixture === undefined ? {} : { fixture }),
|
|
3567
3617
|
...(body === undefined ? {} : await encodeOpBody(ctx, op, body, "baseline", 0, uploads)),
|
|
3568
3618
|
});
|
|
3569
3619
|
if (standDownForFeatureGate(ctx, op, invoked, this.id))
|
|
@@ -3574,13 +3624,14 @@ const sideEffectArrives = {
|
|
|
3574
3624
|
ctx.findings.gap(this.id, subject(ctx.entityName, op.operationId, fixture), `${op.operationId} could not be invoked`, `returned ${invoked.status}; its declared x-wait is unverified`, fixture);
|
|
3575
3625
|
return;
|
|
3576
3626
|
}
|
|
3627
|
+
const scope = bindWaitScope(ctx, op, pollOp, invoked.responseBody);
|
|
3577
3628
|
const outcome = await driveWait({
|
|
3578
3629
|
awaitSideEffect: ctx.hooks.awaitSideEffect,
|
|
3579
3630
|
client: ctx.client,
|
|
3580
3631
|
headers: ctx.auth,
|
|
3581
3632
|
pollOp,
|
|
3582
3633
|
record: invoked.responseBody,
|
|
3583
|
-
scope
|
|
3634
|
+
scope,
|
|
3584
3635
|
spec,
|
|
3585
3636
|
writeOpId: op.operationId,
|
|
3586
3637
|
...(ctx.refreshIfStale === undefined ? {} : { refreshIfStale: ctx.refreshIfStale }),
|
|
@@ -3596,23 +3647,34 @@ const sideEffectArrives = {
|
|
|
3596
3647
|
}
|
|
3597
3648
|
},
|
|
3598
3649
|
};
|
|
3599
|
-
|
|
3650
|
+
function listOpFor(ctx, entityName) {
|
|
3651
|
+
const listId = ctx.model.entities.get(entityName)?.list;
|
|
3652
|
+
return listId === undefined ? undefined : ctx.model.byOperationId.get(listId);
|
|
3653
|
+
}
|
|
3654
|
+
function bindWaitScope(ctx, writeOp, pollOp, writeBody) {
|
|
3655
|
+
const scope = { ...ctx.scope };
|
|
3656
|
+
Object.assign(scope, bindAfterCreateEffects(ctx.model, writeOp.effects, writeBody));
|
|
3657
|
+
Object.assign(scope, bindMissingPathParams(pollOp.pathParams, scope, writeBody));
|
|
3658
|
+
return scope;
|
|
3659
|
+
}
|
|
3660
|
+
async function observe(ctx, listOp, scope = ctx.scope) {
|
|
3600
3661
|
let path;
|
|
3601
3662
|
try {
|
|
3602
|
-
path = fillPath(listOp.path,
|
|
3663
|
+
path = fillPath(listOp.path, scope);
|
|
3603
3664
|
}
|
|
3604
3665
|
catch {
|
|
3605
|
-
return
|
|
3666
|
+
return { status: "unfillable" };
|
|
3606
3667
|
}
|
|
3607
3668
|
const exchange = await ctx.client.get(path, {
|
|
3608
3669
|
headers: ctx.auth(),
|
|
3670
|
+
operationId: listOp.operationId,
|
|
3609
3671
|
query: { limit: listOp.query?.maxLimit ?? 100 },
|
|
3610
3672
|
});
|
|
3611
3673
|
if (exchange.status >= 400)
|
|
3612
|
-
return
|
|
3674
|
+
return { exchange, status: "error" };
|
|
3613
3675
|
const identity = ctx.model.entities.get(listOp.entity ?? "")?.identity ?? "id";
|
|
3614
3676
|
const items = extractItems(exchange.responseBody, listOp.collection?.key ?? null);
|
|
3615
|
-
return { exchange, ids: items.map((item) => String(item[identity])) };
|
|
3677
|
+
return { exchange, ids: items.map((item) => String(item[identity])), status: "ok" };
|
|
3616
3678
|
}
|
|
3617
3679
|
const asyncReachesTerminalState = {
|
|
3618
3680
|
applicable: (ctx) => ctx.asyncOps.length > 0,
|