@atscript/moost-db 0.1.150 → 0.1.152
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/dist/index.cjs +549 -106
- package/dist/index.d.cts +111 -1
- package/dist/index.d.mts +111 -1
- package/dist/index.mjs +567 -125
- package/package.json +20 -20
package/dist/index.cjs
CHANGED
|
@@ -594,30 +594,83 @@ function identityOf(row, fields, read) {
|
|
|
594
594
|
return id;
|
|
595
595
|
}
|
|
596
596
|
/**
|
|
597
|
+
* Sorted field lists per raw key order — every id of one shape sorts its keys
|
|
598
|
+
* once per call, not once per id.
|
|
599
|
+
*/
|
|
600
|
+
var IdShapes = class {
|
|
601
|
+
/** Raw key order (`Object.keys` joined) → the sorted fields. */
|
|
602
|
+
byRaw = /* @__PURE__ */ new Map();
|
|
603
|
+
/** Sorted signature → the sorted fields (one entry per distinct shape). */
|
|
604
|
+
bySorted = /* @__PURE__ */ new Map();
|
|
605
|
+
fieldsOf(id) {
|
|
606
|
+
const keys = Object.keys(id);
|
|
607
|
+
const raw = `${keys.length}:${keys.join("")}`;
|
|
608
|
+
let sorted = this.byRaw.get(raw);
|
|
609
|
+
if (sorted === void 0) {
|
|
610
|
+
const own = keys.length > 1 ? keys.toSorted() : keys;
|
|
611
|
+
const sig = `${own.length}:${own.join("")}`;
|
|
612
|
+
sorted = this.bySorted.get(sig);
|
|
613
|
+
if (sorted === void 0) this.bySorted.set(sig, sorted = own);
|
|
614
|
+
this.byRaw.set(raw, sorted);
|
|
615
|
+
}
|
|
616
|
+
return sorted;
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
/** A value `$in` matches exactly like the equality `{ field: value }` on every adapter. */
|
|
620
|
+
function isInSafe(value) {
|
|
621
|
+
const type = typeof value;
|
|
622
|
+
return type === "string" || type === "bigint" || type === "boolean" || type === "number" && Number.isFinite(value);
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* The filter matching any of `ids` (deduped): `{ field: { $in } }` when they
|
|
626
|
+
* share one single-field shape with scalar values (one equality for a single
|
|
627
|
+
* id), else `{ $or: ids }`.
|
|
628
|
+
*/
|
|
629
|
+
function idsFilter(ids, shapes) {
|
|
630
|
+
if (ids.length > 0 && shapes.bySorted.size === 1) {
|
|
631
|
+
const [fields] = shapes.bySorted.values();
|
|
632
|
+
if (fields.length === 1) {
|
|
633
|
+
const field = fields[0];
|
|
634
|
+
const values = [];
|
|
635
|
+
for (const id of ids) {
|
|
636
|
+
const value = id[field];
|
|
637
|
+
if (!isInSafe(value)) return { $or: ids };
|
|
638
|
+
values.push(value);
|
|
639
|
+
}
|
|
640
|
+
return values.length === 1 ? { [field]: values[0] } : { [field]: { $in: values } };
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return { $or: ids };
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
597
646
|
* The rows `ids` address that also match `scope` (none = every row),
|
|
598
647
|
* aligned with `ids` — `undefined` where nothing matched. One `findMany`
|
|
599
|
-
* over the deduped ids (`{
|
|
600
|
-
* every id field; ids of
|
|
601
|
-
* The action row loader and
|
|
648
|
+
* over the deduped ids (`{ field: { $in } }` for single-field ids, else
|
|
649
|
+
* `{ $or: ids }`; AND `scope`) selecting `select` plus every id field; ids of
|
|
650
|
+
* different identification shapes may be mixed. The action row loader and
|
|
651
|
+
* `$actions` scope check share it.
|
|
602
652
|
*/
|
|
603
653
|
async function findRowsByIds(source, ids, scope, select) {
|
|
604
654
|
if (ids.length === 0) return [];
|
|
605
655
|
const fields = new Set(select);
|
|
606
656
|
const dedupedIds = [];
|
|
607
657
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
658
|
+
const shapes = new IdShapes();
|
|
659
|
+
const idKeys = [];
|
|
608
660
|
for (const id of ids) {
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
const key = idKey(id, sortedFields);
|
|
661
|
+
const key = idKey(id, shapes.fieldsOf(id));
|
|
662
|
+
idKeys.push(key);
|
|
612
663
|
if (key !== void 0 && !seenKeys.has(key)) {
|
|
613
664
|
seenKeys.add(key);
|
|
614
665
|
dedupedIds.push(id);
|
|
615
666
|
}
|
|
616
667
|
}
|
|
617
|
-
|
|
618
|
-
|
|
668
|
+
for (const sortedFields of shapes.bySorted.values()) for (const f of sortedFields) fields.add(f);
|
|
669
|
+
const match = idsFilter(dedupedIds, shapes);
|
|
670
|
+
return alignByKeys(await source.findMany({
|
|
671
|
+
filter: scope ? { $and: [match, scope] } : match,
|
|
619
672
|
controls: { $select: [...fields] }
|
|
620
|
-
}),
|
|
673
|
+
}), idKeys, shapes);
|
|
621
674
|
}
|
|
622
675
|
/**
|
|
623
676
|
* `rows` aligned with `ids` — per id, the first row with its key values
|
|
@@ -625,16 +678,12 @@ async function findRowsByIds(source, ids, scope, select) {
|
|
|
625
678
|
* identification shapes may be mixed.
|
|
626
679
|
*/
|
|
627
680
|
function alignRowsToIds(rows, ids) {
|
|
628
|
-
const shapes =
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const sig = sortedFields.join("");
|
|
633
|
-
if (!shapes.has(sig)) shapes.set(sig, sortedFields);
|
|
634
|
-
idKeys.push(idKey(id, sortedFields));
|
|
635
|
-
}
|
|
681
|
+
const shapes = new IdShapes();
|
|
682
|
+
return alignByKeys(rows, ids.map((id) => idKey(id, shapes.fieldsOf(id))), shapes);
|
|
683
|
+
}
|
|
684
|
+
function alignByKeys(rows, idKeys, shapes) {
|
|
636
685
|
const rowByKey = /* @__PURE__ */ new Map();
|
|
637
|
-
for (const row of rows) for (const sortedFields of shapes.values()) {
|
|
686
|
+
for (const row of rows) for (const sortedFields of shapes.bySorted.values()) {
|
|
638
687
|
const key = idKey(row, sortedFields);
|
|
639
688
|
if (key !== void 0 && !rowByKey.has(key)) rowByKey.set(key, row);
|
|
640
689
|
}
|
|
@@ -661,11 +710,43 @@ function exceedsFields(row, fields) {
|
|
|
661
710
|
for (const key in row) if (!fields.has(key)) return true;
|
|
662
711
|
return false;
|
|
663
712
|
}
|
|
713
|
+
/**
|
|
714
|
+
* `row` without its own `keys` — a NEW object (same key order) when it has
|
|
715
|
+
* any of them, else `row` itself. Rows are rebuilt rather than `delete`d
|
|
716
|
+
* from: a deleted key turns an object into a slow dictionary-mode one for
|
|
717
|
+
* everything after (decoration, serialization).
|
|
718
|
+
*/
|
|
719
|
+
function omitKeys(row, keys) {
|
|
720
|
+
let has = false;
|
|
721
|
+
for (const key of keys) if (Object.hasOwn(row, key)) {
|
|
722
|
+
has = true;
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
if (!has) return row;
|
|
726
|
+
const out = {};
|
|
727
|
+
for (const key of Object.keys(row)) {
|
|
728
|
+
if (keys.has(key)) continue;
|
|
729
|
+
if (key === "__proto__") Object.defineProperty(out, key, {
|
|
730
|
+
value: row[key],
|
|
731
|
+
enumerable: true,
|
|
732
|
+
writable: true,
|
|
733
|
+
configurable: true
|
|
734
|
+
});
|
|
735
|
+
else out[key] = row[key];
|
|
736
|
+
}
|
|
737
|
+
return out;
|
|
738
|
+
}
|
|
739
|
+
/** `fields` split at the dots — pass to {@link projectRow} when projecting many rows. */
|
|
740
|
+
function splitPaths(fields) {
|
|
741
|
+
const out = [];
|
|
742
|
+
for (const path of fields) out.push(path.split("."));
|
|
743
|
+
return out;
|
|
744
|
+
}
|
|
664
745
|
/** `row` narrowed to `fields` (dot paths copied into fresh nested objects). */
|
|
665
746
|
function projectRow$2(row, fields) {
|
|
666
747
|
const out = {};
|
|
667
748
|
for (const path of fields) {
|
|
668
|
-
const parts = path.split(".");
|
|
749
|
+
const parts = typeof path === "string" ? path.split(".") : path;
|
|
669
750
|
let src = row;
|
|
670
751
|
for (const p of parts) src = src?.[p];
|
|
671
752
|
if (src === void 0) continue;
|
|
@@ -1845,13 +1926,13 @@ function copyClientFilter(filter, freeze = false) {
|
|
|
1845
1926
|
} catch {
|
|
1846
1927
|
throw badRequest("", "Malformed query: the filter cannot be copied");
|
|
1847
1928
|
}
|
|
1848
|
-
if (freeze) deepFreeze(copy);
|
|
1929
|
+
if (freeze) deepFreeze$1(copy);
|
|
1849
1930
|
return copy;
|
|
1850
1931
|
}
|
|
1851
|
-
function deepFreeze(v) {
|
|
1932
|
+
function deepFreeze$1(v) {
|
|
1852
1933
|
if (v === null || typeof v !== "object" || Object.isFrozen(v)) return;
|
|
1853
1934
|
Object.freeze(v);
|
|
1854
|
-
for (const child of Object.values(v)) deepFreeze(child);
|
|
1935
|
+
for (const child of Object.values(v)) deepFreeze$1(child);
|
|
1855
1936
|
}
|
|
1856
1937
|
/** The predicate operator maps of a filter's own level (through `$and` / `$or` / `$not`). */
|
|
1857
1938
|
function collectPredicateMaps(filter, out) {
|
|
@@ -2108,6 +2189,129 @@ function walk(node, def, shallow) {
|
|
|
2108
2189
|
}
|
|
2109
2190
|
}
|
|
2110
2191
|
//#endregion
|
|
2192
|
+
//#region src/meta/meta-cache.ts
|
|
2193
|
+
/**
|
|
2194
|
+
* `/meta` identity memo (since 0.1.151). Each layer that turns the cached
|
|
2195
|
+
* envelope into a request's `/meta` object memoizes its output per INPUT
|
|
2196
|
+
* identity and per the request-dependent answers it reads (the key), so
|
|
2197
|
+
* equal inputs give the same object across requests. The final object is
|
|
2198
|
+
* then registered with wooks' `prerenderJson` (JSON serialized once, weak
|
|
2199
|
+
* ETag from the bytes).
|
|
2200
|
+
*
|
|
2201
|
+
* Soundness: a memoized object is never mutated (the `applyMetaOverlay`
|
|
2202
|
+
* contract), so its identity implies its bytes; every key covers every
|
|
2203
|
+
* request-dependent input its layer reads. Inputs that are not themselves
|
|
2204
|
+
* stable (an overlay that builds a fresh object per request) are not memoized —
|
|
2205
|
+
* their output is computed per request, as before, and gets no ETag.
|
|
2206
|
+
*/
|
|
2207
|
+
/** Objects whose identity implies their bytes: cached envelopes and memo outputs derived from one. */
|
|
2208
|
+
const stable = /* @__PURE__ */ new WeakSet();
|
|
2209
|
+
/**
|
|
2210
|
+
* Most memoized variants derived from one root envelope, over all chained
|
|
2211
|
+
* layers (planner → index visibility → delegated actions → own path); past it
|
|
2212
|
+
* a variant is computed per request (and is not stable).
|
|
2213
|
+
*/
|
|
2214
|
+
const MAX_VARIANTS_PER_ROOT = 128;
|
|
2215
|
+
/** The root envelope each memo output was derived from (for the budget). */
|
|
2216
|
+
const rootOf = /* @__PURE__ */ new WeakMap();
|
|
2217
|
+
/** Variants memoized so far per root envelope. */
|
|
2218
|
+
const rootBudget = /* @__PURE__ */ new WeakMap();
|
|
2219
|
+
/** Marks `obj` as identity-stable (never mutated after this point). Returns it. */
|
|
2220
|
+
function markStableMeta(obj) {
|
|
2221
|
+
stable.add(obj);
|
|
2222
|
+
return obj;
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* Marks a `/meta` payload your override memoizes (an `applyMetaOverlay`
|
|
2226
|
+
* variant, a `metaForm` projection) as reusable: it is deep-frozen and, when
|
|
2227
|
+
* served again, sent from its serialized-once JSON with an `ETag`. Return the
|
|
2228
|
+
* same object for the same inputs only — its bytes are fixed from now on.
|
|
2229
|
+
*
|
|
2230
|
+
* @since 0.1.151
|
|
2231
|
+
*/
|
|
2232
|
+
function stableMeta(obj) {
|
|
2233
|
+
deepFreeze(obj);
|
|
2234
|
+
return markStableMeta(obj);
|
|
2235
|
+
}
|
|
2236
|
+
/**
|
|
2237
|
+
* `obj` may be memoized on and registered for prerendering: a cached envelope,
|
|
2238
|
+
* a memo output, or an object passed to {@link stableMeta}. (Frozenness alone
|
|
2239
|
+
* is not enough: `Object.freeze` is shallow.)
|
|
2240
|
+
*/
|
|
2241
|
+
function isStableMeta(obj) {
|
|
2242
|
+
return stable.has(obj);
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* The memoized variant of `input` for `key` in `variants` — `compute()` once
|
|
2246
|
+
* per (input, key). Keys of different layers must not collide (prefix them).
|
|
2247
|
+
* An unstable input is not memoized (computed every time); past
|
|
2248
|
+
* {@link MAX_VARIANTS_PER_ROOT} variants of one envelope a variant is computed
|
|
2249
|
+
* per request (and is not stable).
|
|
2250
|
+
*/
|
|
2251
|
+
function metaVariant(variants, input, key, compute) {
|
|
2252
|
+
if (!isStableMeta(input)) return compute();
|
|
2253
|
+
let byKey = variants.get(input);
|
|
2254
|
+
const hit = byKey?.get(key);
|
|
2255
|
+
if (hit) return hit;
|
|
2256
|
+
const out = compute();
|
|
2257
|
+
const root = rootOf.get(input) ?? input;
|
|
2258
|
+
const used = rootBudget.get(root) ?? 0;
|
|
2259
|
+
if (used < MAX_VARIANTS_PER_ROOT) {
|
|
2260
|
+
rootBudget.set(root, used + 1);
|
|
2261
|
+
if (out !== input) {
|
|
2262
|
+
markStableMeta(out);
|
|
2263
|
+
rootOf.set(out, root);
|
|
2264
|
+
}
|
|
2265
|
+
if (!byKey) variants.set(input, byKey = /* @__PURE__ */ new Map());
|
|
2266
|
+
byKey.set(key, out);
|
|
2267
|
+
}
|
|
2268
|
+
return out;
|
|
2269
|
+
}
|
|
2270
|
+
/** Stable small ids for objects (memo keys over object identities). */
|
|
2271
|
+
const ids = /* @__PURE__ */ new WeakMap();
|
|
2272
|
+
let nextId = 1;
|
|
2273
|
+
/** A per-process id of `obj`'s identity. */
|
|
2274
|
+
function identityId(obj) {
|
|
2275
|
+
let id = ids.get(obj);
|
|
2276
|
+
if (id === void 0) ids.set(obj, id = nextId++);
|
|
2277
|
+
return id;
|
|
2278
|
+
}
|
|
2279
|
+
/** Bit string of `flags` (memo key). */
|
|
2280
|
+
function bits(flags) {
|
|
2281
|
+
let out = "";
|
|
2282
|
+
for (const flag of flags) out += flag ? "1" : "0";
|
|
2283
|
+
return out;
|
|
2284
|
+
}
|
|
2285
|
+
let freezeServed;
|
|
2286
|
+
const deepFrozen = /* @__PURE__ */ new WeakSet();
|
|
2287
|
+
/**
|
|
2288
|
+
* Shared objects are deep-frozen when `NODE_ENV` is `test` or
|
|
2289
|
+
* `development`, or `ATSCRIPT_DB_FREEZE_META=1` — an in-place mutation (which
|
|
2290
|
+
* would keep serving the old bytes) then throws instead.
|
|
2291
|
+
*/
|
|
2292
|
+
function shouldFreeze() {
|
|
2293
|
+
if (freezeServed === void 0) {
|
|
2294
|
+
const env = typeof process === "undefined" ? void 0 : process.env;
|
|
2295
|
+
freezeServed = env?.NODE_ENV === "test" || env?.NODE_ENV === "development" || env?.ATSCRIPT_DB_FREEZE_META === "1";
|
|
2296
|
+
}
|
|
2297
|
+
return freezeServed;
|
|
2298
|
+
}
|
|
2299
|
+
/**
|
|
2300
|
+
* Deep-freezes `obj` in dev / test (see {@link shouldFreeze}); a no-op
|
|
2301
|
+
* otherwise. For objects shared across requests (served `/meta` payloads,
|
|
2302
|
+
* memoized projections), which must never be mutated.
|
|
2303
|
+
*/
|
|
2304
|
+
function freezeShared(obj) {
|
|
2305
|
+
if (!shouldFreeze()) return;
|
|
2306
|
+
deepFreeze(obj);
|
|
2307
|
+
}
|
|
2308
|
+
function deepFreeze(value) {
|
|
2309
|
+
if (value === null || typeof value !== "object" || deepFrozen.has(value)) return;
|
|
2310
|
+
deepFrozen.add(value);
|
|
2311
|
+
Object.freeze(value);
|
|
2312
|
+
for (const key of Object.keys(value)) deepFreeze(value[key]);
|
|
2313
|
+
}
|
|
2314
|
+
//#endregion
|
|
2111
2315
|
//#region src/http-path-design-time.ts
|
|
2112
2316
|
const KEY$1 = "db.http.path";
|
|
2113
2317
|
/** The `@db.http.path` value a model carried before moost-db first wrote its published path. */
|
|
@@ -2516,7 +2720,11 @@ function __decorateParam(paramIndex, decorator) {
|
|
|
2516
2720
|
}
|
|
2517
2721
|
//#endregion
|
|
2518
2722
|
//#region src/as-readable.controller.ts
|
|
2519
|
-
|
|
2723
|
+
var _AsReadableController;
|
|
2724
|
+
const DEFAULT_META_CACHE_CONTROL = "private, no-cache";
|
|
2725
|
+
const DEFAULT_META_VARY = ["Authorization", "Cookie"];
|
|
2726
|
+
const readHttpResponse = () => (0, moost.current)().get(_moostjs_event_http.httpKind.keys.response);
|
|
2727
|
+
let AsReadableController = _AsReadableController = class AsReadableController {
|
|
2520
2728
|
/** The Atscript interface this controller serves. */
|
|
2521
2729
|
boundType;
|
|
2522
2730
|
/** Short human-readable name for logging (usually the table/source name). */
|
|
@@ -2538,6 +2746,12 @@ let AsReadableController = class AsReadableController {
|
|
|
2538
2746
|
/** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
|
|
2539
2747
|
_formSchemas = /* @__PURE__ */ new Map();
|
|
2540
2748
|
/**
|
|
2749
|
+
* @internal Memoized `/meta` variants of this controller (own mount path,
|
|
2750
|
+
* index visibility, decorations, delegated actions) — per input object, then
|
|
2751
|
+
* per layer-prefixed key; see `meta/meta-cache.ts`.
|
|
2752
|
+
*/
|
|
2753
|
+
_metaVariants = /* @__PURE__ */ new WeakMap();
|
|
2754
|
+
/**
|
|
2541
2755
|
* @param opts.canonical Multi-mount models only: marks (`true`) or excludes
|
|
2542
2756
|
* (`false`) this controller as the model's published value-help route
|
|
2543
2757
|
* (since 0.1.150). Decorator-bound controllers use the decorator option.
|
|
@@ -2822,10 +3036,56 @@ let AsReadableController = class AsReadableController {
|
|
|
2822
3036
|
* static envelope is cached (rebuilt when {@link metaCacheKey} changes);
|
|
2823
3037
|
* {@link applyMetaOverlay} runs per request so subclasses can prune the
|
|
2824
3038
|
* response by principal.
|
|
3039
|
+
*
|
|
3040
|
+
* Since 0.1.151 the response is HTTP-cacheable ({@link metaHttpCaching}):
|
|
3041
|
+
* `Cache-Control: private, no-cache`, `Vary: Authorization, Cookie` and —
|
|
3042
|
+
* when the payload is the same object as for an earlier request with the
|
|
3043
|
+
* same inputs — a weak `ETag` computed from the serialized bytes, so a
|
|
3044
|
+
* matching `If-None-Match` answers `304 Not Modified`. The JSON is then
|
|
3045
|
+
* serialized once, not per request.
|
|
2825
3046
|
*/
|
|
2826
3047
|
async meta() {
|
|
2827
3048
|
await this.parseRequest("meta");
|
|
2828
|
-
return this._withOwnHttpPath(await this.resolveMeta());
|
|
3049
|
+
return this._serveMeta(this._withOwnHttpPath(await this.resolveMeta()), this.meta === _AsReadableController.prototype.meta);
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* HTTP caching of `GET /meta` and `GET /meta/form/:name` (since 0.1.151).
|
|
3053
|
+
* Default `{}`: `Cache-Control: private, no-cache` (kept when the response
|
|
3054
|
+
* already carries one), `Vary: Authorization, Cookie` (merged into an
|
|
3055
|
+
* existing `Vary`), and a weak `ETag` from the response bytes with `304`
|
|
3056
|
+
* on a matching `If-None-Match`. Return `false` to send none of it (the
|
|
3057
|
+
* pre-0.1.151 response), or override the header values — e.g.
|
|
3058
|
+
* `{ cacheControl: "no-store" }` for deployments where no browser may keep
|
|
3059
|
+
* a copy.
|
|
3060
|
+
*
|
|
3061
|
+
* The `ETag` is a hash of the final bytes, computed after every overlay:
|
|
3062
|
+
* a request whose principal sees a different `/meta` gets a different tag
|
|
3063
|
+
* (a `200`), so a `304` only ever confirms the bytes the client already
|
|
3064
|
+
* holds. A refused request (`prepareRequest` throwing) is answered with its
|
|
3065
|
+
* error, never `304`.
|
|
3066
|
+
*/
|
|
3067
|
+
metaHttpCaching() {
|
|
3068
|
+
return {};
|
|
3069
|
+
}
|
|
3070
|
+
/**
|
|
3071
|
+
* Applies {@link metaHttpCaching} to the current response and — when `body`
|
|
3072
|
+
* is identity-stable (a cached / memoized object) — registers it for
|
|
3073
|
+
* prerendering with an ETag (deep-frozen in dev / test).
|
|
3074
|
+
*/
|
|
3075
|
+
_serveMeta(body, prerender = true) {
|
|
3076
|
+
const caching = this.metaHttpCaching();
|
|
3077
|
+
if (caching === false) return isStableMeta(body) ? { ...body } : body;
|
|
3078
|
+
let response;
|
|
3079
|
+
try {
|
|
3080
|
+
response = readHttpResponse();
|
|
3081
|
+
} catch {}
|
|
3082
|
+
if (!response) return body;
|
|
3083
|
+
applyMetaHeaders(response, caching);
|
|
3084
|
+
if (prerender && isStableMeta(body)) {
|
|
3085
|
+
freezeShared(body);
|
|
3086
|
+
(0, _moostjs_event_http.prerenderJson)(body, { etag: true });
|
|
3087
|
+
}
|
|
3088
|
+
return body;
|
|
2829
3089
|
}
|
|
2830
3090
|
/**
|
|
2831
3091
|
* The root of a controller's own `/meta` carries its own mount as
|
|
@@ -2835,7 +3095,6 @@ let AsReadableController = class AsReadableController {
|
|
|
2835
3095
|
* cached envelope.
|
|
2836
3096
|
*/
|
|
2837
3097
|
_withOwnHttpPath(meta) {
|
|
2838
|
-
const KEY = "db.http.path";
|
|
2839
3098
|
let prefix;
|
|
2840
3099
|
try {
|
|
2841
3100
|
prefix = (0, moost.useControllerContext)().getPrefix();
|
|
@@ -2845,6 +3104,11 @@ let AsReadableController = class AsReadableController {
|
|
|
2845
3104
|
if (prefix === void 0 || prefix === null) return meta;
|
|
2846
3105
|
const own = normalizeHttpPath(prefix);
|
|
2847
3106
|
if (isParametricPath(own)) return meta;
|
|
3107
|
+
return this.getSerializeOptions === _AsReadableController.prototype.getSerializeOptions ? metaVariant(this._metaVariants, meta, `o:${own}`, () => this._ownHttpPathOf(meta, own)) : this._ownHttpPathOf(meta, own);
|
|
3108
|
+
}
|
|
3109
|
+
/** {@link _withOwnHttpPath} for the mount `own`. */
|
|
3110
|
+
_ownHttpPathOf(meta, own) {
|
|
3111
|
+
const KEY = "db.http.path";
|
|
2848
3112
|
const options = this.getSerializeOptions();
|
|
2849
3113
|
if (options.ignoreAnnotations?.includes(KEY)) return meta;
|
|
2850
3114
|
let key = KEY;
|
|
@@ -2889,7 +3153,7 @@ let AsReadableController = class AsReadableController {
|
|
|
2889
3153
|
this.enterScope(scope);
|
|
2890
3154
|
const key = this.metaCacheKey();
|
|
2891
3155
|
if (!this._metaResponse || key !== this._metaResponseKey) {
|
|
2892
|
-
this._metaResponse = this.buildScoped(scope, () => this.buildMetaResponse());
|
|
3156
|
+
this._metaResponse = markStableMeta(this.buildScoped(scope, () => this.buildMetaResponse()));
|
|
2893
3157
|
this._metaResponseKey = key;
|
|
2894
3158
|
}
|
|
2895
3159
|
return this.applyMetaOverlay(this._metaResponse);
|
|
@@ -2922,10 +3186,10 @@ let AsReadableController = class AsReadableController {
|
|
|
2922
3186
|
this.enterScope(scope);
|
|
2923
3187
|
let cached = this._formSchemas.get(name);
|
|
2924
3188
|
if (!cached) {
|
|
2925
|
-
cached = this.buildScoped(scope, () => this.serializeForMeta(formType));
|
|
3189
|
+
cached = markStableMeta(this.buildScoped(scope, () => this.serializeForMeta(formType)));
|
|
2926
3190
|
this._formSchemas.set(name, cached);
|
|
2927
3191
|
}
|
|
2928
|
-
return cached;
|
|
3192
|
+
return this._serveMeta(cached);
|
|
2929
3193
|
}
|
|
2930
3194
|
/**
|
|
2931
3195
|
* Per-request gate for `GET /meta/form/:name`: return `false` to refuse the
|
|
@@ -2982,6 +3246,12 @@ let AsReadableController = class AsReadableController {
|
|
|
2982
3246
|
* composables). The cached envelope MUST NOT be mutated — see
|
|
2983
3247
|
* `docs/http/permissions.md` for the full contract, including the
|
|
2984
3248
|
* "discoverability only" caveat.
|
|
3249
|
+
*
|
|
3250
|
+
* `/meta`'s serialize-once memo and `ETag` (since 0.1.151) need a stable
|
|
3251
|
+
* object: return the envelope itself when nothing changes, or memoize your
|
|
3252
|
+
* outputs and return them through `stableMeta()` (deep-freezes and marks
|
|
3253
|
+
* them) — never mutate one afterwards. A fresh object per request is
|
|
3254
|
+
* correct, just served without an `ETag`.
|
|
2985
3255
|
*/
|
|
2986
3256
|
applyMetaOverlay(meta) {
|
|
2987
3257
|
return meta;
|
|
@@ -3000,13 +3270,36 @@ __decorate([
|
|
|
3000
3270
|
__decorateMetadata("design:paramtypes", [String]),
|
|
3001
3271
|
__decorateMetadata("design:returntype", Promise)
|
|
3002
3272
|
], AsReadableController.prototype, "metaForm", null);
|
|
3003
|
-
AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMetadata("design:paramtypes", [
|
|
3273
|
+
AsReadableController = _AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMetadata("design:paramtypes", [
|
|
3004
3274
|
Object,
|
|
3005
3275
|
String,
|
|
3006
3276
|
typeof moost.Moost === "undefined" ? Object : moost.Moost,
|
|
3007
3277
|
Object,
|
|
3008
3278
|
Object
|
|
3009
3279
|
])], AsReadableController);
|
|
3280
|
+
/** Sets `/meta`'s `Cache-Control` (unless present) and merges its `Vary` tokens. */
|
|
3281
|
+
function applyMetaHeaders(response, caching) {
|
|
3282
|
+
const headers = response.headers();
|
|
3283
|
+
let cacheControlSet = false;
|
|
3284
|
+
let varyKey = "vary";
|
|
3285
|
+
for (const key in headers) {
|
|
3286
|
+
const lower = key.toLowerCase();
|
|
3287
|
+
if (lower === "cache-control") cacheControlSet = true;
|
|
3288
|
+
else if (lower === "vary") varyKey = key;
|
|
3289
|
+
}
|
|
3290
|
+
if (!cacheControlSet) response.setHeader("cache-control", caching.cacheControl ?? DEFAULT_META_CACHE_CONTROL);
|
|
3291
|
+
const wanted = caching.vary ?? DEFAULT_META_VARY;
|
|
3292
|
+
if (wanted.length === 0) return;
|
|
3293
|
+
const existing = headers[varyKey];
|
|
3294
|
+
const tokens = (Array.isArray(existing) ? existing.join(",") : existing ?? "").split(",").map((t) => t.trim()).filter(Boolean);
|
|
3295
|
+
if (tokens.includes("*")) return;
|
|
3296
|
+
const seen = new Set(tokens.map((t) => t.toLowerCase()));
|
|
3297
|
+
for (const token of wanted) if (!seen.has(token.toLowerCase())) {
|
|
3298
|
+
seen.add(token.toLowerCase());
|
|
3299
|
+
tokens.push(token);
|
|
3300
|
+
}
|
|
3301
|
+
response.setHeader(varyKey, tokens.join(", "));
|
|
3302
|
+
}
|
|
3010
3303
|
//#endregion
|
|
3011
3304
|
//#region src/actions/verdict.ts
|
|
3012
3305
|
/** Assert that a `disabled` predicate returned an array of the expected length; throws HTTP 500 otherwise. */
|
|
@@ -3069,7 +3362,8 @@ function computeStripFields(candidates, resolvedProjection) {
|
|
|
3069
3362
|
/**
|
|
3070
3363
|
* Sets `$actions` on every row (plus `$disabledReasons` on rows where a
|
|
3071
3364
|
* predicate returned a reason string) and strips the columns fetched only for an
|
|
3072
|
-
* action's `requiredFields` — IN PLACE
|
|
3365
|
+
* action's `requiredFields` — IN PLACE in `rows` (a stripped row is replaced
|
|
3366
|
+
* by a rebuilt copy, see `omitKeys`); returns the same array, typed as
|
|
3073
3367
|
* augmented.
|
|
3074
3368
|
*/
|
|
3075
3369
|
function augmentRowsWithActions(args) {
|
|
@@ -3079,7 +3373,11 @@ function augmentRowsWithActions(args) {
|
|
|
3079
3373
|
const verdicts = candidates.map((c) => {
|
|
3080
3374
|
if (!c.disabledFn) return void 0;
|
|
3081
3375
|
const fields = gateFields?.(c.envelope);
|
|
3082
|
-
|
|
3376
|
+
let input = rows;
|
|
3377
|
+
if (fields && rows.some((row) => exceedsFields(row, fields))) {
|
|
3378
|
+
const paths = splitPaths(fields);
|
|
3379
|
+
input = rows.map((row) => projectRow$2(row, paths));
|
|
3380
|
+
}
|
|
3083
3381
|
return judgeRows(c.envelope.info.name, c.disabledFn, input);
|
|
3084
3382
|
});
|
|
3085
3383
|
const masks = outOfScope && candidates.map((c) => outOfScope.get(c.envelope.info.name));
|
|
@@ -3103,7 +3401,11 @@ function augmentRowsWithActions(args) {
|
|
|
3103
3401
|
}
|
|
3104
3402
|
if (resolvedProjection !== null) {
|
|
3105
3403
|
const stripFields = computeStripFields(candidates, resolvedProjection);
|
|
3106
|
-
if (stripFields !== null) for (
|
|
3404
|
+
if (stripFields !== null) for (let i = 0; i < rows.length; i++) {
|
|
3405
|
+
const row = rows[i];
|
|
3406
|
+
if ((0, _atscript_db.isPlainObject)(row)) rows[i] = omitKeys(row, stripFields);
|
|
3407
|
+
else for (const key of stripFields) delete row[key];
|
|
3408
|
+
}
|
|
3107
3409
|
}
|
|
3108
3410
|
return rows;
|
|
3109
3411
|
}
|
|
@@ -4186,7 +4488,7 @@ function buildDecorationIndex(controller, meta, source) {
|
|
|
4186
4488
|
requires,
|
|
4187
4489
|
visibleOn,
|
|
4188
4490
|
leavesOf,
|
|
4189
|
-
memo: {
|
|
4491
|
+
memo: {}
|
|
4190
4492
|
};
|
|
4191
4493
|
}
|
|
4192
4494
|
//#endregion
|
|
@@ -4377,45 +4679,46 @@ var DecorationPlanner = class {
|
|
|
4377
4679
|
* `fields[key]` entry (from its virtual capability-index entry); the others
|
|
4378
4680
|
* are pruned, and `decorations` is dropped when none is left. Runs after
|
|
4379
4681
|
* `applyMetaOverlay` — an overlay never sees the decoration `fields`
|
|
4380
|
-
* entries. Memoized per input
|
|
4682
|
+
* entries. Memoized per input and per the request's decoration visibility.
|
|
4381
4683
|
*/
|
|
4382
4684
|
meta(meta) {
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
const
|
|
4386
|
-
const hit = memoize ? memo.meta.get(meta) : void 0;
|
|
4387
|
-
if (hit) return hit;
|
|
4685
|
+
const decorations = meta.decorations;
|
|
4686
|
+
if (!decorations) return meta;
|
|
4687
|
+
const { keySet } = this.index;
|
|
4388
4688
|
const capabilities = this.host.capabilities();
|
|
4389
|
-
const props =
|
|
4390
|
-
const
|
|
4391
|
-
const
|
|
4392
|
-
|
|
4393
|
-
const
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
out = rest;
|
|
4406
|
-
} else out = {
|
|
4407
|
-
...meta,
|
|
4408
|
-
fields,
|
|
4409
|
-
decorations: {
|
|
4410
|
-
...meta.decorations,
|
|
4411
|
-
type: {
|
|
4412
|
-
...meta.decorations.type,
|
|
4413
|
-
props: kept
|
|
4414
|
-
}
|
|
4689
|
+
const props = decorations.type.props ?? {};
|
|
4690
|
+
const entries = Object.entries(props);
|
|
4691
|
+
const visible = entries.map(([key]) => keySet.has(key) && this.visible(key));
|
|
4692
|
+
return metaVariant(this.host.metaVariants, meta, `p:${identityId(capabilities)}:${bits(visible)}`, () => {
|
|
4693
|
+
const kept = {};
|
|
4694
|
+
const fields = { ...meta.fields };
|
|
4695
|
+
for (let i = 0; i < entries.length; i++) {
|
|
4696
|
+
const [key, prop] = entries[i];
|
|
4697
|
+
const cap = visible[i] ? capabilities.decorationCap(key) : void 0;
|
|
4698
|
+
if (!cap) continue;
|
|
4699
|
+
kept[key] = prop;
|
|
4700
|
+
fields[key] = {
|
|
4701
|
+
sortable: cap.sortable,
|
|
4702
|
+
filterable: cap.filterable,
|
|
4703
|
+
decoration: true
|
|
4704
|
+
};
|
|
4415
4705
|
}
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4706
|
+
if (Object.keys(kept).length === 0) {
|
|
4707
|
+
const { decorations: _dropped, ...rest } = meta;
|
|
4708
|
+
return rest;
|
|
4709
|
+
}
|
|
4710
|
+
return {
|
|
4711
|
+
...meta,
|
|
4712
|
+
fields,
|
|
4713
|
+
decorations: {
|
|
4714
|
+
...decorations,
|
|
4715
|
+
type: {
|
|
4716
|
+
...decorations.type,
|
|
4717
|
+
props: kept
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
};
|
|
4721
|
+
});
|
|
4419
4722
|
}
|
|
4420
4723
|
/** The declared interface serialized for `/meta.decorations`, once per class and app scope. */
|
|
4421
4724
|
serialized(scope, serialize) {
|
|
@@ -4427,18 +4730,31 @@ var DecorationPlanner = class {
|
|
|
4427
4730
|
return memo.serialized;
|
|
4428
4731
|
}
|
|
4429
4732
|
};
|
|
4430
|
-
/**
|
|
4733
|
+
/**
|
|
4734
|
+
* Removes what a read must not carry — the unserved declared keys and the
|
|
4735
|
+
* hook-only paths — from `rows`, in place in the array: a row losing a
|
|
4736
|
+
* top-level key is replaced by a rebuilt copy (`omitKeys`); nested paths are
|
|
4737
|
+
* deleted inside it.
|
|
4738
|
+
*/
|
|
4431
4739
|
function stripDecorations(rows, read) {
|
|
4432
4740
|
const { dropKeys, dropPaths, keepPaths, selectedPaths } = read;
|
|
4433
4741
|
if (dropKeys.length === 0 && dropPaths.length === 0) return;
|
|
4434
4742
|
const owned = (prefix) => selectedPaths !== void 0 && selectedPaths.some((sel) => prefix.every((part, i) => sel[i] === part));
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4743
|
+
const topLevel = new Set(dropKeys);
|
|
4744
|
+
const nested = [];
|
|
4745
|
+
dropPaths.forEach((parts, i) => {
|
|
4746
|
+
if (parts.length === 1 && (keepPaths[i] ?? []).length === 0) topLevel.add(parts[0]);
|
|
4747
|
+
else nested.push(i);
|
|
4748
|
+
});
|
|
4749
|
+
for (let r = 0; r < rows.length; r++) {
|
|
4750
|
+
let row = rows[r];
|
|
4751
|
+
if ((0, _atscript_db.isPlainObject)(row)) row = rows[r] = omitKeys(row, topLevel);
|
|
4752
|
+
else for (const key of dropKeys) delete row[key];
|
|
4753
|
+
for (const i of nested) {
|
|
4438
4754
|
const keep = keepPaths[i] ?? [];
|
|
4439
|
-
if (keep.length === 0) deleteDescending(row,
|
|
4440
|
-
else pruneExcept(row,
|
|
4441
|
-
}
|
|
4755
|
+
if (keep.length === 0) deleteDescending(row, dropPaths[i], 0, selectedPaths !== void 0, owned);
|
|
4756
|
+
else pruneExcept(row, dropPaths[i], 0, keep);
|
|
4757
|
+
}
|
|
4442
4758
|
}
|
|
4443
4759
|
}
|
|
4444
4760
|
/** An object without keys, or an array (of any nesting) holding nothing but such values (what a strip left of a parent). */
|
|
@@ -4561,6 +4877,15 @@ function routedController(ctx) {
|
|
|
4561
4877
|
return controllerOf(c);
|
|
4562
4878
|
}
|
|
4563
4879
|
}
|
|
4880
|
+
/**
|
|
4881
|
+
* `collectQueryPaths`' aggregate flag for a parsed query: `true` when its
|
|
4882
|
+
* `$select` carries computed entries (aggregates, calendar buckets), else
|
|
4883
|
+
* `undefined` (aggregate mode iff `$groupBy`).
|
|
4884
|
+
*/
|
|
4885
|
+
function computedMode(parsed) {
|
|
4886
|
+
const select = parsed.controls?.$select;
|
|
4887
|
+
return Array.isArray(select) && select.some((item) => typeof item !== "string") || void 0;
|
|
4888
|
+
}
|
|
4564
4889
|
/** The 400 of a filter / sort on a `@db.writeOnly` field. */
|
|
4565
4890
|
function writeOnlyError(path, op) {
|
|
4566
4891
|
const verdict = writeOnlyVerdict(path, op);
|
|
@@ -4601,13 +4926,34 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4601
4926
|
* would keep advertising (and gating) the pre-sync answer (since 0.1.132).
|
|
4602
4927
|
*/
|
|
4603
4928
|
get capabilities() {
|
|
4929
|
+
const readable = this.readable;
|
|
4930
|
+
const geo = readable.isGeoSearchable();
|
|
4931
|
+
const units = readable.calendarBucketUnits();
|
|
4932
|
+
const fns = readable.aggregateFns();
|
|
4933
|
+
const expr = readable.supportsAggregateExpressions();
|
|
4604
4934
|
const current = this._capabilities;
|
|
4605
|
-
|
|
4606
|
-
|
|
4935
|
+
const seen = this._capabilityInputs;
|
|
4936
|
+
if (current && seen && seen.geo === geo && seen.units === units && seen.unitsSize === units.size && seen.fns === fns && seen.fnsSize === fns.size && seen.expr === expr) return current;
|
|
4937
|
+
const inputs = {
|
|
4938
|
+
geo,
|
|
4939
|
+
units,
|
|
4940
|
+
unitsSize: units.size,
|
|
4941
|
+
fns,
|
|
4942
|
+
fnsSize: fns.size,
|
|
4943
|
+
expr
|
|
4944
|
+
};
|
|
4945
|
+
if (current && current.signature === FieldCapabilityIndex.adapterSignature(readable)) {
|
|
4946
|
+
this._capabilityInputs = inputs;
|
|
4947
|
+
return current;
|
|
4948
|
+
}
|
|
4949
|
+
const index = new FieldCapabilityIndex(readable, this._writeOnlySet, this._decorations?.visibleOn);
|
|
4607
4950
|
this._capabilities = index;
|
|
4951
|
+
this._capabilityInputs = inputs;
|
|
4608
4952
|
return index;
|
|
4609
4953
|
}
|
|
4610
4954
|
_capabilities;
|
|
4955
|
+
/** The adapter answers {@link capabilities} last confirmed its index against (identity fast path). */
|
|
4956
|
+
_capabilityInputs;
|
|
4611
4957
|
/** The client relational-predicate gate (since 0.1.147), built on first use — see {@link _relationGate}. */
|
|
4612
4958
|
_relGate;
|
|
4613
4959
|
/**
|
|
@@ -4640,6 +4986,12 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4640
4986
|
_derivedSource;
|
|
4641
4987
|
/** `@db.writeOnly` paths of `$with` target readables, collected once per target. */
|
|
4642
4988
|
_targetWriteOnly = /* @__PURE__ */ new WeakMap();
|
|
4989
|
+
/** The forced exclusion `$select` per sealed set (see {@link _sealSelect}). */
|
|
4990
|
+
_exclusionBySealed = /* @__PURE__ */ new WeakMap();
|
|
4991
|
+
/** The objects of {@link _exclusionBySealed}. */
|
|
4992
|
+
_forcedExclusions = /* @__PURE__ */ new WeakSet();
|
|
4993
|
+
/** {@link widenPreferredIdProjection} of each forced exclusion. */
|
|
4994
|
+
_widenedExclusions = /* @__PURE__ */ new WeakMap();
|
|
4643
4995
|
/** Own leaf paths per readable (bound + `$with` targets), see {@link _leavesOf}. */
|
|
4644
4996
|
_targetLeaves = /* @__PURE__ */ new WeakMap();
|
|
4645
4997
|
_indexFieldPathsCache;
|
|
@@ -4725,7 +5077,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4725
5077
|
this._idOpts = scoped ? { isFieldVisible: isVisible } : void 0;
|
|
4726
5078
|
this._planner = this._decorations && new DecorationPlanner(this._decorations, {
|
|
4727
5079
|
isVisible,
|
|
4728
|
-
|
|
5080
|
+
metaVariants: this._metaVariants,
|
|
4729
5081
|
preferred: this._preferredIdSet,
|
|
4730
5082
|
capabilities: () => this.capabilities,
|
|
4731
5083
|
firstVisibleField: () => this._invertibleFields.find((path) => isVisible(path))
|
|
@@ -4871,8 +5223,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4871
5223
|
checkCapabilities(parsed) {
|
|
4872
5224
|
const capabilities = this.capabilities;
|
|
4873
5225
|
const isVisible = this.fieldVisibility.isVisible;
|
|
4874
|
-
const
|
|
4875
|
-
const refs = (0, _atscript_db.collectQueryPaths)(parsed,
|
|
5226
|
+
const shared = this._sharedRefs;
|
|
5227
|
+
const refs = shared?.parsed === parsed ? shared.refs : (0, _atscript_db.collectQueryPaths)(parsed, computedMode(parsed));
|
|
4876
5228
|
if (refs.unsupportedOperator !== void 0) return badRequest(refs.unsupportedOperator, (0, _atscript_db.unsupportedOperatorMessage)(refs.unsupportedOperator));
|
|
4877
5229
|
const relState = { nodes: 0 };
|
|
4878
5230
|
for (const ref of refs.filter) {
|
|
@@ -4899,6 +5251,21 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4899
5251
|
if (having) return badRequest(having.path, having.message);
|
|
4900
5252
|
return this.checkGates(parsed) ?? this._checkIndexGate(parsed.controls ?? {});
|
|
4901
5253
|
}
|
|
5254
|
+
/** {@link checkCapabilities} reusing `refs` (the walk it makes) when the method is not overridden. */
|
|
5255
|
+
_checkCapabilitiesWith(parsed, refs) {
|
|
5256
|
+
if (this.checkCapabilities !== _AsDbReadableController.prototype.checkCapabilities) return this.checkCapabilities(parsed);
|
|
5257
|
+
this._sharedRefs = {
|
|
5258
|
+
parsed,
|
|
5259
|
+
refs
|
|
5260
|
+
};
|
|
5261
|
+
try {
|
|
5262
|
+
return this.checkCapabilities(parsed);
|
|
5263
|
+
} finally {
|
|
5264
|
+
this._sharedRefs = void 0;
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
/** The walk of the `parsed` in flight, handed to {@link checkCapabilities} (sync, cleared after). */
|
|
5268
|
+
_sharedRefs;
|
|
4902
5269
|
/**
|
|
4903
5270
|
* The core's shared normalizer of `$select` computed entries
|
|
4904
5271
|
* (`normalizeComputedSelect`) as a 400 with the core's wording and `path`
|
|
@@ -5237,15 +5604,22 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5237
5604
|
_applyIndexVisibility(meta) {
|
|
5238
5605
|
if (!this._indexGateActive) return meta;
|
|
5239
5606
|
const entries = this.indexFieldPaths();
|
|
5607
|
+
const indexVisible = entries.map((e) => this._indexVisible(e));
|
|
5608
|
+
const isSearchable = this.readable.isSearchable();
|
|
5609
|
+
const fallbackVisible = isSearchable ? [] : this._searchFallbackFields.map((f) => this.fieldVisibility.isVisible(f));
|
|
5610
|
+
return metaVariant(this._metaVariants, meta, `i:${bits(indexVisible)}:${isSearchable ? 1 : 0}:${bits(fallbackVisible)}`, () => this._narrowIndexVisibility(meta, entries, indexVisible, isSearchable, fallbackVisible));
|
|
5611
|
+
}
|
|
5612
|
+
/** {@link _applyIndexVisibility} for the given answers. */
|
|
5613
|
+
_narrowIndexVisibility(meta, entries, indexVisible, isSearchable, fallbackVisible) {
|
|
5240
5614
|
const visibleDefault = (type) => {
|
|
5241
|
-
const
|
|
5242
|
-
return
|
|
5615
|
+
const at = entries.findIndex((e) => e.type === type && e.isDefault);
|
|
5616
|
+
return at >= 0 && indexVisible[at];
|
|
5243
5617
|
};
|
|
5244
5618
|
const has = (type) => entries.some((e) => e.type === type);
|
|
5245
|
-
const anyVisible = (type) => entries.some((e) => e.type === type &&
|
|
5246
|
-
const hidden = new Set(entries.filter((e) => e.type !== "geo" && !
|
|
5247
|
-
const searchable =
|
|
5248
|
-
const textUsable = searchable ||
|
|
5619
|
+
const anyVisible = (type) => entries.some((e, i) => e.type === type && indexVisible[i]);
|
|
5620
|
+
const hidden = new Set(entries.filter((e, i) => e.type !== "geo" && !indexVisible[i]).map((e) => e.name));
|
|
5621
|
+
const searchable = isSearchable ? visibleDefault("text") : fallbackVisible.some(Boolean);
|
|
5622
|
+
const textUsable = searchable || isSearchable && anyVisible("text");
|
|
5249
5623
|
const vectorUsable = anyVisible("vector");
|
|
5250
5624
|
const unusable = /* @__PURE__ */ new Set();
|
|
5251
5625
|
if (has("text") && !anyVisible("text")) unusable.add("index");
|
|
@@ -5479,7 +5853,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5479
5853
|
const transformed = await this.transformProjection(plan ? plan.select : controls.$select);
|
|
5480
5854
|
const sealed = this._sealControls(controls, transformed);
|
|
5481
5855
|
const finish = () => {
|
|
5482
|
-
const select = this.
|
|
5856
|
+
const select = this._widenSealed(sealed.$select);
|
|
5483
5857
|
if (select instanceof _moostjs_event_http.HttpError) return select;
|
|
5484
5858
|
let kept;
|
|
5485
5859
|
const keptPaths = () => kept === void 0 ? kept = this._resolveProjectionForAugmenter(select) : kept;
|
|
@@ -5494,6 +5868,23 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5494
5868
|
finish
|
|
5495
5869
|
};
|
|
5496
5870
|
}
|
|
5871
|
+
/**
|
|
5872
|
+
* {@link widenPreferredIdProjection} of a sealed `$select` — memoized for the
|
|
5873
|
+
* forced exclusion of a read without `$select` (one object per sealed set,
|
|
5874
|
+
* see {@link _sealSelect}): its widening depends on the controller alone.
|
|
5875
|
+
*/
|
|
5876
|
+
_widenSealed(select) {
|
|
5877
|
+
if (select === void 0 || !this._forcedExclusions.has(select)) return this.widenPreferredIdProjection(select);
|
|
5878
|
+
let widened = this._widenedExclusions.get(select);
|
|
5879
|
+
if (widened === void 0) {
|
|
5880
|
+
const out = this.widenPreferredIdProjection(select);
|
|
5881
|
+
if (out instanceof _moostjs_event_http.HttpError) return out;
|
|
5882
|
+
if (out !== void 0 && out !== select) freezeShared(out);
|
|
5883
|
+
this._widenedExclusions.set(select, out);
|
|
5884
|
+
widened = out;
|
|
5885
|
+
}
|
|
5886
|
+
return widened;
|
|
5887
|
+
}
|
|
5497
5888
|
widenPreferredIdProjection(projection) {
|
|
5498
5889
|
const widened = this.widenQuantityRefProjection(projection);
|
|
5499
5890
|
if (widened instanceof _moostjs_event_http.HttpError) return widened;
|
|
@@ -5821,7 +6212,15 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5821
6212
|
const plain = (q) => this.readable.findMany(q);
|
|
5822
6213
|
return byIds(plain, ids, void 0, fields);
|
|
5823
6214
|
}
|
|
5824
|
-
|
|
6215
|
+
const pathsByShape = /* @__PURE__ */ new Map();
|
|
6216
|
+
const pathsOf = (id) => {
|
|
6217
|
+
const keys = Object.keys(id);
|
|
6218
|
+
const shape = keys.join("");
|
|
6219
|
+
let paths = pathsByShape.get(shape);
|
|
6220
|
+
if (!paths) pathsByShape.set(shape, paths = splitPaths(new Set([...fields, ...keys])));
|
|
6221
|
+
return paths;
|
|
6222
|
+
};
|
|
6223
|
+
return Promise.resolve(alignRowsToIds(rows, ids).map((row, i) => row ? projectRow$2(row, pathsOf(ids[i])) : void 0));
|
|
5825
6224
|
}
|
|
5826
6225
|
};
|
|
5827
6226
|
}
|
|
@@ -6004,7 +6403,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
6004
6403
|
sortGate: ids.length > 0 ? void 0 : order,
|
|
6005
6404
|
routeParams: sameRoute ? void 0 : {}
|
|
6006
6405
|
});
|
|
6007
|
-
|
|
6406
|
+
const paths = splitPaths(sealedSelect);
|
|
6407
|
+
return rows.map((row) => projectRow$2(row, paths));
|
|
6008
6408
|
}
|
|
6009
6409
|
/**
|
|
6010
6410
|
* Runs `fn` as a READ of this controller (since 0.1.147): in a child of
|
|
@@ -6048,8 +6448,18 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
6048
6448
|
_sealSelect(select, writeOnly, readable) {
|
|
6049
6449
|
if (writeOnly.size === 0) return select;
|
|
6050
6450
|
const exclusion = () => {
|
|
6051
|
-
const
|
|
6052
|
-
|
|
6451
|
+
const shared = writeOnly === this._writeOnlySet || writeOnly === this._targetWriteOnly.get(readable);
|
|
6452
|
+
let out = shared ? this._exclusionBySealed.get(writeOnly) : void 0;
|
|
6453
|
+
if (out === void 0) {
|
|
6454
|
+
const built = {};
|
|
6455
|
+
for (const f of writeOnly) built[f] = 0;
|
|
6456
|
+
out = built;
|
|
6457
|
+
if (shared) {
|
|
6458
|
+
freezeShared(out);
|
|
6459
|
+
this._exclusionBySealed.set(writeOnly, out);
|
|
6460
|
+
this._forcedExclusions.add(out);
|
|
6461
|
+
}
|
|
6462
|
+
}
|
|
6053
6463
|
return out;
|
|
6054
6464
|
};
|
|
6055
6465
|
if (select === void 0) return exclusion();
|
|
@@ -6321,10 +6731,11 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
6321
6731
|
const [overlaid, delegated] = await Promise.all([own, this._hasDelegations ? this._delegatedInfos() : []]);
|
|
6322
6732
|
const meta = this._planner ? this._planner.meta(overlaid) : overlaid;
|
|
6323
6733
|
const visible = this._applyIndexVisibility(meta);
|
|
6324
|
-
|
|
6734
|
+
if (delegated.length === 0) return visible;
|
|
6735
|
+
return metaVariant(this._metaVariants, visible, `d:${delegated.map((info) => identityId(info)).join(",")}`, () => ({
|
|
6325
6736
|
...visible,
|
|
6326
6737
|
actions: [...visible.actions, ...delegated]
|
|
6327
|
-
}
|
|
6738
|
+
}));
|
|
6328
6739
|
})();
|
|
6329
6740
|
}
|
|
6330
6741
|
/**
|
|
@@ -6590,8 +7001,10 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
6590
7001
|
if (groupBy?.length && controls.$vector !== void 0) return new _moostjs_event_http.HttpError(400, "Cannot combine $vector and $groupBy in the same query");
|
|
6591
7002
|
const error = this.validateParsed(parsed, "query");
|
|
6592
7003
|
if (error) return error;
|
|
7004
|
+
let gateRefs;
|
|
6593
7005
|
if (groupBy?.length && this._writeOnlySet.size > 0) {
|
|
6594
|
-
|
|
7006
|
+
gateRefs = (0, _atscript_db.collectQueryPaths)(parsed, computedMode(parsed));
|
|
7007
|
+
const refs = gateRefs.aggregateMode ? gateRefs : (0, _atscript_db.collectQueryPaths)(parsed, true);
|
|
6595
7008
|
const sealed = [
|
|
6596
7009
|
...refs.groupBy,
|
|
6597
7010
|
...refs.aggregate,
|
|
@@ -6601,7 +7014,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
6601
7014
|
].find((path) => this._writeOnlySet.has(path) && this.fieldVisibility.isVisible(path));
|
|
6602
7015
|
if (sealed) return new _moostjs_event_http.HttpError(400, `Field "${sealed}" is @db.writeOnly and cannot be aggregated`);
|
|
6603
7016
|
}
|
|
6604
|
-
const gateError = this.checkCapabilities(parsed);
|
|
7017
|
+
const gateError = gateRefs ? this._checkCapabilitiesWith(parsed, gateRefs) : this.checkCapabilities(parsed);
|
|
6605
7018
|
if (gateError) return gateError;
|
|
6606
7019
|
const clientFilter = await this._relationOverlay(parsed);
|
|
6607
7020
|
if (groupBy?.length) {
|
|
@@ -6793,14 +7206,15 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
6793
7206
|
};
|
|
6794
7207
|
const item = await this.returnOne(this._findRow(resolvedId, overlay, readControls));
|
|
6795
7208
|
if (item instanceof _moostjs_event_http.HttpError) return item;
|
|
6796
|
-
const
|
|
7209
|
+
const rows = [item];
|
|
7210
|
+
const pending = this._finishRows(rows, prep, {
|
|
6797
7211
|
endpoint: "one",
|
|
6798
7212
|
projection: projected.select,
|
|
6799
7213
|
controls,
|
|
6800
7214
|
decorations: projected.read?.served ?? NO_DECORATIONS
|
|
6801
7215
|
}, projected.read);
|
|
6802
7216
|
if (pending) await pending;
|
|
6803
|
-
return
|
|
7217
|
+
return rows[0];
|
|
6804
7218
|
}
|
|
6805
7219
|
/**
|
|
6806
7220
|
* **GET /meta/actions/:id** — the row-level actions the caller may run on
|
|
@@ -7069,7 +7483,9 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
7069
7483
|
for (const row of rows) if (row) present.push(row);
|
|
7070
7484
|
const verdicts = envelopes.map((e, i) => {
|
|
7071
7485
|
const disabled = getCandidate(e)?.disabledFn;
|
|
7072
|
-
|
|
7486
|
+
if (!disabled || present.length === 0) return void 0;
|
|
7487
|
+
const paths = splitPaths(fieldsOf[i]);
|
|
7488
|
+
return judgeRows(e.info.name, disabled, present.map((row) => projectRow$2(row, paths)));
|
|
7073
7489
|
});
|
|
7074
7490
|
let p = 0;
|
|
7075
7491
|
return rows.map((row, r) => {
|
|
@@ -7877,22 +8293,29 @@ let AsJsonValueHelpController = class AsJsonValueHelpController extends AsValueH
|
|
|
7877
8293
|
}
|
|
7878
8294
|
async query(controls) {
|
|
7879
8295
|
let rows = this.rows;
|
|
7880
|
-
if (controls.filter && Object.keys(controls.filter).length > 0) {
|
|
7881
|
-
const predicate = (0, _atscript_db_memory.buildMemoryPredicate)(controls.filter);
|
|
7882
|
-
rows = rows.filter((row) => predicate(row));
|
|
7883
|
-
}
|
|
7884
8296
|
const search = controls.controls.$search;
|
|
7885
8297
|
if (search) {
|
|
7886
8298
|
const needle = search.toLowerCase();
|
|
7887
|
-
const
|
|
7888
|
-
rows = rows.filter((row) => {
|
|
7889
|
-
for (const field of
|
|
8299
|
+
const columns = this.searchableFields.filter((field) => this.hasField(field)).map((field) => [field, this._lowercased(field)]);
|
|
8300
|
+
rows = rows.filter((row, i) => {
|
|
8301
|
+
for (const [field, column] of columns) {
|
|
7890
8302
|
const v = row[field];
|
|
7891
|
-
if (typeof v
|
|
8303
|
+
if (typeof v !== "string") continue;
|
|
8304
|
+
let lower = column.lower[i];
|
|
8305
|
+
if (lower === void 0 || column.src[i] !== v) {
|
|
8306
|
+
lower = v.toLowerCase();
|
|
8307
|
+
column.src[i] = v;
|
|
8308
|
+
column.lower[i] = lower;
|
|
8309
|
+
}
|
|
8310
|
+
if (lower.includes(needle)) return true;
|
|
7892
8311
|
}
|
|
7893
8312
|
return false;
|
|
7894
8313
|
});
|
|
7895
8314
|
}
|
|
8315
|
+
if (controls.filter && Object.keys(controls.filter).length > 0) {
|
|
8316
|
+
const predicate = (0, _atscript_db_memory.buildMemoryPredicate)(controls.filter);
|
|
8317
|
+
rows = rows.filter((row) => predicate(row));
|
|
8318
|
+
}
|
|
7896
8319
|
const sort = this.normalizeSort(controls.controls.$sort);
|
|
7897
8320
|
if (sort) rows = (0, _atscript_db_memory.sortRows)(rows, sort);
|
|
7898
8321
|
const total = rows.length;
|
|
@@ -7905,6 +8328,25 @@ let AsJsonValueHelpController = class AsJsonValueHelpController extends AsValueH
|
|
|
7905
8328
|
count: total
|
|
7906
8329
|
};
|
|
7907
8330
|
}
|
|
8331
|
+
/**
|
|
8332
|
+
* `field`'s lowercased values by position in {@link rows} (the `$search`
|
|
8333
|
+
* haystack). An entry is used only while the row still holds the value it
|
|
8334
|
+
* was lowercased from (`src`), so rows edited in place stay correct.
|
|
8335
|
+
*/
|
|
8336
|
+
_lowercased(field) {
|
|
8337
|
+
let haystack = this._haystack;
|
|
8338
|
+
if (haystack?.rows !== this.rows) haystack = this._haystack = {
|
|
8339
|
+
rows: this.rows,
|
|
8340
|
+
byField: /* @__PURE__ */ new Map()
|
|
8341
|
+
};
|
|
8342
|
+
let column = haystack.byField.get(field);
|
|
8343
|
+
if (!column) haystack.byField.set(field, column = {
|
|
8344
|
+
src: [],
|
|
8345
|
+
lower: []
|
|
8346
|
+
});
|
|
8347
|
+
return column;
|
|
8348
|
+
}
|
|
8349
|
+
_haystack;
|
|
7908
8350
|
async getOne(id) {
|
|
7909
8351
|
return this._pkIndex?.get(String(id)) ?? null;
|
|
7910
8352
|
}
|
|
@@ -8786,6 +9228,7 @@ exports.resolveBoundReadable = resolveBoundReadable;
|
|
|
8786
9228
|
exports.resolveDbSpace = require_db_space_registry.resolveDbSpace;
|
|
8787
9229
|
exports.resolveProp = resolveProp;
|
|
8788
9230
|
exports.resolveTerminalRef = resolveTerminalRef;
|
|
9231
|
+
exports.stableMeta = stableMeta;
|
|
8789
9232
|
exports.unknownRelationError = unknownRelationError;
|
|
8790
9233
|
exports.useDbActionId = useDbActionId;
|
|
8791
9234
|
exports.useDbActionIds = useDbActionIds;
|