@volter/world-core 3.0.156 → 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 +77 -8
- 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/blob-store.d.ts +16 -0
- package/dist/src/blob-store.js +76 -1
- package/dist/src/derived-core.d.ts +72 -6
- package/dist/src/derived-core.js +192 -51
- package/dist/src/derived-real.js +97 -15
- package/dist/src/derived.d.ts +5 -0
- package/dist/src/executor.d.ts +7 -0
- package/dist/src/executor.js +15 -1
- package/dist/src/git/lfs.d.ts +13 -3
- package/dist/src/git/lfs.js +41 -9
- package/dist/src/head.d.ts +2 -0
- package/dist/src/head.js +6 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- 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/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/mongodb/stream.d.ts +23 -0
- package/dist/src/pack-fetch.d.ts +6 -1
- package/dist/src/pack-fetch.js +49 -6
- package/dist/src/pack-paths.d.ts +2 -1
- package/dist/src/pack-paths.js +6 -1
- package/dist/src/resource-blob.d.ts +4 -0
- package/dist/src/resource-blob.js +8 -1
- package/dist/src/runtime.d.ts +3 -3
- package/dist/src/runtime.js +2 -2
- package/dist/src/serve-http.js +53 -3
- package/dist/src/sockets.d.ts +4 -1
- package/dist/src/sockets.js +59 -6
- package/dist/src/vendor-call.js +58 -0
- package/dist/src/world-store.d.ts +15 -1
- package/dist/src/world-store.js +78 -2
- package/generated/pack-facts.json +77 -8
- package/network-policy.cjs +11 -1
- package/network-policy.d.cts +1 -0
- package/pack-facts.d.cts +3 -0
- package/package.json +8 -1
- package/required-credential-transport.cjs +29 -0
- package/required-credential-transport.d.cts +3 -0
- package/src/blob-store.ts +59 -1
- package/src/derived-core.ts +215 -51
- package/src/derived-real.ts +80 -14
- package/src/derived.ts +6 -1
- package/src/executor.ts +17 -3
- package/src/git/lfs.ts +40 -9
- package/src/head.ts +6 -0
- package/src/index.ts +1 -0
- package/src/live/ingest.ts +7 -3
- package/src/live/rtmp.ts +7 -5
- package/src/live/segmenter.ts +7 -1
- package/src/machines.ts +5 -1
- package/src/managed-go-programs.ts +146 -0
- package/src/mongodb/stream.ts +10 -0
- package/src/pack-fetch.ts +46 -8
- package/src/pack-paths.ts +5 -1
- package/src/resource-blob.ts +9 -1
- package/src/runtime.ts +4 -2
- package/src/serve-http.ts +43 -3
- package/src/sockets.ts +39 -8
- package/src/vendor-call.ts +54 -0
- package/src/world-store.ts +66 -2
|
@@ -68,6 +68,8 @@ export type Transition = {
|
|
|
68
68
|
export type StateField = {
|
|
69
69
|
initial: string | boolean;
|
|
70
70
|
transitions: Transition[];
|
|
71
|
+
/** Plain updates may repeat the current representation without another state move. */
|
|
72
|
+
unchanged?: 'allow';
|
|
71
73
|
derive?: Array<{
|
|
72
74
|
state: string;
|
|
73
75
|
when: {
|
|
@@ -139,6 +141,8 @@ export type ResourceDecl = {
|
|
|
139
141
|
resource: string;
|
|
140
142
|
allowDeleted?: boolean;
|
|
141
143
|
where?: Record<string, string>;
|
|
144
|
+
/** Global subject paths omit the collection's parent labels; tenancy still applies. */
|
|
145
|
+
whenNamed?: true;
|
|
142
146
|
/** private fields a child observed under its parent keeps from the parent's parameters (`_jurisdiction:
|
|
143
147
|
* '{jurisdiction}'`), where the child's own read names none and a later read under it needs one ("Refresh") */
|
|
144
148
|
keep?: Record<string, string>;
|
|
@@ -215,7 +219,9 @@ export type ResourceDecl = {
|
|
|
215
219
|
* holds this one's id (or vendor `parentKey`), only those with `present` set or `absent` unset when named and without
|
|
216
220
|
* any matching `unless` field, plus `plus` (a customer's
|
|
217
221
|
* `next_invoice_sequence` is one past its numbered invoices). `by` may be a dot path; exact field names take
|
|
218
|
-
* precedence. The kernel keeps each whenever a child is written
|
|
222
|
+
* precedence. The kernel keeps each whenever a child is written, the parent it left too when the write moves it; `of`
|
|
223
|
+
* may name a bookkeeping type no resource declares, written with `ctx.record`. A count is kept by the lane that
|
|
224
|
+
* declares it: a child another lane writes is not counted there. */
|
|
219
225
|
counts?: Record<string, {
|
|
220
226
|
of: string;
|
|
221
227
|
by: string;
|
|
@@ -261,6 +267,10 @@ export type ResourceDecl = {
|
|
|
261
267
|
} | {
|
|
262
268
|
get: string;
|
|
263
269
|
known?: true;
|
|
270
|
+
gone?: {
|
|
271
|
+
status: number;
|
|
272
|
+
says: string;
|
|
273
|
+
};
|
|
264
274
|
}) & {
|
|
265
275
|
params?: Record<string, string>;
|
|
266
276
|
headers?: Record<string, string>;
|
|
@@ -376,6 +386,17 @@ export type ErrorSpec = {
|
|
|
376
386
|
param?: string;
|
|
377
387
|
kind?: string;
|
|
378
388
|
};
|
|
389
|
+
/** How a form value outside its spec's list is refused (DerivedOperation.enums): the error, its message filled with
|
|
390
|
+
* `{param}` (the bracket name) and `{values}` (the values it takes); `documented`, values the vendor documents outside
|
|
391
|
+
* its spec, by operation and path; `version`, the header naming the API version a request is made at and the version
|
|
392
|
+
* the spec is, so a request pinned to another version, whose lists the pack does not hold, is not checked. */
|
|
393
|
+
export type EnumRefusal = ErrorSpec & {
|
|
394
|
+
documented?: Record<string, Record<string, string[]>>;
|
|
395
|
+
version?: {
|
|
396
|
+
header: string;
|
|
397
|
+
served: string;
|
|
398
|
+
};
|
|
399
|
+
};
|
|
379
400
|
/** A vendor screen a World serves (docs/contributing/architecture.md, "Screens"). A `flow` is a hosted
|
|
380
401
|
* page an application sends its user through, held to its documented round trip; a `workspace` is a
|
|
381
402
|
* screen people work in. `demand` says who reaches it; `controls` names what a person acts on, the
|
|
@@ -401,6 +422,14 @@ export type ScreenDecl = {
|
|
|
401
422
|
* takes, with its demand (an upload bucket a page posts a lease's form to: Reddit's S3 hosts) (architecture, "A page
|
|
402
423
|
* at a World's place") */
|
|
403
424
|
takes?: ReadonlyArray<'POST'>;
|
|
425
|
+
/** a workspace page that shows one record: the stored type and the path parameter its id fills (a pull request's
|
|
426
|
+
* page, `{number}`); `?show=<type>:<id>` asked of the pack's workspaces opens it (architecture, "Workspace") */
|
|
427
|
+
shows?: {
|
|
428
|
+
type: string;
|
|
429
|
+
param: string;
|
|
430
|
+
};
|
|
431
|
+
/** the stored types a workspace page lists, where `?show=` lands for a record no page shows by itself */
|
|
432
|
+
lists?: ReadonlyArray<string>;
|
|
404
433
|
/** the vendor's documentation of the round trip or the screen */
|
|
405
434
|
source: string;
|
|
406
435
|
};
|
|
@@ -443,6 +472,7 @@ export type DerivedManifest = {
|
|
|
443
472
|
body: {
|
|
444
473
|
form?: {
|
|
445
474
|
coerce: boolean;
|
|
475
|
+
enumRefusal?: EnumRefusal;
|
|
446
476
|
};
|
|
447
477
|
json?: 'always';
|
|
448
478
|
validation?: {
|
|
@@ -458,6 +488,7 @@ export type DerivedManifest = {
|
|
|
458
488
|
unknown: string;
|
|
459
489
|
type: string;
|
|
460
490
|
range: string;
|
|
491
|
+
arrays?: Record<string, 'string'>;
|
|
461
492
|
};
|
|
462
493
|
};
|
|
463
494
|
/** `acceptProvided`: a create that names its own `id` keeps it (a vendor that seeds by id). The template counts
|
|
@@ -469,6 +500,13 @@ export type DerivedManifest = {
|
|
|
469
500
|
};
|
|
470
501
|
/** operations the derived core must not claim though their resource is declared: a gap until someone models them */
|
|
471
502
|
unmodeled?: string[];
|
|
503
|
+
/** documented variants of a served operation that no demand asks for, by operation: each a parameter present
|
|
504
|
+
* (`pause_collection`), present on any item of a list (`items[].price_data`), or with a value
|
|
505
|
+
* (`payment_method_data.type=sepa_debit`, `payment_method_types[]=sepa_debit`), or holding one entry of a comma-separated
|
|
506
|
+
* value (`tweet.fields~=geo`; a parameter whose name holds a dot is read by that name). A request asking one is answered the
|
|
507
|
+
* manifest's gap before its handler runs, as an operation no handler serves is (architecture, "Creating a pack",
|
|
508
|
+
* step 7), so no pack line answers it */
|
|
509
|
+
variants?: Record<string, string[]>;
|
|
472
510
|
/** operations that only read though nothing in the spec says so (a POST that returns data):
|
|
473
511
|
* a read-only twin answers them */
|
|
474
512
|
reads?: string[];
|
|
@@ -506,6 +544,9 @@ export type DerivedManifest = {
|
|
|
506
544
|
readOnly: ErrorSpec;
|
|
507
545
|
/** what a body labelled JSON that does not parse answers; absent, a 400 in the vendor's error body */
|
|
508
546
|
malformedBody?: ErrorSpec;
|
|
547
|
+
/** the vendor's front merges repeated slashes in a path before it routes (nginx's default `merge_slashes`): a request
|
|
548
|
+
* to `//r/x//about` is `/r/x/about`, merged on arrival before a door, a screen or an operation is matched */
|
|
549
|
+
mergeSlashes?: boolean;
|
|
509
550
|
/** a request header that selects the vendor's API version, and the answer to a malformed one
|
|
510
551
|
* (`{value}` in its message is the header's value) */
|
|
511
552
|
version?: {
|
|
@@ -702,7 +743,13 @@ export type DerivedManifest = {
|
|
|
702
743
|
method: string;
|
|
703
744
|
path: string;
|
|
704
745
|
note?: string;
|
|
746
|
+
redactBody?: string[];
|
|
705
747
|
}>;
|
|
748
|
+
/** REST refresh binds observed tenant metadata through the root account identity endpoint. */
|
|
749
|
+
refreshIdentity?: {
|
|
750
|
+
operation: string;
|
|
751
|
+
field: string;
|
|
752
|
+
};
|
|
706
753
|
/** The vendor's GraphQL API beside its REST one (`semantics/graphql.ts` over `generated/graphql-sdl.gen.json`): the
|
|
707
754
|
* paths it is POSTed to, and the answer to a query selecting a field nothing models (`{field}`, `{type}`). */
|
|
708
755
|
graphql?: {
|
|
@@ -939,8 +986,6 @@ export type SemanticsContext = {
|
|
|
939
986
|
/** A mail the vendor sends a person, over SMTP to the route its settings name (smtp.ts), under the World's egress rule;
|
|
940
987
|
* throws when it cannot be delivered. */
|
|
941
988
|
mail(route: SmtpRoute, mail: Mail, headers?: Record<string, string>): Promise<void>;
|
|
942
|
-
/** who is calling, when the manifest says how to tell */
|
|
943
|
-
actor: string | undefined;
|
|
944
989
|
now(): unknown;
|
|
945
990
|
/** Render a stored row or vendor view using its resource declaration, retaining the stored subject id fallback. */
|
|
946
991
|
render(row: Record<string, unknown>, resource?: string): Record<string, unknown>;
|
|
@@ -985,12 +1030,16 @@ export type SemanticsContext = {
|
|
|
985
1030
|
/** Allocate a stored occurrence and append its fields under one action lock. The optional field factory is pure. */
|
|
986
1031
|
create(resource: string, fields: Record<string, unknown> | ((id: string) => Record<string, unknown>), operation: string): Promise<Record<string, unknown>>;
|
|
987
1032
|
/** Create under declared unique keys and the action lock; a collision returns the existing own-field view without a write. */
|
|
988
|
-
createUnique(resource: string, fields: Record<string, unknown> | ((id: string) => Record<string, unknown>), operation: string
|
|
1033
|
+
createUnique(resource: string, fields: Record<string, unknown> | ((id: string) => Record<string, unknown>), operation: string, options?: {
|
|
1034
|
+
input: 'omit';
|
|
1035
|
+
}): Promise<{
|
|
989
1036
|
created: boolean;
|
|
990
1037
|
row: Record<string, unknown>;
|
|
991
1038
|
}>;
|
|
992
1039
|
/** Decide an update from the current live stored row under the action lock; undefined leaves it unchanged. */
|
|
993
|
-
change(resource: string, id: string, decide: (row: Record<string, unknown>) => Record<string, unknown> | undefined, operation: string
|
|
1040
|
+
change(resource: string, id: string, decide: (row: Record<string, unknown>) => Record<string, unknown> | undefined, operation: string, options?: {
|
|
1041
|
+
input: 'omit';
|
|
1042
|
+
}): Promise<Record<string, unknown> | undefined>;
|
|
994
1043
|
write(resource: string, id: string, fields: Record<string, unknown>, operation: string): Promise<Record<string, unknown>>;
|
|
995
1044
|
/**
|
|
996
1045
|
* One command's writes as one group (S2 R17; docs/contributing/architecture.md, "A command's writes as one group").
|
|
@@ -1126,6 +1175,16 @@ export type SemanticsContext = {
|
|
|
1126
1175
|
put(key: string, bytes: Uint8Array): Promise<void>;
|
|
1127
1176
|
get(key: string): Promise<Uint8Array | null>;
|
|
1128
1177
|
remove(key: string): Promise<void>;
|
|
1178
|
+
/** a body written as it arrives (a request's own stream), kept only when `accept` holds for its size and SHA-256:
|
|
1179
|
+
* an upload of a large object never held whole. True when kept */
|
|
1180
|
+
putStream(key: string, body: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>, accept: (written: {
|
|
1181
|
+
size: number;
|
|
1182
|
+
sha256: string;
|
|
1183
|
+
}) => boolean): Promise<boolean>;
|
|
1184
|
+
/** a blob's length, or null when there is none: a large object is answered without loading it */
|
|
1185
|
+
size(key: string): Promise<number | null>;
|
|
1186
|
+
/** the bytes `start`..`endInclusive` of a blob (a ranged read where the store has one), or null when there is none */
|
|
1187
|
+
range(key: string, start: number, endInclusive: number): Promise<Uint8Array | null>;
|
|
1129
1188
|
};
|
|
1130
1189
|
/** A body's fields checked against the operation's declared ones under the manifest's `body.strict`: the fields, or
|
|
1131
1190
|
* the vendor's refusal of the first that is unknown, of another type or out of its bounds. */
|
|
@@ -1155,10 +1214,17 @@ export type HandlerContext = Omit<SemanticsContext, 'now' | 'core' | 'atomically
|
|
|
1155
1214
|
export type WriteHookContext = Omit<HandlerContext, 'write' | 'batch' | 'create' | 'createUnique' | 'change' | 'issue' | 'record' | 'accumulate' | 'at' | 'over'>;
|
|
1156
1215
|
/** A derived pack's handler: one operation, by its operationId, over the contract's context. */
|
|
1157
1216
|
export type Handler = (ctx: HandlerContext) => Promise<Response>;
|
|
1158
|
-
/** The context the kernel opens for a call (a handler's, a door's, a screen's). Kernel-internal: packs are given it. */
|
|
1159
1217
|
export declare function contextFor(m: DerivedManifest, call: DerivedCall, scope: CoreScope): Promise<SemanticsContext>;
|
|
1160
1218
|
/** A pack's semantics handlers as the dispatch's handlers. */
|
|
1161
1219
|
export declare function bindSemantics(m: DerivedManifest, handlers: Record<string, Semantics>, scope?: CoreScope): Record<string, DerivedHandler>;
|
|
1220
|
+
/** The first of a request's form parameters whose value is not one its spec lists (DerivedOperation.enums), with the
|
|
1221
|
+
* bracket name a form gives it (`spending_controls[spending_limits][0][interval]`) and the values it takes. */
|
|
1222
|
+
export declare function valueOutside(params: Record<string, unknown>, enums: Readonly<Record<string, ReadonlyArray<string>>>): {
|
|
1223
|
+
param: string;
|
|
1224
|
+
values: ReadonlyArray<string>;
|
|
1225
|
+
} | undefined;
|
|
1226
|
+
/** Whether a request's parameters ask one of the operation's undemanded variants (DerivedManifest.variants). */
|
|
1227
|
+
export declare function variantAsked(params: Record<string, unknown>, variants: readonly string[], headers?: Headers): boolean;
|
|
1162
1228
|
/** The derived core as the dispatch's core: it owns every operation on a resource the manifest declares. */
|
|
1163
1229
|
export declare function coreFor(m: DerivedManifest, scope?: CoreScope): {
|
|
1164
1230
|
owns: (o: DerivedOperation) => boolean;
|
package/dist/src/derived-core.js
CHANGED
|
@@ -22,7 +22,7 @@ import { MACHINE_POOL, poolFor, guardedMachinePool } from "./machines.js";
|
|
|
22
22
|
import { askApplication, deliverEvents } from "./events.js";
|
|
23
23
|
import { handlerCrypto, hmac, ksuidFrom, lettersFrom, sha256, uuidFrom } from "./signing.js";
|
|
24
24
|
import { hashFieldValue } from "./hash.js";
|
|
25
|
-
import { readResourceBlob, removeResourceBlob, writeResourceBlob } from "./resource-blob.js";
|
|
25
|
+
import { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobSize, writeResourceBlob, writeResourceBlobStream } from "./resource-blob.js";
|
|
26
26
|
import { managedDatabase, unboundDatabase } from "./managed-database.js";
|
|
27
27
|
import { redisServiceTimes } from "./redis/service-times.js";
|
|
28
28
|
import { execRedisRun } from "./redis/engine.js";
|
|
@@ -73,12 +73,13 @@ export function strictFields(m, operation, body) {
|
|
|
73
73
|
if (!field)
|
|
74
74
|
return refuse(strict.unknown, { name });
|
|
75
75
|
const { type } = field;
|
|
76
|
-
const fits = value === null
|
|
76
|
+
const fits = (value === null && !(type === 'array' && strict.arrays?.[name]))
|
|
77
77
|
|| (type === 'boolean' && typeof value === 'boolean')
|
|
78
78
|
|| (type === 'integer' && isExactIntegerValue(value))
|
|
79
79
|
|| (type === 'number' && (typeof value === 'number' || isExactIntegerValue(value)))
|
|
80
80
|
|| (type === 'string' && typeof value === 'string')
|
|
81
|
-
||
|
|
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);
|
|
82
83
|
if (!fits)
|
|
83
84
|
return refuse(strict.type, { name, type: type === 'integer' ? 'an integer' : `a ${type}` });
|
|
84
85
|
if ((typeof value === 'number' || isExactIntegerValue(value)) && ((field.minimum !== undefined && compareExactNumbers(value, field.minimum) < 0) || (field.maximum !== undefined && compareExactNumbers(value, field.maximum) > 0))) {
|
|
@@ -303,13 +304,20 @@ function parentField(row, path) {
|
|
|
303
304
|
value = value && typeof value === 'object' ? value[key] : undefined;
|
|
304
305
|
return value;
|
|
305
306
|
}
|
|
306
|
-
function
|
|
307
|
-
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);
|
|
308
315
|
if (!parent)
|
|
309
316
|
return undefined;
|
|
310
317
|
const id = parentValue(parent, call.params);
|
|
311
318
|
const where = Object.entries(parent.where ?? {}).map(([field, template]) => [field, String(fill(template, call.params))]);
|
|
312
|
-
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);
|
|
313
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));
|
|
314
322
|
}
|
|
315
323
|
/** The vendor's view of a stored subject: its own fields, bookkeeping (`_`) left out. */
|
|
@@ -692,7 +700,11 @@ function recordedInput(m, call, merged) {
|
|
|
692
700
|
const id = call.operation.id;
|
|
693
701
|
const sent = sentOf.get(call);
|
|
694
702
|
const activity = call.request.headers.get('x-volter-world-activity') === 'provisioning' ? { activity: 'provisioning' } : {};
|
|
695
|
-
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 };
|
|
696
708
|
if (id === 'graphql') {
|
|
697
709
|
const b = (sent?.body ?? {});
|
|
698
710
|
return { ...activity, graphql: { query: b.query, variables: b.variables ?? {}, ...(b.operationName ? { operationName: b.operationName } : {}) }, ...(sent?.lane ? { lane: sent.lane } : {}), headers: sent?.headers ?? {} };
|
|
@@ -711,28 +723,27 @@ const scopeOfCall = new WeakMap();
|
|
|
711
723
|
const requestOfCall = new WeakMap();
|
|
712
724
|
async function writeDetailed(m, call, resource, id, fields, operation, params, root, occurredAt, checks) {
|
|
713
725
|
const before = stateBeforeWrite(m, resource, id, root);
|
|
726
|
+
const prior = counted(m, resource) ? stored(m, resource, root).find((r) => r.id === id) : undefined;
|
|
714
727
|
const withFiles = async (input) => { const files = input.operationId ? await keptFiles(m, call, root) : undefined; return files ? { ...input, files } : input; };
|
|
715
728
|
const { resource: row, result } = await applyTwinWrite(m.service,
|
|
716
|
-
// who made it: the login the pack's identity reads off the request, when it declares one, so a subject's
|
|
717
|
-
// history can say who did each thing
|
|
718
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
|
|
719
730
|
inVendorMove()
|
|
720
731
|
? { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: { clock: true }, occurredAt, actor: { kind: 'system' } }
|
|
721
|
-
: { 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);
|
|
722
733
|
observeStateWrite(m, result, before, root, checks);
|
|
723
|
-
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 });
|
|
724
735
|
}
|
|
725
|
-
async function atomicWrite(m, call, resource, decide, operation, params, root, occurredAt, unchangedId, checks) {
|
|
726
|
-
const input = recordedInput(m, call, params);
|
|
727
|
-
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;
|
|
728
739
|
let before;
|
|
729
740
|
const committed = await applyTwinWriteAtomic(m.service, (snapshot) => {
|
|
730
741
|
const selected = decide(snapshot);
|
|
731
|
-
if (selected && transitionObserver)
|
|
742
|
+
if (selected && (transitionObserver || counted(m, resource)))
|
|
732
743
|
before = snapshot.find((r) => r.type === storedType(m, resource) && r.id === selected.id);
|
|
733
744
|
return selected ? { kind: 'write', value: selected.id, write: {
|
|
734
745
|
operation, subjectType: storedType(m, resource), subjectId: selected.id, fields: selected.fields,
|
|
735
|
-
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) } : {}) },
|
|
736
747
|
} } : { kind: 'skip', value: undefined };
|
|
737
748
|
}, root);
|
|
738
749
|
if (!committed.result || !committed.value) {
|
|
@@ -741,10 +752,10 @@ async function atomicWrite(m, call, resource, decide, operation, params, root, o
|
|
|
741
752
|
}
|
|
742
753
|
observeStateWrite(m, committed.result, before, root, checks);
|
|
743
754
|
const row = resourcesOfType(m.service, storedType(m, resource), root).find((r) => r.id === resolveSubjectId(m.service, storedType(m, resource), committed.value, root));
|
|
744
|
-
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;
|
|
745
756
|
}
|
|
746
|
-
async function finishWrite(m, call, resource, row, result, operation, params, root, occurredAt) {
|
|
747
|
-
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);
|
|
748
759
|
const body = view(m, resource, row);
|
|
749
760
|
// the write's own context for its hooks, in the scope the call was served in (a walk's application answers, a tenant,
|
|
750
761
|
// the managed database), at the write's moment
|
|
@@ -766,27 +777,30 @@ async function finishWrite(m, call, resource, row, result, operation, params, ro
|
|
|
766
777
|
}
|
|
767
778
|
/** The declared counts a written subject is a child in (a resource's `counts`), each kept on its parent: written when
|
|
768
779
|
* the count moved, through the write path, so the parent's history says when. */
|
|
769
|
-
async function keepCounts(m, call, resource, row, params, root, occurredAt) {
|
|
780
|
+
async function keepCounts(m, call, resource, row, params, root, occurredAt, prior) {
|
|
770
781
|
for (const [parent, decl] of Object.entries(m.resources)) {
|
|
771
782
|
for (const [field, c] of Object.entries(decl.counts ?? {})) {
|
|
772
783
|
if (storedType(m, c.of) !== storedType(m, resource))
|
|
773
784
|
continue;
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
+
}
|
|
787
798
|
}
|
|
788
799
|
}
|
|
789
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)));
|
|
790
804
|
/** An answer in the vendor's success envelope, under the operation's key, when the vendor has one. */
|
|
791
805
|
function envelope(m, op, body) {
|
|
792
806
|
return m.success && op.answers?.key ? { ...m.success, [op.answers.key]: body } : body;
|
|
@@ -1133,16 +1147,16 @@ export async function serveCore(m, call, scope = {}) {
|
|
|
1133
1147
|
// the body is read from a copy, so the call's own request is still unread for the write hook's context
|
|
1134
1148
|
const params = await boundaryParams(m, { ...call, request: call.request.clone() }, root);
|
|
1135
1149
|
const idParam = subjectOf(m, resource, call);
|
|
1136
|
-
const
|
|
1137
|
-
if (orphan)
|
|
1138
|
-
return { served: orphan };
|
|
1139
|
-
const parent = decl.parent;
|
|
1150
|
+
const parent = parentOfCall(decl, call);
|
|
1140
1151
|
const parentId = parent ? parentValue(parent, call.params) : undefined;
|
|
1141
1152
|
// the caller's tenant, when the vendor has tenancy and the resource is not shared by every tenant
|
|
1142
1153
|
const tenantField = m.tenant && !decl.shared ? m.tenant.field : undefined;
|
|
1143
1154
|
const tenant = tenantField && scope.tenant ? await scope.tenant(await contextFor(m, { ...call, request: call.request.clone() }, scope)) : undefined;
|
|
1144
1155
|
if (tenantField && tenant === undefined)
|
|
1145
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 };
|
|
1146
1160
|
const underParent = (r) => (!parent || r[parent.field] === parentId)
|
|
1147
1161
|
&& (!tenantField || r[tenantField] === tenant);
|
|
1148
1162
|
const paths = expandPaths(m, params);
|
|
@@ -1226,6 +1240,10 @@ export async function serveCore(m, call, scope = {}) {
|
|
|
1226
1240
|
const requested = op.class === 'update' ? data[field] : undefined;
|
|
1227
1241
|
if (op.class === 'update' && requested === undefined)
|
|
1228
1242
|
continue;
|
|
1243
|
+
if (op.class === 'update' && sdecl.unchanged === 'allow' && stateOf(sdecl, current[field]) === stateOf(sdecl, requested)) {
|
|
1244
|
+
changes[field] = requested;
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1229
1247
|
const { move, refusal } = transitionFor(field, sdecl, op.id, current[field], requested, hit.id);
|
|
1230
1248
|
if (refusal)
|
|
1231
1249
|
return { served: vendorError(m, refusal) };
|
|
@@ -1286,6 +1304,12 @@ export function sse(m, events, operationId) {
|
|
|
1286
1304
|
return new Response(body, { status: 200, headers: { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache' } });
|
|
1287
1305
|
}
|
|
1288
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
|
+
};
|
|
1289
1313
|
export async function contextFor(m, call, scope) {
|
|
1290
1314
|
const root = scope.root;
|
|
1291
1315
|
const asAsked = call;
|
|
@@ -1295,8 +1319,10 @@ export async function contextFor(m, call, scope) {
|
|
|
1295
1319
|
// the guard's parse when it made one (taken once), else this context's own
|
|
1296
1320
|
// a body that is its payload's bytes (S3's PutObject) is never read as JSON, whatever its label
|
|
1297
1321
|
const payload = bytesPayload(call.operation);
|
|
1298
|
-
|
|
1299
|
-
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(() => '');
|
|
1300
1326
|
let body = parsed?.value;
|
|
1301
1327
|
if (!parsed) {
|
|
1302
1328
|
if (payload)
|
|
@@ -1359,7 +1385,6 @@ export async function contextFor(m, call, scope) {
|
|
|
1359
1385
|
return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/repos/${name}`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
|
|
1360
1386
|
},
|
|
1361
1387
|
mail: (route, mail, headers) => sendMail(route, mail, headers),
|
|
1362
|
-
actor: m.identity ? m.identity(call.request.headers.get('authorization'), root) : undefined,
|
|
1363
1388
|
now: () => now(m, at),
|
|
1364
1389
|
render: (row, resource) => {
|
|
1365
1390
|
const raw = rawResources.has(row);
|
|
@@ -1400,11 +1425,17 @@ export async function contextFor(m, call, scope) {
|
|
|
1400
1425
|
record: async (type, fields, id, operation) => {
|
|
1401
1426
|
if (!type.startsWith('_'))
|
|
1402
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);
|
|
1403
1431
|
if (id !== undefined) {
|
|
1404
1432
|
const before = operation ? stateBeforeWrite(m, type, id, root) : undefined;
|
|
1405
|
-
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);
|
|
1406
1435
|
if (operation)
|
|
1407
1436
|
observeStateWrite(m, result, before, root, stateChecks);
|
|
1437
|
+
if (keeps)
|
|
1438
|
+
await keepCounts(m, call, type, row, params, root, at, prior && { ...prior });
|
|
1408
1439
|
return id;
|
|
1409
1440
|
}
|
|
1410
1441
|
const { value, result } = await applyTwinWriteAtomic(m.service, (resources) => {
|
|
@@ -1425,6 +1456,9 @@ export async function contextFor(m, call, scope) {
|
|
|
1425
1456
|
}, root, () => resourcesOfType(m.service, type, root));
|
|
1426
1457
|
if (operation && result)
|
|
1427
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);
|
|
1428
1462
|
return value;
|
|
1429
1463
|
},
|
|
1430
1464
|
get machines() {
|
|
@@ -1437,7 +1471,9 @@ export async function contextFor(m, call, scope) {
|
|
|
1437
1471
|
const id = mintId(m, resource, root, at, snapshot);
|
|
1438
1472
|
return { id, fields: typeof fields === 'function' ? fields(id) : fields };
|
|
1439
1473
|
}, operation, params, root, at, undefined, stateChecks).then((row) => row),
|
|
1440
|
-
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');
|
|
1441
1477
|
const rules = m.resources[resource]?.unique;
|
|
1442
1478
|
if (!rules?.length)
|
|
1443
1479
|
throw new Error('createUnique requires declared unique keys');
|
|
@@ -1451,16 +1487,20 @@ export async function contextFor(m, call, scope) {
|
|
|
1451
1487
|
return undefined;
|
|
1452
1488
|
}
|
|
1453
1489
|
return { id, fields: selected };
|
|
1454
|
-
}, operation, params, root, at, undefined, stateChecks);
|
|
1490
|
+
}, operation, params, root, at, undefined, stateChecks, options?.input === 'omit');
|
|
1455
1491
|
return existing ? { created: false, row: existing } : { created: true, row: row };
|
|
1456
1492
|
},
|
|
1457
|
-
change: (resource, id, decide, operation
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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
|
+
},
|
|
1464
1504
|
write: (resource, id, fields, operation) => write(m, call, resource, id, fields, operation, params, root, at, stateChecks),
|
|
1465
1505
|
accumulate: async (resource, id, deltas, operation) => {
|
|
1466
1506
|
if (!resource.startsWith('_') || !m.resources[resource])
|
|
@@ -1546,8 +1586,11 @@ export async function contextFor(m, call, scope) {
|
|
|
1546
1586
|
live: liveAccess({ rows: (type) => resourcesOfType(m.service, type, root), blob: (key) => readResourceBlob(m.service, key, root) }, call.request),
|
|
1547
1587
|
blobs: {
|
|
1548
1588
|
put: (key, bytes) => writeResourceBlob(m.service, key, bytes, root),
|
|
1589
|
+
putStream: (key, body, accept) => writeResourceBlobStream(m.service, key, body, accept, root),
|
|
1549
1590
|
get: (key) => readResourceBlob(m.service, key, root),
|
|
1550
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),
|
|
1551
1594
|
},
|
|
1552
1595
|
merge: (resource, prior, next) => mergeField(m.resources[resource]?.update, prior, next),
|
|
1553
1596
|
conflict: (resource, fields, except) => {
|
|
@@ -1702,7 +1745,105 @@ async function writeGroup(m, call, writes, params, root, occurredAt) {
|
|
|
1702
1745
|
/** A pack's semantics handlers as the dispatch's handlers. */
|
|
1703
1746
|
export function bindSemantics(m, handlers, scope = {}) {
|
|
1704
1747
|
prepareViewOmissions(m);
|
|
1705
|
-
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) });
|
|
1706
1847
|
}
|
|
1707
1848
|
/** The derived core as the dispatch's core: it owns every operation on a resource the manifest declares. */
|
|
1708
1849
|
export function coreFor(m, scope = {}) {
|