@volter/world-core 3.0.155 → 3.0.157
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/attach.cjs +2 -0
- package/capability-decision.cjs +6 -4
- package/dist/attach.cjs +2 -0
- package/dist/capability-decision.cjs +6 -4
- package/dist/generated/pack-facts.json +204 -4
- package/dist/inject.cjs +150 -86
- package/dist/network-policy.cjs +11 -1
- package/dist/network-policy.d.cts +1 -0
- package/dist/pack-facts.d.cts +3 -0
- package/dist/required-credential-transport.cjs +29 -0
- package/dist/required-credential-transport.d.cts +3 -0
- package/dist/src/actions.d.ts +15 -0
- package/dist/src/actions.js +82 -21
- package/dist/src/ancestry.d.ts +5 -4
- package/dist/src/ancestry.js +53 -28
- package/dist/src/blob-store.d.ts +16 -0
- package/dist/src/blob-store.js +76 -1
- package/dist/src/changeset.js +1 -1
- package/dist/src/clickhouse/compression.d.ts +28 -0
- package/dist/src/clickhouse/compression.js +281 -0
- package/dist/src/clickhouse/index.d.ts +1 -0
- package/dist/src/clickhouse/index.js +1 -0
- package/dist/src/derived-core.d.ts +84 -7
- package/dist/src/derived-core.js +336 -61
- package/dist/src/derived-real.js +97 -15
- package/dist/src/derived.d.ts +5 -0
- package/dist/src/derived.js +3 -0
- package/dist/src/executor.d.ts +7 -0
- package/dist/src/executor.js +15 -1
- package/dist/src/fork.js +1 -1
- package/dist/src/git/lfs.d.ts +13 -3
- package/dist/src/git/lfs.js +41 -9
- package/dist/src/grpc-wire.js +2 -1
- package/dist/src/h2.js +63 -10
- package/dist/src/head.d.ts +5 -0
- package/dist/src/head.js +15 -0
- package/dist/src/history.d.ts +1 -1
- package/dist/src/history.js +7 -7
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.js +6 -1
- package/dist/src/live/ingest.js +9 -13
- package/dist/src/live/rtmp.js +7 -5
- package/dist/src/live/segmenter.js +7 -1
- package/dist/src/log.d.ts +23 -3
- package/dist/src/log.js +160 -37
- package/dist/src/machines.js +5 -1
- package/dist/src/managed-go-programs.d.ts +55 -0
- package/dist/src/managed-go-programs.js +133 -0
- package/dist/src/managed-original-inputs.d.ts +101 -0
- package/dist/src/managed-original-inputs.js +125 -0
- package/dist/src/mongodb/client.d.ts +8 -0
- package/dist/src/mongodb/client.js +27 -0
- package/dist/src/mongodb/index.d.ts +2 -0
- package/dist/src/mongodb/index.js +5 -0
- package/dist/src/mongodb/stream.d.ts +97 -0
- package/dist/src/mongodb/stream.js +206 -0
- package/dist/src/observe.js +1 -1
- package/dist/src/pack-fetch.d.ts +6 -1
- package/dist/src/pack-fetch.js +69 -14
- package/dist/src/pack-paths.d.ts +2 -1
- package/dist/src/pack-paths.js +6 -1
- package/dist/src/packRegistry.d.ts +10 -14
- package/dist/src/packRegistry.js +2 -13
- package/dist/src/protocol.d.ts +13 -0
- package/dist/src/protocol.js +13 -0
- package/dist/src/redis/engine.d.ts +10 -4
- package/dist/src/redis/engine.js +103 -32
- package/dist/src/redis/member-storage.js +20 -8
- package/dist/src/redis/service-times.d.ts +25 -0
- package/dist/src/redis/service-times.js +87 -0
- package/dist/src/redis/stream.d.ts +2 -1
- package/dist/src/redis/stream.js +109 -20
- package/dist/src/resource-blob.d.ts +4 -0
- package/dist/src/resource-blob.js +8 -1
- package/dist/src/runtime-state.d.ts +11 -0
- package/dist/src/runtime-state.js +13 -0
- package/dist/src/runtime.d.ts +7 -5
- package/dist/src/runtime.js +7 -4
- package/dist/src/serve-http.d.ts +2 -0
- package/dist/src/serve-http.js +73 -6
- package/dist/src/serve.d.ts +10 -0
- package/dist/src/serve.js +43 -3
- package/dist/src/sockets.d.ts +4 -1
- package/dist/src/sockets.js +59 -6
- package/dist/src/storage.d.ts +2 -1
- package/dist/src/storage.js +6 -3
- package/dist/src/vendor-call.js +58 -0
- package/dist/src/world-boot.d.ts +1 -0
- package/dist/src/world-boot.js +6 -0
- package/dist/src/world-clock.js +6 -2
- package/dist/src/world-store.d.ts +122 -7
- package/dist/src/world-store.js +959 -103
- package/dist/src/xml.d.ts +30 -0
- package/dist/src/xml.js +74 -0
- package/generated/pack-facts.json +204 -4
- package/inject.cjs +150 -86
- package/network-policy.cjs +11 -1
- package/network-policy.d.cts +1 -0
- package/pack-facts.d.cts +3 -0
- package/package.json +27 -1
- package/required-credential-transport.cjs +29 -0
- package/required-credential-transport.d.cts +3 -0
- package/src/actions.ts +78 -21
- package/src/ancestry.ts +46 -24
- package/src/blob-store.ts +59 -1
- package/src/changeset.ts +1 -1
- package/src/clickhouse/compression.ts +172 -0
- package/src/clickhouse/index.ts +2 -0
- package/src/derived-core.ts +356 -61
- package/src/derived-real.ts +80 -14
- package/src/derived.ts +9 -1
- package/src/executor.ts +17 -3
- package/src/fork.ts +1 -1
- package/src/git/lfs.ts +40 -9
- package/src/grpc-wire.ts +2 -1
- package/src/h2.ts +46 -5
- package/src/head.ts +15 -0
- package/src/history.ts +7 -7
- package/src/index.ts +7 -1
- package/src/live/ingest.ts +7 -3
- package/src/live/rtmp.ts +7 -5
- package/src/live/segmenter.ts +7 -1
- package/src/log.ts +147 -32
- package/src/machines.ts +5 -1
- package/src/managed-go-programs.ts +146 -0
- package/src/managed-original-inputs.ts +163 -0
- package/src/mongodb/client.ts +27 -0
- package/src/mongodb/index.ts +5 -0
- package/src/mongodb/stream.ts +223 -0
- package/src/observe.ts +1 -1
- package/src/pack-fetch.ts +66 -16
- package/src/pack-paths.ts +5 -1
- package/src/packRegistry.ts +3 -15
- package/src/protocol.ts +13 -0
- package/src/redis/engine.ts +100 -57
- package/src/redis/member-storage.ts +17 -9
- package/src/redis/service-times.ts +81 -0
- package/src/redis/stream.ts +66 -14
- package/src/resource-blob.ts +9 -1
- package/src/runtime-state.ts +14 -0
- package/src/runtime.ts +10 -3
- package/src/serve-http.ts +65 -6
- package/src/serve.ts +45 -3
- package/src/sockets.ts +39 -8
- package/src/storage.ts +5 -3
- package/src/vendor-call.ts +54 -0
- package/src/world-boot.ts +6 -0
- package/src/world-clock.ts +6 -2
- package/src/world-store.ts +768 -91
- package/src/xml.ts +71 -0
package/dist/src/derived-core.js
CHANGED
|
@@ -5,8 +5,9 @@ import { parseExactJson, compareExactNumbers, exactInteger, isExactIntegerValue
|
|
|
5
5
|
// how lists page, how errors read, which fields are state and how they move). From those two this
|
|
6
6
|
// serves CRUD generically and applies declared transitions, over the kernel's tree and write path.
|
|
7
7
|
// Nothing here knows a vendor: every vendor difference is a manifest value.
|
|
8
|
-
import { applyTwinWrite, applyTwinWriteAtomic, twinResources } from "./serve.js";
|
|
9
|
-
import { copyResource, ownFields, parentStamp, subjectHistory, treeChangesSince, treeStamp } from "./log.js";
|
|
8
|
+
import { applyTwinWrite, applyTwinWriteAtomic, applyTwinWrites, twinResources } from "./serve.js";
|
|
9
|
+
import { copyResource, foldLocalWrite, heldSubject, ownFields, parentStamp, subjectHistory, treeChangesSince, treeResources, treeStamp } from "./log.js";
|
|
10
|
+
import { performsAtHeadNow } from "./head.js";
|
|
10
11
|
import { getActiveWorldStore } from "./world-store.js";
|
|
11
12
|
import { getActiveBlobStore } from "./blob-store.js";
|
|
12
13
|
import { worldPaths } from "./storage.js";
|
|
@@ -21,8 +22,9 @@ import { MACHINE_POOL, poolFor, guardedMachinePool } from "./machines.js";
|
|
|
21
22
|
import { askApplication, deliverEvents } from "./events.js";
|
|
22
23
|
import { handlerCrypto, hmac, ksuidFrom, lettersFrom, sha256, uuidFrom } from "./signing.js";
|
|
23
24
|
import { hashFieldValue } from "./hash.js";
|
|
24
|
-
import { readResourceBlob, removeResourceBlob, writeResourceBlob } from "./resource-blob.js";
|
|
25
|
+
import { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobSize, writeResourceBlob, writeResourceBlobStream } from "./resource-blob.js";
|
|
25
26
|
import { managedDatabase, unboundDatabase } from "./managed-database.js";
|
|
27
|
+
import { redisServiceTimes } from "./redis/service-times.js";
|
|
26
28
|
import { execRedisRun } from "./redis/engine.js";
|
|
27
29
|
import { sendMail } from "./smtp.js";
|
|
28
30
|
import { ownField, parseBracketForm } from "./form.js";
|
|
@@ -71,12 +73,13 @@ export function strictFields(m, operation, body) {
|
|
|
71
73
|
if (!field)
|
|
72
74
|
return refuse(strict.unknown, { name });
|
|
73
75
|
const { type } = field;
|
|
74
|
-
const fits = value === null
|
|
76
|
+
const fits = (value === null && !(type === 'array' && strict.arrays?.[name]))
|
|
75
77
|
|| (type === 'boolean' && typeof value === 'boolean')
|
|
76
78
|
|| (type === 'integer' && isExactIntegerValue(value))
|
|
77
79
|
|| (type === 'number' && (typeof value === 'number' || isExactIntegerValue(value)))
|
|
78
80
|
|| (type === 'string' && typeof value === 'string')
|
|
79
|
-
||
|
|
81
|
+
|| (type === 'array' && Array.isArray(value) && (!strict.arrays?.[name] || value.every(item => typeof item === strict.arrays[name])))
|
|
82
|
+
|| !['boolean', 'integer', 'number', 'string', 'array'].includes(type);
|
|
80
83
|
if (!fits)
|
|
81
84
|
return refuse(strict.type, { name, type: type === 'integer' ? 'an integer' : `a ${type}` });
|
|
82
85
|
if ((typeof value === 'number' || isExactIntegerValue(value)) && ((field.minimum !== undefined && compareExactNumbers(value, field.minimum) < 0) || (field.maximum !== undefined && compareExactNumbers(value, field.maximum) > 0))) {
|
|
@@ -263,14 +266,16 @@ export function resourcesOfType(service, type, root) {
|
|
|
263
266
|
}
|
|
264
267
|
held.stamp = delta.stamp;
|
|
265
268
|
}
|
|
266
|
-
else
|
|
267
|
-
// the stamp is read before the fold, so a write between them can only leave a newer fold under an older stamp
|
|
269
|
+
else {
|
|
270
|
+
// the stamp is read once, before the fold, so a write between them can only leave a newer fold under an older stamp
|
|
268
271
|
const stamp = treeStamp(service, root);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
(
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
if (!held || held.stamp !== stamp) {
|
|
273
|
+
const byType = new Map();
|
|
274
|
+
for (const r of twinResources(service, root))
|
|
275
|
+
(byType.get(r.type) ?? byType.set(r.type, new Map()).get(r.type)).set(`${r.type}:${r.id}`, r);
|
|
276
|
+
held = { stamp, byType };
|
|
277
|
+
indexes.set(key, held);
|
|
278
|
+
}
|
|
274
279
|
}
|
|
275
280
|
return [...(held.byType.get(type)?.values() ?? [])].map(copyResource);
|
|
276
281
|
}
|
|
@@ -299,13 +304,20 @@ function parentField(row, path) {
|
|
|
299
304
|
value = value && typeof value === 'object' ? value[key] : undefined;
|
|
300
305
|
return value;
|
|
301
306
|
}
|
|
302
|
-
function
|
|
303
|
-
const parent =
|
|
307
|
+
function parentOfCall(decl, call) {
|
|
308
|
+
const parent = decl?.parent;
|
|
309
|
+
const names = parent?.params ?? (parent?.param === undefined ? [] : [parent.param]);
|
|
310
|
+
return parent?.whenNamed && names.length && !names.some((name) => Object.hasOwn(call.params, name))
|
|
311
|
+
? undefined : parent;
|
|
312
|
+
}
|
|
313
|
+
function missingParent(m, resource, call, root, tenant) {
|
|
314
|
+
const parent = parentOfCall(m.resources[resource], call);
|
|
304
315
|
if (!parent)
|
|
305
316
|
return undefined;
|
|
306
317
|
const id = parentValue(parent, call.params);
|
|
307
318
|
const where = Object.entries(parent.where ?? {}).map(([field, template]) => [field, String(fill(template, call.params))]);
|
|
308
|
-
const found = (r) => (where.length ? where.every(([field, v]) => String(parentField(r, field)) === v) : r.id === id)
|
|
319
|
+
const found = (r) => (where.length ? where.every(([field, v]) => String(parentField(r, field)) === v) : r.id === id)
|
|
320
|
+
&& (!m.tenant || m.resources[parent.resource]?.shared || r[m.tenant.field] === tenant);
|
|
309
321
|
return id !== undefined && stored(m, parent.resource, root, { withDeleted: parent.allowDeleted === true }).some(found) ? undefined : notFound(m, parent.resource, String(id), parent.param ?? parent.params?.at(-1));
|
|
310
322
|
}
|
|
311
323
|
/** The vendor's view of a stored subject: its own fields, bookkeeping (`_`) left out. */
|
|
@@ -688,7 +700,11 @@ function recordedInput(m, call, merged) {
|
|
|
688
700
|
const id = call.operation.id;
|
|
689
701
|
const sent = sentOf.get(call);
|
|
690
702
|
const activity = call.request.headers.get('x-volter-world-activity') === 'provisioning' ? { activity: 'provisioning' } : {};
|
|
691
|
-
const
|
|
703
|
+
const door = m.doors?.find(d => d.id === id);
|
|
704
|
+
const body = sent?.body ?? merged;
|
|
705
|
+
const safeBody = door?.redactBody?.length && body && typeof body === 'object' && !Array.isArray(body)
|
|
706
|
+
? Object.fromEntries(Object.entries(body).filter(([key]) => !door.redactBody.includes(key))) : body;
|
|
707
|
+
const asSent = sent ? { ...activity, path: sent.path, query: sent.query, body: safeBody, headers: sent.headers, ...(sent.lane ? { lane: sent.lane } : {}), ...(sent.requestId ? { requestId: sent.requestId } : {}), ...(sent.type ? { contentType: sent.type } : {}) } : { ...activity, path: call.params, body: safeBody };
|
|
692
708
|
if (id === 'graphql') {
|
|
693
709
|
const b = (sent?.body ?? {});
|
|
694
710
|
return { ...activity, graphql: { query: b.query, variables: b.variables ?? {}, ...(b.operationName ? { operationName: b.operationName } : {}) }, ...(sent?.lane ? { lane: sent.lane } : {}), headers: sent?.headers ?? {} };
|
|
@@ -707,28 +723,27 @@ const scopeOfCall = new WeakMap();
|
|
|
707
723
|
const requestOfCall = new WeakMap();
|
|
708
724
|
async function writeDetailed(m, call, resource, id, fields, operation, params, root, occurredAt, checks) {
|
|
709
725
|
const before = stateBeforeWrite(m, resource, id, root);
|
|
726
|
+
const prior = counted(m, resource) ? stored(m, resource, root).find((r) => r.id === id) : undefined;
|
|
710
727
|
const withFiles = async (input) => { const files = input.operationId ? await keptFiles(m, call, root) : undefined; return files ? { ...input, files } : input; };
|
|
711
728
|
const { resource: row, result } = await applyTwinWrite(m.service,
|
|
712
|
-
// who made it: the login the pack's identity reads off the request, when it declares one, so a subject's
|
|
713
|
-
// history can say who did each thing
|
|
714
729
|
// a vendor's own move (ctx.asVendor, the clock) is the World's, as the clock's writes are: no API call a head performs
|
|
715
730
|
inVendorMove()
|
|
716
731
|
? { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: { clock: true }, occurredAt, actor: { kind: 'system' } }
|
|
717
|
-
: { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: await withFiles(recordedInput(m, call, params)), occurredAt, actor: { kind: 'agent'
|
|
732
|
+
: { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: await withFiles(recordedInput(m, call, params)), occurredAt, actor: { kind: 'agent' } }, root);
|
|
718
733
|
observeStateWrite(m, result, before, root, checks);
|
|
719
|
-
return finishWrite(m, call, resource, row, result, operation, params, root, occurredAt);
|
|
734
|
+
return finishWrite(m, call, resource, row, result, operation, params, root, occurredAt, prior && { ...prior });
|
|
720
735
|
}
|
|
721
|
-
async function atomicWrite(m, call, resource, decide, operation, params, root, occurredAt, unchangedId, checks) {
|
|
722
|
-
const input = recordedInput(m, call, params);
|
|
723
|
-
const files = input
|
|
736
|
+
async function atomicWrite(m, call, resource, decide, operation, params, root, occurredAt, unchangedId, checks, omitInput = false) {
|
|
737
|
+
const input = omitInput ? undefined : recordedInput(m, call, params);
|
|
738
|
+
const files = input?.operationId ? await keptFiles(m, call, root) : undefined;
|
|
724
739
|
let before;
|
|
725
740
|
const committed = await applyTwinWriteAtomic(m.service, (snapshot) => {
|
|
726
741
|
const selected = decide(snapshot);
|
|
727
|
-
if (selected && transitionObserver)
|
|
742
|
+
if (selected && (transitionObserver || counted(m, resource)))
|
|
728
743
|
before = snapshot.find((r) => r.type === storedType(m, resource) && r.id === selected.id);
|
|
729
744
|
return selected ? { kind: 'write', value: selected.id, write: {
|
|
730
745
|
operation, subjectType: storedType(m, resource), subjectId: selected.id, fields: selected.fields,
|
|
731
|
-
input: files ? { ...input, files } : input, occurredAt, actor: { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) },
|
|
746
|
+
...(input ? { input: files ? { ...input, files } : input } : {}), occurredAt, actor: { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) },
|
|
732
747
|
} } : { kind: 'skip', value: undefined };
|
|
733
748
|
}, root);
|
|
734
749
|
if (!committed.result || !committed.value) {
|
|
@@ -737,10 +752,10 @@ async function atomicWrite(m, call, resource, decide, operation, params, root, o
|
|
|
737
752
|
}
|
|
738
753
|
observeStateWrite(m, committed.result, before, root, checks);
|
|
739
754
|
const row = resourcesOfType(m.service, storedType(m, resource), root).find((r) => r.id === resolveSubjectId(m.service, storedType(m, resource), committed.value, root));
|
|
740
|
-
return (await finishWrite(m, call, resource, row, committed.result, operation, params, root, occurredAt)).body;
|
|
755
|
+
return (await finishWrite(m, call, resource, row, committed.result, operation, params, root, occurredAt, before)).body;
|
|
741
756
|
}
|
|
742
|
-
async function finishWrite(m, call, resource, row, result, operation, params, root, occurredAt) {
|
|
743
|
-
await keepCounts(m, call, resource, row, params, root, occurredAt);
|
|
757
|
+
async function finishWrite(m, call, resource, row, result, operation, params, root, occurredAt, prior) {
|
|
758
|
+
await keepCounts(m, call, resource, row, params, root, occurredAt, prior);
|
|
744
759
|
const body = view(m, resource, row);
|
|
745
760
|
// the write's own context for its hooks, in the scope the call was served in (a walk's application answers, a tenant,
|
|
746
761
|
// the managed database), at the write's moment
|
|
@@ -762,27 +777,30 @@ async function finishWrite(m, call, resource, row, result, operation, params, ro
|
|
|
762
777
|
}
|
|
763
778
|
/** The declared counts a written subject is a child in (a resource's `counts`), each kept on its parent: written when
|
|
764
779
|
* the count moved, through the write path, so the parent's history says when. */
|
|
765
|
-
async function keepCounts(m, call, resource, row, params, root, occurredAt) {
|
|
780
|
+
async function keepCounts(m, call, resource, row, params, root, occurredAt, prior) {
|
|
766
781
|
for (const [parent, decl] of Object.entries(m.resources)) {
|
|
767
782
|
for (const [field, c] of Object.entries(decl.counts ?? {})) {
|
|
768
783
|
if (storedType(m, c.of) !== storedType(m, resource))
|
|
769
784
|
continue;
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
785
|
+
// the parent the subject is under now, and the one it left when the write moved it: both counts changed
|
|
786
|
+
const pids = new Set([parentField(row, c.by), prior === undefined ? undefined : parentField(prior, c.by)].filter((v) => v !== undefined && v !== null).map(String));
|
|
787
|
+
for (const pid of pids) {
|
|
788
|
+
const held = stored(m, parent, root).find((r) => String(c.parentKey ? ownFields(r)[c.parentKey] : r.id) === pid);
|
|
789
|
+
if (!held)
|
|
790
|
+
continue;
|
|
791
|
+
const n = stored(m, c.of, root).filter((r) => {
|
|
792
|
+
const x = r;
|
|
793
|
+
return String(parentField(x, c.by)) === pid && (c.present === undefined || (x[c.present] !== undefined && x[c.present] !== null)) && (c.absent === undefined || parentField(ownFields(r), c.absent) === undefined || parentField(ownFields(r), c.absent) === null) && Object.entries(c.unless ?? {}).every(([key, value]) => ownFields(r)[key] !== value);
|
|
794
|
+
}).length + (c.plus ?? 0);
|
|
795
|
+
if (held[field] !== n)
|
|
796
|
+
await writeDetailed(m, call, parent, String(held.id), { [field]: n }, `${storedType(m, parent)}.counted`, params, root, occurredAt);
|
|
797
|
+
}
|
|
783
798
|
}
|
|
784
799
|
}
|
|
785
800
|
}
|
|
801
|
+
/** Whether any resource counts this one (`counts`): only then does a write read the subject as it stood, for the parent
|
|
802
|
+
* it may leave. */
|
|
803
|
+
const counted = (m, resource) => Object.values(m.resources).some((d) => Object.values(d.counts ?? {}).some((c) => storedType(m, c.of) === storedType(m, resource)));
|
|
786
804
|
/** An answer in the vendor's success envelope, under the operation's key, when the vendor has one. */
|
|
787
805
|
function envelope(m, op, body) {
|
|
788
806
|
return m.success && op.answers?.key ? { ...m.success, [op.answers.key]: body } : body;
|
|
@@ -1129,16 +1147,16 @@ export async function serveCore(m, call, scope = {}) {
|
|
|
1129
1147
|
// the body is read from a copy, so the call's own request is still unread for the write hook's context
|
|
1130
1148
|
const params = await boundaryParams(m, { ...call, request: call.request.clone() }, root);
|
|
1131
1149
|
const idParam = subjectOf(m, resource, call);
|
|
1132
|
-
const
|
|
1133
|
-
if (orphan)
|
|
1134
|
-
return { served: orphan };
|
|
1135
|
-
const parent = decl.parent;
|
|
1150
|
+
const parent = parentOfCall(decl, call);
|
|
1136
1151
|
const parentId = parent ? parentValue(parent, call.params) : undefined;
|
|
1137
1152
|
// the caller's tenant, when the vendor has tenancy and the resource is not shared by every tenant
|
|
1138
1153
|
const tenantField = m.tenant && !decl.shared ? m.tenant.field : undefined;
|
|
1139
1154
|
const tenant = tenantField && scope.tenant ? await scope.tenant(await contextFor(m, { ...call, request: call.request.clone() }, scope)) : undefined;
|
|
1140
1155
|
if (tenantField && tenant === undefined)
|
|
1141
1156
|
return { unmodeled: `the caller names no tenant (semantics/tenant.ts) for ${resource}` };
|
|
1157
|
+
const orphan = missingParent(m, resource, call, root, tenant);
|
|
1158
|
+
if (orphan)
|
|
1159
|
+
return { served: orphan };
|
|
1142
1160
|
const underParent = (r) => (!parent || r[parent.field] === parentId)
|
|
1143
1161
|
&& (!tenantField || r[tenantField] === tenant);
|
|
1144
1162
|
const paths = expandPaths(m, params);
|
|
@@ -1222,6 +1240,10 @@ export async function serveCore(m, call, scope = {}) {
|
|
|
1222
1240
|
const requested = op.class === 'update' ? data[field] : undefined;
|
|
1223
1241
|
if (op.class === 'update' && requested === undefined)
|
|
1224
1242
|
continue;
|
|
1243
|
+
if (op.class === 'update' && sdecl.unchanged === 'allow' && stateOf(sdecl, current[field]) === stateOf(sdecl, requested)) {
|
|
1244
|
+
changes[field] = requested;
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1225
1247
|
const { move, refusal } = transitionFor(field, sdecl, op.id, current[field], requested, hit.id);
|
|
1226
1248
|
if (refusal)
|
|
1227
1249
|
return { served: vendorError(m, refusal) };
|
|
@@ -1282,6 +1304,12 @@ export function sse(m, events, operationId) {
|
|
|
1282
1304
|
return new Response(body, { status: 200, headers: { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache' } });
|
|
1283
1305
|
}
|
|
1284
1306
|
/** The context the kernel opens for a call (a handler's, a door's, a screen's). Kernel-internal: packs are given it. */
|
|
1307
|
+
/** A media type whose body the core reads for a context (and records with a write): JSON, a form, multipart, text,
|
|
1308
|
+
* or none named. */
|
|
1309
|
+
const coreReadsBody = (type) => {
|
|
1310
|
+
const media = (type ?? '').split(';')[0].trim().toLowerCase();
|
|
1311
|
+
return media === '' || media === 'application/json' || media.endsWith('+json') || media === 'application/x-www-form-urlencoded' || media.startsWith('multipart/') || media.startsWith('text/');
|
|
1312
|
+
};
|
|
1285
1313
|
export async function contextFor(m, call, scope) {
|
|
1286
1314
|
const root = scope.root;
|
|
1287
1315
|
const asAsked = call;
|
|
@@ -1291,8 +1319,10 @@ export async function contextFor(m, call, scope) {
|
|
|
1291
1319
|
// the guard's parse when it made one (taken once), else this context's own
|
|
1292
1320
|
// a body that is its payload's bytes (S3's PutObject) is never read as JSON, whatever its label
|
|
1293
1321
|
const payload = bytesPayload(call.operation);
|
|
1294
|
-
|
|
1295
|
-
const
|
|
1322
|
+
// a screen's body the core does not read (an upload's bytes) is left to its handler, unread (DerivedCall.streamed)
|
|
1323
|
+
const opaque = call.streamed === true && !coreReadsBody(call.request.headers.get('content-type'));
|
|
1324
|
+
const parsed = payload || opaque ? undefined : takeParsedBody(call.request);
|
|
1325
|
+
const text = opaque ? '' : await bodyTextOf(call.request.clone()).catch(() => '');
|
|
1296
1326
|
let body = parsed?.value;
|
|
1297
1327
|
if (!parsed) {
|
|
1298
1328
|
if (payload)
|
|
@@ -1336,7 +1366,7 @@ export async function contextFor(m, call, scope) {
|
|
|
1336
1366
|
rawResources.set(row, name ?? resource);
|
|
1337
1367
|
return row;
|
|
1338
1368
|
};
|
|
1339
|
-
|
|
1369
|
+
const context = {
|
|
1340
1370
|
call,
|
|
1341
1371
|
params,
|
|
1342
1372
|
id: Object.values(call.params).at(-1),
|
|
@@ -1355,7 +1385,6 @@ export async function contextFor(m, call, scope) {
|
|
|
1355
1385
|
return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/repos/${name}`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
|
|
1356
1386
|
},
|
|
1357
1387
|
mail: (route, mail, headers) => sendMail(route, mail, headers),
|
|
1358
|
-
actor: m.identity ? m.identity(call.request.headers.get('authorization'), root) : undefined,
|
|
1359
1388
|
now: () => now(m, at),
|
|
1360
1389
|
render: (row, resource) => {
|
|
1361
1390
|
const raw = rawResources.has(row);
|
|
@@ -1396,11 +1425,17 @@ export async function contextFor(m, call, scope) {
|
|
|
1396
1425
|
record: async (type, fields, id, operation) => {
|
|
1397
1426
|
if (!type.startsWith('_'))
|
|
1398
1427
|
throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
|
|
1428
|
+
// a bookkeeping subject a resource counts (`counts` naming it: sentry's issue users) keeps that count as any
|
|
1429
|
+
// child does, the parent it left recounted too
|
|
1430
|
+
const keeps = counted(m, type);
|
|
1399
1431
|
if (id !== undefined) {
|
|
1400
1432
|
const before = operation ? stateBeforeWrite(m, type, id, root) : undefined;
|
|
1401
|
-
const
|
|
1433
|
+
const prior = keeps ? resourcesOfType(m.service, type, root).find((r) => r.id === id) : undefined;
|
|
1434
|
+
const { resource: row, result } = await applyTwinWrite(m.service, { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: id, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } }, root);
|
|
1402
1435
|
if (operation)
|
|
1403
1436
|
observeStateWrite(m, result, before, root, stateChecks);
|
|
1437
|
+
if (keeps)
|
|
1438
|
+
await keepCounts(m, call, type, row, params, root, at, prior && { ...prior });
|
|
1404
1439
|
return id;
|
|
1405
1440
|
}
|
|
1406
1441
|
const { value, result } = await applyTwinWriteAtomic(m.service, (resources) => {
|
|
@@ -1421,6 +1456,9 @@ export async function contextFor(m, call, scope) {
|
|
|
1421
1456
|
}, root, () => resourcesOfType(m.service, type, root));
|
|
1422
1457
|
if (operation && result)
|
|
1423
1458
|
observeStateWrite(m, result, undefined, root, stateChecks);
|
|
1459
|
+
const made = keeps && value !== undefined ? resourcesOfType(m.service, type, root).find((r) => r.id === value) : undefined;
|
|
1460
|
+
if (made)
|
|
1461
|
+
await keepCounts(m, call, type, made, params, root, at);
|
|
1424
1462
|
return value;
|
|
1425
1463
|
},
|
|
1426
1464
|
get machines() {
|
|
@@ -1433,7 +1471,9 @@ export async function contextFor(m, call, scope) {
|
|
|
1433
1471
|
const id = mintId(m, resource, root, at, snapshot);
|
|
1434
1472
|
return { id, fields: typeof fields === 'function' ? fields(id) : fields };
|
|
1435
1473
|
}, operation, params, root, at, undefined, stateChecks).then((row) => row),
|
|
1436
|
-
createUnique: async (resource, fields, operation) => {
|
|
1474
|
+
createUnique: async (resource, fields, operation, options) => {
|
|
1475
|
+
if (options?.input === 'omit' && !resource.startsWith('_'))
|
|
1476
|
+
throw new Error('only bookkeeping writes may omit request input');
|
|
1437
1477
|
const rules = m.resources[resource]?.unique;
|
|
1438
1478
|
if (!rules?.length)
|
|
1439
1479
|
throw new Error('createUnique requires declared unique keys');
|
|
@@ -1447,16 +1487,20 @@ export async function contextFor(m, call, scope) {
|
|
|
1447
1487
|
return undefined;
|
|
1448
1488
|
}
|
|
1449
1489
|
return { id, fields: selected };
|
|
1450
|
-
}, operation, params, root, at, undefined, stateChecks);
|
|
1490
|
+
}, operation, params, root, at, undefined, stateChecks, options?.input === 'omit');
|
|
1451
1491
|
return existing ? { created: false, row: existing } : { created: true, row: row };
|
|
1452
1492
|
},
|
|
1453
|
-
change: (resource, id, decide, operation
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1493
|
+
change: (resource, id, decide, operation, options) => {
|
|
1494
|
+
if (options?.input === 'omit' && !resource.startsWith('_'))
|
|
1495
|
+
throw new Error('only bookkeeping writes may omit request input');
|
|
1496
|
+
return atomicWrite(m, call, resource, (snapshot) => {
|
|
1497
|
+
const row = snapshot.find((r) => r.type === storedType(m, resource) && r.id === id && r.deleted !== true);
|
|
1498
|
+
if (!row)
|
|
1499
|
+
return undefined;
|
|
1500
|
+
const fields = decide(row);
|
|
1501
|
+
return fields === undefined ? undefined : { id, fields };
|
|
1502
|
+
}, operation, params, root, at, id, stateChecks, options?.input === 'omit');
|
|
1503
|
+
},
|
|
1460
1504
|
write: (resource, id, fields, operation) => write(m, call, resource, id, fields, operation, params, root, at, stateChecks),
|
|
1461
1505
|
accumulate: async (resource, id, deltas, operation) => {
|
|
1462
1506
|
if (!resource.startsWith('_') || !m.resources[resource])
|
|
@@ -1542,8 +1586,11 @@ export async function contextFor(m, call, scope) {
|
|
|
1542
1586
|
live: liveAccess({ rows: (type) => resourcesOfType(m.service, type, root), blob: (key) => readResourceBlob(m.service, key, root) }, call.request),
|
|
1543
1587
|
blobs: {
|
|
1544
1588
|
put: (key, bytes) => writeResourceBlob(m.service, key, bytes, root),
|
|
1589
|
+
putStream: (key, body, accept) => writeResourceBlobStream(m.service, key, body, accept, root),
|
|
1545
1590
|
get: (key) => readResourceBlob(m.service, key, root),
|
|
1546
1591
|
remove: (key) => removeResourceBlob(m.service, key, root),
|
|
1592
|
+
size: (key) => resourceBlobSize(m.service, key, root),
|
|
1593
|
+
range: (key, start, endInclusive) => readResourceBlobRange(m.service, key, start, endInclusive, root),
|
|
1547
1594
|
},
|
|
1548
1595
|
merge: (resource, prior, next) => mergeField(m.resources[resource]?.update, prior, next),
|
|
1549
1596
|
conflict: (resource, fields, except) => {
|
|
@@ -1563,12 +1610,240 @@ export async function contextFor(m, call, scope) {
|
|
|
1563
1610
|
over: (other) => contextFor(other, { ...asAsked, request: asAsked.request.clone() }, { ...scope, clock: () => at }),
|
|
1564
1611
|
core: async () => { const out = await serveCore(m, { ...call, request: pristine.clone() }, { ...scope, clock: () => at }); return 'served' in out ? out.served : undefined; },
|
|
1565
1612
|
own: (row) => ownFields(row),
|
|
1613
|
+
batch: (fn) => fn(),
|
|
1614
|
+
};
|
|
1615
|
+
if (batchable(m, root))
|
|
1616
|
+
bindBatch(m, call, context, params, root, at, rawRow);
|
|
1617
|
+
return context;
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Whether a call's writes may be grouped (`ctx.batch`) with nothing about them changing: every effect a write has
|
|
1621
|
+
* besides its action and the tree (write hooks, events, sockets, a parent's counts, a state observer, a head that
|
|
1622
|
+
* performs it, an alias a read resolves through) is one the grouped path would move, so where any is present the
|
|
1623
|
+
* writes land one at a time, as they always have.
|
|
1624
|
+
*/
|
|
1625
|
+
function batchable(m, root) {
|
|
1626
|
+
return !m.onWrite && !m.events && !m.sockets?.length && !transitionObserver
|
|
1627
|
+
&& !Object.values(m.resources).some((decl) => decl.counts && Object.keys(decl.counts).length > 0)
|
|
1628
|
+
&& !performsAtHeadNow(m.service, root) && subjectAliases(m.service, root).size === 0;
|
|
1629
|
+
}
|
|
1630
|
+
/** The members of a context that read the tree other than `row`/`rowsRaw`: refused while writes are staged. */
|
|
1631
|
+
const BATCH_REFUSED_READS = ['get', 'rows', 'tree', 'history', 'find', 'list', 'expand', 'mint', 'conflict', 'ownerRow', 'render'];
|
|
1632
|
+
/** The members that write, or wait on writes, or answer as another context: each lands the staged writes first. */
|
|
1633
|
+
const BATCH_LANDING_MEMBERS = ['create', 'createUnique', 'change', 'accumulate', 'writeDetailed', 'record', 'atomically', 'remove', 'issue', 'at', 'over', 'core', 'awaitWrite', 'awaitCondition',
|
|
1634
|
+
// a World's seed and signing keys are written when first asked (worldSeed, worldSigningKey, as a vendor's move)
|
|
1635
|
+
'secret', 'signingKey'];
|
|
1636
|
+
/** `ctx.batch` over a context whose writes may be grouped (see `batchable`). */
|
|
1637
|
+
function bindBatch(m, call, context, params, root, at, rawRow) {
|
|
1638
|
+
let staged;
|
|
1639
|
+
// each staged subject as it will stand, by `type:id`, in the order first written
|
|
1640
|
+
const subjects = new Map();
|
|
1641
|
+
const land = async () => {
|
|
1642
|
+
if (!staged?.length)
|
|
1643
|
+
return;
|
|
1644
|
+
const writes = staged;
|
|
1645
|
+
staged = [];
|
|
1646
|
+
subjects.clear();
|
|
1647
|
+
await writeGroup(m, call, writes, params, root, at);
|
|
1566
1648
|
};
|
|
1649
|
+
const ctx = context;
|
|
1650
|
+
for (const name of BATCH_LANDING_MEMBERS) {
|
|
1651
|
+
const plain = ctx[name];
|
|
1652
|
+
if (typeof plain !== 'function')
|
|
1653
|
+
continue;
|
|
1654
|
+
ctx[name] = async (...args) => { await land(); return plain(...args); };
|
|
1655
|
+
}
|
|
1656
|
+
for (const name of BATCH_REFUSED_READS) {
|
|
1657
|
+
const plain = ctx[name];
|
|
1658
|
+
if (typeof plain !== 'function')
|
|
1659
|
+
continue;
|
|
1660
|
+
ctx[name] = (...args) => {
|
|
1661
|
+
if (staged?.length)
|
|
1662
|
+
throw new Error(`ctx.${name} reads the tree while ctx.batch holds staged writes: only row and rowsRaw see them`);
|
|
1663
|
+
return plain(...args);
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
const plainWrite = context.write, plainRow = context.row, plainRowsRaw = context.rowsRaw;
|
|
1667
|
+
context.write = async (resource, id, fields, operation) => {
|
|
1668
|
+
// a vendor's own move (ctx.asVendor) is the clock's write, recorded as the system's: it lands alone, in its place
|
|
1669
|
+
if (!staged || inVendorMove()) {
|
|
1670
|
+
await land();
|
|
1671
|
+
return plainWrite(resource, id, fields, operation);
|
|
1672
|
+
}
|
|
1673
|
+
const type = storedType(m, resource), key = `${type}:${id}`;
|
|
1674
|
+
// the one subject, as the memoized tree holds it (readTreeMap would copy the whole tree on every staged write)
|
|
1675
|
+
const subject = foldLocalWrite(subjects.get(key) ?? heldSubject(m.service, type, id, root), type, id, fields, at);
|
|
1676
|
+
subjects.set(key, subject);
|
|
1677
|
+
staged.push({ resource, id, fields, operation });
|
|
1678
|
+
return view(m, resource, treeResources(new Map([[key, subject]]))[0]);
|
|
1679
|
+
};
|
|
1680
|
+
// the stored rows of a resource's type with the staged subjects folded over them, filtered as `stored` filters
|
|
1681
|
+
const overlaid = (resource, opts) => {
|
|
1682
|
+
if (!staged?.length)
|
|
1683
|
+
return undefined;
|
|
1684
|
+
const type = storedType(m, resource), prefix = `${type}:`;
|
|
1685
|
+
const mine = [...subjects].filter(([key]) => key.startsWith(prefix));
|
|
1686
|
+
if (!mine.length)
|
|
1687
|
+
return undefined;
|
|
1688
|
+
const replaced = new Map(mine.map(([key, subject]) => [key, treeResources(new Map([[key, subject]]))[0]]));
|
|
1689
|
+
const base = resourcesOfType(m.service, type, root);
|
|
1690
|
+
// a subject the tree holds stays where it stands; one new to it goes to the end, in the order first written
|
|
1691
|
+
const rows = base.map((row) => replaced.get(`${prefix}${row.id}`) ?? row);
|
|
1692
|
+
const held = new Set(base.map((row) => `${prefix}${row.id}`));
|
|
1693
|
+
for (const [key, row] of replaced)
|
|
1694
|
+
if (!held.has(key))
|
|
1695
|
+
rows.push(row);
|
|
1696
|
+
const when = Object.entries(m.resources[resource]?.readableWhen ?? {});
|
|
1697
|
+
return rows.filter((r) => (opts?.withDeleted || r.deleted !== true) && when.every(([k, v]) => r[k] === v));
|
|
1698
|
+
};
|
|
1699
|
+
context.row = (resource, id, opts) => {
|
|
1700
|
+
const rows = overlaid(resource, opts);
|
|
1701
|
+
if (!rows)
|
|
1702
|
+
return plainRow(resource, id, opts);
|
|
1703
|
+
const row = rows.find((r) => r.id === id);
|
|
1704
|
+
return row ? rawRow(resource, row) : undefined;
|
|
1705
|
+
};
|
|
1706
|
+
context.rowsRaw = (resource, opts) => {
|
|
1707
|
+
const rows = overlaid(resource, opts);
|
|
1708
|
+
return rows ? rows.map((row) => rawRow(resource, row)) : plainRowsRaw(resource, opts);
|
|
1709
|
+
};
|
|
1710
|
+
context.batch = async (fn) => {
|
|
1711
|
+
// a batch inside a batch is the outer one's
|
|
1712
|
+
if (staged !== undefined)
|
|
1713
|
+
return fn();
|
|
1714
|
+
staged = [];
|
|
1715
|
+
try {
|
|
1716
|
+
return await fn();
|
|
1717
|
+
}
|
|
1718
|
+
finally {
|
|
1719
|
+
// every write `write` answered lands, whether fn returned or threw: an ordered insert's prefix stays written
|
|
1720
|
+
try {
|
|
1721
|
+
await land();
|
|
1722
|
+
}
|
|
1723
|
+
finally {
|
|
1724
|
+
staged = undefined;
|
|
1725
|
+
subjects.clear();
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* A command's staged writes landed as one group: each recorded with the request as `writeDetailed` records it (one
|
|
1732
|
+
* recordedInput, the same for every write of the call), the call's moment and actor, in order, under one lock and one
|
|
1733
|
+
* append (serve.ts applyTwinWrites); then a held read on each written type is told, as each write tells it.
|
|
1734
|
+
*/
|
|
1735
|
+
async function writeGroup(m, call, writes, params, root, occurredAt) {
|
|
1736
|
+
const input = recordedInput(m, call, params);
|
|
1737
|
+
const files = input.operationId ? await keptFiles(m, call, root) : undefined;
|
|
1738
|
+
const recorded = files ? { ...input, files } : input;
|
|
1739
|
+
const actor = { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) };
|
|
1740
|
+
await applyTwinWrites(m.service, writes.map((write) => ({ operation: write.operation, subjectType: storedType(m, write.resource),
|
|
1741
|
+
subjectId: write.id, fields: write.fields, input: recorded, occurredAt, actor })), root);
|
|
1742
|
+
for (const type of new Set(writes.map((write) => storedType(m, write.resource))))
|
|
1743
|
+
heldReadsWrite(m.service, root, type);
|
|
1567
1744
|
}
|
|
1568
1745
|
/** A pack's semantics handlers as the dispatch's handlers. */
|
|
1569
1746
|
export function bindSemantics(m, handlers, scope = {}) {
|
|
1570
1747
|
prepareViewOmissions(m);
|
|
1571
|
-
return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call) =>
|
|
1748
|
+
return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call) => {
|
|
1749
|
+
const ctx = await contextFor(m, call, scope);
|
|
1750
|
+
redisServiceTimes.lap('6c the handler\'s own context opened');
|
|
1751
|
+
// an operation's own variants, else the ones every operation shares (`'*'`)
|
|
1752
|
+
const variants = m.variants?.[id] ?? m.variants?.['*'];
|
|
1753
|
+
if (variants && variantAsked(ctx.params, variants, call.request.headers))
|
|
1754
|
+
return variantGap(m, call.request);
|
|
1755
|
+
const outside = enumRefused(m, call, ctx.params);
|
|
1756
|
+
return outside ?? h(ctx);
|
|
1757
|
+
}]));
|
|
1758
|
+
}
|
|
1759
|
+
/** The first of a request's form parameters whose value is not one its spec lists (DerivedOperation.enums), with the
|
|
1760
|
+
* bracket name a form gives it (`spending_controls[spending_limits][0][interval]`) and the values it takes. */
|
|
1761
|
+
export function valueOutside(params, enums) {
|
|
1762
|
+
const at = (value, path, name, values) => {
|
|
1763
|
+
if (value === undefined || value === null)
|
|
1764
|
+
return undefined;
|
|
1765
|
+
if (!path.length)
|
|
1766
|
+
return typeof value === 'object' || values.includes(String(value)) ? undefined : { param: name, values };
|
|
1767
|
+
const [head, ...rest] = path;
|
|
1768
|
+
if (head === '[]') {
|
|
1769
|
+
const items = Array.isArray(value) ? value.map((v, i) => [String(i), v]) : value && typeof value === 'object' ? Object.entries(value) : [];
|
|
1770
|
+
for (const [i, v] of items) {
|
|
1771
|
+
const found = at(v, rest, `${name}[${i}]`, values);
|
|
1772
|
+
if (found)
|
|
1773
|
+
return found;
|
|
1774
|
+
}
|
|
1775
|
+
return undefined;
|
|
1776
|
+
}
|
|
1777
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? at(value[head], rest, `${name}[${head}]`, values) : undefined;
|
|
1778
|
+
};
|
|
1779
|
+
for (const [path, values] of Object.entries(enums)) {
|
|
1780
|
+
const [first, ...rest] = path.split('.');
|
|
1781
|
+
const found = at(params[first], rest, first, values);
|
|
1782
|
+
if (found)
|
|
1783
|
+
return found;
|
|
1784
|
+
}
|
|
1785
|
+
return undefined;
|
|
1786
|
+
}
|
|
1787
|
+
/** The manifest's refusal of a form value outside the operation's spec lists (and the values documented beside
|
|
1788
|
+
* them), unless the request names an API version other than the one the spec is. */
|
|
1789
|
+
function enumRefused(m, call, params) {
|
|
1790
|
+
const refusal = m.body.form?.enumRefusal;
|
|
1791
|
+
if (!refusal || !call.operation.enums)
|
|
1792
|
+
return undefined;
|
|
1793
|
+
const pinned = refusal.version ? call.request.headers.get(refusal.version.header) : null;
|
|
1794
|
+
if (pinned && pinned !== refusal.version.served)
|
|
1795
|
+
return undefined;
|
|
1796
|
+
const documented = refusal.documented?.[call.operation.id] ?? {};
|
|
1797
|
+
const enums = Object.fromEntries(Object.entries(call.operation.enums).map(([path, values]) => [path, [...values, ...(documented[path] ?? [])]]));
|
|
1798
|
+
const outside = valueOutside(params, enums);
|
|
1799
|
+
if (!outside)
|
|
1800
|
+
return undefined;
|
|
1801
|
+
const { documented: _d, version: _v, ...error } = refusal;
|
|
1802
|
+
return vendorError(m, { ...error, param: outside.param, message: error.message.replace(/\{param\}/g, outside.param).replace(/\{values\}/g, oneOfList(outside.values)) });
|
|
1803
|
+
}
|
|
1804
|
+
/** A list of values as a sentence names them: `a, b, or c`. */
|
|
1805
|
+
const oneOfList = (values) => (values.length < 2 ? values.join('') : `${values.slice(0, -1).join(', ')}, or ${values[values.length - 1]}`);
|
|
1806
|
+
/** Whether a request's parameters ask one of the operation's undemanded variants (DerivedManifest.variants). */
|
|
1807
|
+
export function variantAsked(params, variants, headers) {
|
|
1808
|
+
const valuesAt = (value, path) => {
|
|
1809
|
+
if (!path.length)
|
|
1810
|
+
return value === undefined ? [] : [value];
|
|
1811
|
+
const [head, ...rest] = path;
|
|
1812
|
+
// `!name` is the field left out: the value itself where it has none (`items[].!id`, an item naming no id)
|
|
1813
|
+
if (head.startsWith('!'))
|
|
1814
|
+
return value && typeof value === 'object' && value[head.slice(1)] === undefined ? [value] : [];
|
|
1815
|
+
const list = head.endsWith('[]');
|
|
1816
|
+
const key = list ? head.slice(0, -2) : head;
|
|
1817
|
+
const next = value && typeof value === 'object' ? value[key] : undefined;
|
|
1818
|
+
if (!list)
|
|
1819
|
+
return valuesAt(next, rest);
|
|
1820
|
+
const items = Array.isArray(next) ? next : next && typeof next === 'object' ? Object.values(next) : [];
|
|
1821
|
+
return items.flatMap((item) => valuesAt(item, rest));
|
|
1822
|
+
};
|
|
1823
|
+
// a variant joined with `&` is its parts asked together (`trial_end=now&items`): the combination, not either alone;
|
|
1824
|
+
// a part led by `!` is a parameter left out (`items[].price&!items[].id`, an item added rather than changed)
|
|
1825
|
+
return variants.some((variant) => variant.split('&').every((part) => {
|
|
1826
|
+
const absent = part.startsWith('!');
|
|
1827
|
+
// `name~=value` is one entry of a comma-separated value (`tweet.fields~=geo`, X's fields lists); `name=value` the whole
|
|
1828
|
+
const body = absent ? part.slice(1) : part;
|
|
1829
|
+
const listed = body.includes('~=');
|
|
1830
|
+
const [path, wanted] = listed ? body.split('~=') : body.split('=');
|
|
1831
|
+
// a part led by `@` names a request header (`@stripe-account&application_fee_amount`, a charge made on a connected
|
|
1832
|
+
// account with the platform's fee)
|
|
1833
|
+
const header = path.startsWith('@') ? headers?.get(path.slice(1)) : undefined;
|
|
1834
|
+
// a parameter whose own name holds a dot (X's `tweet.fields`) is read by that name before the path is walked
|
|
1835
|
+
const found = path.startsWith('@') ? (header === null || header === undefined ? [] : [header])
|
|
1836
|
+
: Object.hasOwn(params, path) ? (params[path] === undefined ? [] : [params[path]]) : valuesAt(params, path.split('.'));
|
|
1837
|
+
const asked = wanted === undefined ? found.length > 0
|
|
1838
|
+
: found.some((value) => (listed ? String(value).split(',').map((entry) => entry.trim()).includes(wanted) : String(value) === wanted));
|
|
1839
|
+
return absent ? !asked : asked;
|
|
1840
|
+
}));
|
|
1841
|
+
}
|
|
1842
|
+
/** The manifest's gap, as a variant is answered: the unknown-request refusal (else its not-found). */
|
|
1843
|
+
function variantGap(m, request) {
|
|
1844
|
+
const nf = m.notFound;
|
|
1845
|
+
const gap = m.gap ?? { status: nf.status, message: nf.message, ...(nf.code !== undefined ? { code: nf.code } : {}), ...(nf.kind !== undefined ? { kind: nf.kind } : {}) };
|
|
1846
|
+
return vendorError(m, { ...gap, message: gap.message.replace(/\{method\}/g, request.method).replace(/\{path\}/g, new URL(request.url).pathname) });
|
|
1572
1847
|
}
|
|
1573
1848
|
/** The derived core as the dispatch's core: it owns every operation on a resource the manifest declares. */
|
|
1574
1849
|
export function coreFor(m, scope = {}) {
|