@atscript/moost-db 0.1.149 → 0.1.150
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 +616 -216
- package/dist/index.d.cts +105 -15
- package/dist/index.d.mts +105 -15
- package/dist/index.mjs +617 -217
- package/package.json +12 -12
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { ValidatorError, defineAnnotatedType, isAnnotatedType, serializeAnnotate
|
|
|
3
3
|
import { Body, Delete, Get, HttpError, MoostHttp, Patch, Post, Put, Query, Url } from "@moostjs/event-http";
|
|
4
4
|
import { ApplyDecorators, Controller, Inherit, Inject, Intercept, Moost, Optional, Param, Pipe, Provide, Resolve, TInterceptorPriority, TPipePriority, defineBeforeInterceptor, defineInterceptor, definePipeFn, getMoostMate, useControllerContext, withControllerContext } from "moost";
|
|
5
5
|
import { parseUrl } from "@uniqu/url";
|
|
6
|
-
import { ADAPTER_FILTER_REASON, ALL_AGGREGATE_FNS, DbError, ENCRYPTED_REASON, acceptedOperatorsHint, aliasTargetOf, andFilters, bucketSourceVerdict, canFilterLeaf, checkHavingKeys, classifyQueryPath, collectQueryPaths, collectQueryPaths as collectQueryPaths$1, containsRelationFilter, findAncestorInSet, geoIndexNotFoundMessage, groupSourceVerdict, hasRelationOp, isEmptyObject, isJsonValueField, isPlainObject, isRelationOp, narrowerFilterOps, normalizeComputedSelect, numericOperandProblem, reconcileCas, searchIndexNotFoundMessage, selfOrAncestor, unsupportedOperatorMessage, vectorIndexNotFoundMessage } from "@atscript/db";
|
|
6
|
+
import { ADAPTER_FILTER_REASON, ALL_AGGREGATE_FNS, DbError, ENCRYPTED_REASON, acceptedOperatorsHint, aliasTargetOf, andFilters, bucketSourceVerdict, canFilterLeaf, checkHavingKeys, classifyQueryPath, collectQueryPaths, collectQueryPaths as collectQueryPaths$1, containsRelationFilter, findAncestorInSet, geoIndexNotFoundMessage, groupSourceVerdict, hasRelationOp, isEmptyObject, isJsonValueField, isPlainObject, isRelationOp, narrowerFilterOps, normalizeComputedSelect, numericOperandProblem, reconcileCas, searchIndexNotFoundMessage, searchMemberKind, selfOrAncestor, unsupportedOperatorMessage, vectorIndexNotFoundMessage } from "@atscript/db";
|
|
7
7
|
import { cached, current, defineWook, key, routeParamsKey, useRouteParams } from "@wooksjs/event-core";
|
|
8
8
|
import { useBody } from "@wooksjs/http-body";
|
|
9
9
|
import { BUCKET_UNITS } from "@uniqu/core";
|
|
@@ -2107,6 +2107,393 @@ function walk(node, def, shallow) {
|
|
|
2107
2107
|
}
|
|
2108
2108
|
}
|
|
2109
2109
|
//#endregion
|
|
2110
|
+
//#region src/http-path-design-time.ts
|
|
2111
|
+
const KEY$1 = "db.http.path";
|
|
2112
|
+
/** The `@db.http.path` value a model carried before moost-db first wrote its published path. */
|
|
2113
|
+
const designTime = /* @__PURE__ */ new WeakMap();
|
|
2114
|
+
/**
|
|
2115
|
+
* Remembers the model's own `@db.http.path` (or its absence) before the first
|
|
2116
|
+
* published path is mirrored into its runtime metadata. Idempotent.
|
|
2117
|
+
*
|
|
2118
|
+
* @since 0.1.150
|
|
2119
|
+
*/
|
|
2120
|
+
function captureDesignTime(type) {
|
|
2121
|
+
if (!designTime.has(type)) designTime.set(type, type.metadata.get(KEY$1));
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* The route hint a model declares with `@db.http.path` — never the path
|
|
2125
|
+
* moost-db mirrored into the runtime metadata afterwards. Decorators and
|
|
2126
|
+
* `assertExposed` read this.
|
|
2127
|
+
*
|
|
2128
|
+
* @since 0.1.150
|
|
2129
|
+
*/
|
|
2130
|
+
function designTimeHttpPath(type) {
|
|
2131
|
+
return designTime.has(type) ? designTime.get(type) : type.metadata.get(KEY$1);
|
|
2132
|
+
}
|
|
2133
|
+
/** The captured design-time value (`undefined` when none was captured or declared). */
|
|
2134
|
+
function getDesignTime(type) {
|
|
2135
|
+
return designTime.get(type);
|
|
2136
|
+
}
|
|
2137
|
+
//#endregion
|
|
2138
|
+
//#region src/decorators.ts
|
|
2139
|
+
/**
|
|
2140
|
+
* DI token under which the {@link AtscriptDbReadable} instance
|
|
2141
|
+
* is exposed to the readable controller's constructor via `@Inject`.
|
|
2142
|
+
*/
|
|
2143
|
+
const READABLE_DEF = "__atscript_db_readable_def";
|
|
2144
|
+
/**
|
|
2145
|
+
* DI token under which the {@link AtscriptDbTable} instance
|
|
2146
|
+
* is exposed to the controller's constructor via `@Inject`.
|
|
2147
|
+
* Points to the same token as READABLE_DEF for backward compatibility.
|
|
2148
|
+
*/
|
|
2149
|
+
const TABLE_DEF = READABLE_DEF;
|
|
2150
|
+
function normalizeOptions(prefixOrOptions) {
|
|
2151
|
+
if (typeof prefixOrOptions === "string") return { prefix: prefixOrOptions };
|
|
2152
|
+
return prefixOrOptions ?? {};
|
|
2153
|
+
}
|
|
2154
|
+
/**
|
|
2155
|
+
* Builds the shared binding metadata for a decorator invocation: classifies
|
|
2156
|
+
* the binding form, computes the static route prefix, and packages a uniform
|
|
2157
|
+
* `resolve()` used by both the DI provide factory and the base controller's
|
|
2158
|
+
* `super(app)` fallback.
|
|
2159
|
+
*/
|
|
2160
|
+
function buildBinding(binding, options, decoratorName) {
|
|
2161
|
+
const canonical = options.canonical;
|
|
2162
|
+
if (isAnnotatedType(binding)) {
|
|
2163
|
+
const model = binding;
|
|
2164
|
+
const space = options.space ?? model.metadata.get("db.space");
|
|
2165
|
+
const hinted = designTimeHttpPath(model);
|
|
2166
|
+
const prefix = options.prefix || hinted || model.metadata.get("db.table") || model.metadata.get("db.view") || model.id || "";
|
|
2167
|
+
const prefixSource = options.prefix ? "option" : hinted ? "annotation" : "name";
|
|
2168
|
+
if (!prefix) throw new Error(`[moost-db] @${decoratorName}: cannot derive a route prefix from the model token (no @db.http.path / @db.table / @db.view and no type id). Pass an explicit prefix.`);
|
|
2169
|
+
return {
|
|
2170
|
+
meta: {
|
|
2171
|
+
model,
|
|
2172
|
+
resolve: () => resolveDbSpace(space).get(model),
|
|
2173
|
+
prefix,
|
|
2174
|
+
prefixSource,
|
|
2175
|
+
canonical
|
|
2176
|
+
},
|
|
2177
|
+
prefix
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
if (typeof binding === "function") {
|
|
2181
|
+
if (!options.prefix) throw new Error(`[moost-db] @${decoratorName}: the lazy factory form needs an explicit route prefix (the readable is not created until app.init()). Pass a prefix, or use the model token form which derives it from @db.http.path / @db.table.`);
|
|
2182
|
+
return {
|
|
2183
|
+
meta: {
|
|
2184
|
+
resolve: binding,
|
|
2185
|
+
prefix: options.prefix,
|
|
2186
|
+
prefixSource: "option",
|
|
2187
|
+
canonical
|
|
2188
|
+
},
|
|
2189
|
+
prefix: options.prefix
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
const readable = binding;
|
|
2193
|
+
const hinted = designTimeHttpPath(readable.type);
|
|
2194
|
+
const prefix = options.prefix || hinted || readable.tableName;
|
|
2195
|
+
return {
|
|
2196
|
+
meta: {
|
|
2197
|
+
model: readable.type,
|
|
2198
|
+
resolve: () => readable,
|
|
2199
|
+
prefix,
|
|
2200
|
+
prefixSource: options.prefix ? "option" : hinted ? "annotation" : "name",
|
|
2201
|
+
canonical
|
|
2202
|
+
},
|
|
2203
|
+
prefix
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
function bindReadableController(binding, prefixOrOptions, decoratorName) {
|
|
2207
|
+
const { meta, prefix } = buildBinding(binding, normalizeOptions(prefixOrOptions), decoratorName);
|
|
2208
|
+
return ApplyDecorators(getAtscriptDbMate().decorate((classMeta) => {
|
|
2209
|
+
classMeta.atscript_db_readable_binding = meta;
|
|
2210
|
+
return classMeta;
|
|
2211
|
+
}), Provide(READABLE_DEF, () => meta.resolve()), Controller(prefix), Inherit());
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Combines the boilerplate needed to turn an {@link AsDbController}
|
|
2215
|
+
* subclass into a fully wired HTTP controller for a given `@db.table` model.
|
|
2216
|
+
*
|
|
2217
|
+
* Internally applies three decorators:
|
|
2218
|
+
* 1. **Provide** — registers the readable resolver under {@link TABLE_DEF}.
|
|
2219
|
+
* 2. **Controller** — registers the class as a Moost HTTP controller
|
|
2220
|
+
* with an optional route prefix (defaults to `@db.http.path`, then the
|
|
2221
|
+
* table name).
|
|
2222
|
+
* 3. **Inherit** — copies metadata (routes, guards, etc.) from the
|
|
2223
|
+
* parent class so they stay active in the derived controller.
|
|
2224
|
+
*
|
|
2225
|
+
* All three binding forms are supported (see {@link TReadableBinding}):
|
|
2226
|
+
*
|
|
2227
|
+
* ```ts
|
|
2228
|
+
* @TableController(User) // model token (preferred)
|
|
2229
|
+
* @TableController(User, { space: "analytics" })
|
|
2230
|
+
* @TableController(() => db.getTable(User), "users") // lazy factory
|
|
2231
|
+
* @TableController(usersTable) // instance (legacy)
|
|
2232
|
+
* export class UsersController extends AsDbController<typeof User> {}
|
|
2233
|
+
* ```
|
|
2234
|
+
*
|
|
2235
|
+
* Token and factory forms resolve lazily at first controller instantiation
|
|
2236
|
+
* (during `app.init()`), so the `DbSpace` does not have to exist when the
|
|
2237
|
+
* controller module is imported. For the token form, register the space with
|
|
2238
|
+
* `provideDbSpace(db)` before `app.init()`.
|
|
2239
|
+
*
|
|
2240
|
+
* @param binding Model token, lazy factory, or {@link AtscriptDbTable} instance.
|
|
2241
|
+
* @param prefixOrOptions Route prefix string, or {@link TControllerBindingOptions}.
|
|
2242
|
+
*/
|
|
2243
|
+
const TableController = (binding, prefixOrOptions) => bindReadableController(binding, prefixOrOptions, "TableController");
|
|
2244
|
+
/**
|
|
2245
|
+
* Combines the boilerplate needed to turn an {@link AsDbReadableController}
|
|
2246
|
+
* subclass into a fully wired HTTP controller for a given `@db.view` or `@db.table` model.
|
|
2247
|
+
*
|
|
2248
|
+
* Accepts the same three binding forms as {@link TableController}.
|
|
2249
|
+
*
|
|
2250
|
+
* @param binding Model token, lazy factory, or {@link AtscriptDbReadable} instance.
|
|
2251
|
+
* @param prefixOrOptions Route prefix string, or {@link TControllerBindingOptions}.
|
|
2252
|
+
*
|
|
2253
|
+
* @example
|
|
2254
|
+
* ```ts
|
|
2255
|
+
* @ReadableController(ActiveTasks)
|
|
2256
|
+
* export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
|
|
2257
|
+
* ```
|
|
2258
|
+
*/
|
|
2259
|
+
const ReadableController = (binding, prefixOrOptions) => bindReadableController(binding, prefixOrOptions, "ReadableController");
|
|
2260
|
+
/**
|
|
2261
|
+
* Alias for {@link ReadableController} — use with view-backed controllers.
|
|
2262
|
+
*
|
|
2263
|
+
* @example
|
|
2264
|
+
* ```ts
|
|
2265
|
+
* @ViewController(ActiveTasks)
|
|
2266
|
+
* export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
|
|
2267
|
+
* ```
|
|
2268
|
+
*/
|
|
2269
|
+
const ViewController = ReadableController;
|
|
2270
|
+
/**
|
|
2271
|
+
* Finds the readable binding written by `@TableController` /
|
|
2272
|
+
* `@ReadableController` / `@ViewController` on a controller class, walking the
|
|
2273
|
+
* prototype chain so intermediate undecorated classes don't hide the binding
|
|
2274
|
+
* (nearest decorated ancestor wins).
|
|
2275
|
+
*/
|
|
2276
|
+
function findReadableBinding(ctor) {
|
|
2277
|
+
const mate = getAtscriptDbMate();
|
|
2278
|
+
let current = ctor;
|
|
2279
|
+
while (typeof current === "function") {
|
|
2280
|
+
const binding = mate.read(current)?.atscript_db_readable_binding;
|
|
2281
|
+
if (binding) return binding;
|
|
2282
|
+
current = Object.getPrototypeOf(current);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* Resolves the readable bound to a controller class via
|
|
2287
|
+
* {@link findReadableBinding}, throwing with wiring guidance when none is
|
|
2288
|
+
* found.
|
|
2289
|
+
*
|
|
2290
|
+
* Used by {@link AsDbReadableController}'s constructor when `readable` is
|
|
2291
|
+
* `undefined` — i.e. a subclass with its own constructor called
|
|
2292
|
+
* `super(app)` instead of forwarding an injected instance.
|
|
2293
|
+
*/
|
|
2294
|
+
function resolveBoundReadable(ctor) {
|
|
2295
|
+
const binding = findReadableBinding(ctor);
|
|
2296
|
+
if (binding) return binding.resolve();
|
|
2297
|
+
throw new Error(`[moost-db] ${ctor?.name || "controller"}: no readable bound. Either pass a table/view to super(...), or decorate the class with @TableController / @ReadableController (model token, lazy factory, or instance form).`);
|
|
2298
|
+
}
|
|
2299
|
+
//#endregion
|
|
2300
|
+
//#region src/http-path.ts
|
|
2301
|
+
const KEY = "db.http.path";
|
|
2302
|
+
/**
|
|
2303
|
+
* Normalizes a controller prefix into a public path: one leading slash, no
|
|
2304
|
+
* empty segments, no trailing slash.
|
|
2305
|
+
*
|
|
2306
|
+
* @since 0.1.150
|
|
2307
|
+
*/
|
|
2308
|
+
function normalizeHttpPath(prefix) {
|
|
2309
|
+
return `/${prefix.split("/").filter(Boolean).join("/")}`;
|
|
2310
|
+
}
|
|
2311
|
+
/** A path with a route parameter or a wildcard cannot be a static picker URL. */
|
|
2312
|
+
function isParametricPath(path) {
|
|
2313
|
+
return path.split("/").some((s) => s.startsWith(":") || s.includes("*"));
|
|
2314
|
+
}
|
|
2315
|
+
/** Per app (the constructor's `app` argument): another app's construction never alters this app's records. */
|
|
2316
|
+
const bound = /* @__PURE__ */ new WeakMap();
|
|
2317
|
+
/**
|
|
2318
|
+
* Process-wide records per ctor: the fallback for an app that never
|
|
2319
|
+
* constructed the class itself (a DI singleton reused across apps), read only
|
|
2320
|
+
* when it holds exactly one model.
|
|
2321
|
+
*/
|
|
2322
|
+
const boundAny = /* @__PURE__ */ new WeakMap();
|
|
2323
|
+
let boundVersion = 0;
|
|
2324
|
+
/**
|
|
2325
|
+
* Records that `ctor` serves `type` inside `app` (called by the readable base constructor).
|
|
2326
|
+
* Pure bookkeeping — nothing is written to the model. Only a NEW pair bumps
|
|
2327
|
+
* the version that invalidates per-app scopes.
|
|
2328
|
+
*/
|
|
2329
|
+
function recordBoundType(app, ctor, type, canonical) {
|
|
2330
|
+
let ofApp = bound.get(app);
|
|
2331
|
+
if (!ofApp) {
|
|
2332
|
+
ofApp = /* @__PURE__ */ new WeakMap();
|
|
2333
|
+
bound.set(app, ofApp);
|
|
2334
|
+
}
|
|
2335
|
+
let models = ofApp.get(ctor);
|
|
2336
|
+
if (!models) {
|
|
2337
|
+
models = /* @__PURE__ */ new Map();
|
|
2338
|
+
ofApp.set(ctor, models);
|
|
2339
|
+
}
|
|
2340
|
+
let any = boundAny.get(ctor);
|
|
2341
|
+
if (!any) {
|
|
2342
|
+
any = /* @__PURE__ */ new Map();
|
|
2343
|
+
boundAny.set(ctor, any);
|
|
2344
|
+
}
|
|
2345
|
+
any.set(type, canonical);
|
|
2346
|
+
if (!models.has(type) || models.get(type) !== canonical) {
|
|
2347
|
+
models.set(type, canonical);
|
|
2348
|
+
boundVersion++;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
const apps = /* @__PURE__ */ new WeakMap();
|
|
2352
|
+
function stateOf(app) {
|
|
2353
|
+
let s = apps.get(app);
|
|
2354
|
+
if (!s) {
|
|
2355
|
+
s = {
|
|
2356
|
+
version: -1,
|
|
2357
|
+
published: false,
|
|
2358
|
+
hookAdded: false,
|
|
2359
|
+
warned: /* @__PURE__ */ new Set()
|
|
2360
|
+
};
|
|
2361
|
+
apps.set(app, s);
|
|
2362
|
+
}
|
|
2363
|
+
return s;
|
|
2364
|
+
}
|
|
2365
|
+
const idOf = (type) => type.id ?? "(unnamed model)";
|
|
2366
|
+
const hasOverview = (app) => !!app && typeof app.getControllersOverview === "function";
|
|
2367
|
+
function endsWithSegments(path, suffix) {
|
|
2368
|
+
const a = path.split("/").filter(Boolean);
|
|
2369
|
+
const b = suffix.split("/").filter(Boolean);
|
|
2370
|
+
if (b.length > a.length) return false;
|
|
2371
|
+
return b.every((seg, i) => a[a.length - b.length + i] === seg);
|
|
2372
|
+
}
|
|
2373
|
+
function warnOnce(app, s, key, message) {
|
|
2374
|
+
if (s.warned.has(key)) return;
|
|
2375
|
+
s.warned.add(key);
|
|
2376
|
+
try {
|
|
2377
|
+
app.getLogger?.("moost-db")?.warn(message);
|
|
2378
|
+
} catch {}
|
|
2379
|
+
}
|
|
2380
|
+
function buildScope(app, s) {
|
|
2381
|
+
const mounts = /* @__PURE__ */ new Map();
|
|
2382
|
+
for (const o of app.getControllersOverview()) {
|
|
2383
|
+
if (typeof o.computedPrefix !== "string") continue;
|
|
2384
|
+
const ctor = o.type;
|
|
2385
|
+
const binding = findReadableBinding(ctor);
|
|
2386
|
+
let recorded = bound.get(app)?.get(ctor);
|
|
2387
|
+
if (!recorded) {
|
|
2388
|
+
const any = boundAny.get(ctor);
|
|
2389
|
+
if (any?.size === 1) recorded = any;
|
|
2390
|
+
}
|
|
2391
|
+
const models = recorded?.size ? [...recorded] : binding?.model ? [[binding.model, void 0]] : [];
|
|
2392
|
+
if (models.length === 0) continue;
|
|
2393
|
+
if (models.length > 1) {
|
|
2394
|
+
warnOnce(app, s, `multi-model|${ctor.name}`, `[moost-db] ${ctor.name} serves several models; none of them gets a published value-help path (db.http.path).`);
|
|
2395
|
+
continue;
|
|
2396
|
+
}
|
|
2397
|
+
const [model, recordedCanonical] = models[0];
|
|
2398
|
+
const path = normalizeHttpPath(o.computedPrefix);
|
|
2399
|
+
const ownPrefix = o.meta?.controller?.prefix ?? "";
|
|
2400
|
+
const fromAnnotation = binding?.prefixSource === "annotation" && ownPrefix === binding.prefix && endsWithSegments(path, binding.prefix ?? "");
|
|
2401
|
+
const list = mounts.get(model) ?? [];
|
|
2402
|
+
list.push({
|
|
2403
|
+
ctor,
|
|
2404
|
+
path,
|
|
2405
|
+
canonical: binding?.canonical ?? recordedCanonical,
|
|
2406
|
+
fromAnnotation,
|
|
2407
|
+
parametric: isParametricPath(path)
|
|
2408
|
+
});
|
|
2409
|
+
mounts.set(model, list);
|
|
2410
|
+
}
|
|
2411
|
+
const paths = /* @__PURE__ */ new Map();
|
|
2412
|
+
for (const [model, list] of mounts) paths.set(model, resolveCanonical(app, s, model, list));
|
|
2413
|
+
return { paths };
|
|
2414
|
+
}
|
|
2415
|
+
const distinct = (list) => [...new Set(list.map((m) => m.path))];
|
|
2416
|
+
function resolveCanonical(app, s, model, list) {
|
|
2417
|
+
const cands = list.filter((m) => !m.parametric && m.canonical !== false);
|
|
2418
|
+
const marked = distinct(cands.filter((m) => m.canonical === true));
|
|
2419
|
+
if (marked.length === 1) return marked[0];
|
|
2420
|
+
if (marked.length > 1) return ambiguous(app, s, model, cands, "has several canonical mounts");
|
|
2421
|
+
const all = distinct(cands);
|
|
2422
|
+
if (all.length === 0) return null;
|
|
2423
|
+
if (all.length === 1) return all[0];
|
|
2424
|
+
const derived = distinct(cands.filter((m) => m.fromAnnotation));
|
|
2425
|
+
if (derived.length === 1) return derived[0];
|
|
2426
|
+
return ambiguous(app, s, model, cands, "is served by several controllers on different routes");
|
|
2427
|
+
}
|
|
2428
|
+
function ambiguous(app, s, model, cands, what) {
|
|
2429
|
+
const where = cands.map((m) => `${m.ctor.name} at ${m.path}`);
|
|
2430
|
+
const id = idOf(model);
|
|
2431
|
+
warnOnce(app, s, `${id}|${distinct(cands).toSorted().join(",")}|${what}`, `[moost-db] Model "${id}" ${what}:\n ${where.join(", ")}.\nIts value-help path (db.http.path) is left unset, so references to it render no picker.\nMark the controller that serves its value help with \`canonical: true\`\n(@TableController(${id}, { canonical: true }), or a value-help controller's \`{ canonical: true }\`\nconstructor option) or \`canonical: false\` on the others, or declare @db.http.path on the\nmodel and mount that controller without an explicit prefix.`);
|
|
2432
|
+
return null;
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* The scope of `app`: built lazily, rebuilt on publish or when a new
|
|
2436
|
+
* controller → model pair was recorded. A hit never polls the overview.
|
|
2437
|
+
* `undefined` when `app` has no controller overview (mock apps).
|
|
2438
|
+
*/
|
|
2439
|
+
function httpPathScopeFor(app) {
|
|
2440
|
+
if (!hasOverview(app)) return void 0;
|
|
2441
|
+
const s = stateOf(app);
|
|
2442
|
+
if (!s.scope || s.version !== boundVersion) {
|
|
2443
|
+
s.scope = buildScope(app, s);
|
|
2444
|
+
s.version = boundVersion;
|
|
2445
|
+
}
|
|
2446
|
+
return s.scope;
|
|
2447
|
+
}
|
|
2448
|
+
/** Models whose runtime metadata a publish wrote, with the value written. */
|
|
2449
|
+
const mirrored = /* @__PURE__ */ new Map();
|
|
2450
|
+
/** (Re)builds the scope of `app`, warns on ambiguity and writes the global compat mirror. */
|
|
2451
|
+
function publishDbHttpPaths(app) {
|
|
2452
|
+
if (!hasOverview(app)) return;
|
|
2453
|
+
const s = stateOf(app);
|
|
2454
|
+
s.published = true;
|
|
2455
|
+
s.scope = buildScope(app, s);
|
|
2456
|
+
s.version = boundVersion;
|
|
2457
|
+
const { paths } = s.scope;
|
|
2458
|
+
for (const [type, value] of paths) {
|
|
2459
|
+
captureDesignTime(type);
|
|
2460
|
+
if (value === null) type.metadata.delete(KEY);
|
|
2461
|
+
else type.metadata.set(KEY, value);
|
|
2462
|
+
mirrored.set(type, value ?? void 0);
|
|
2463
|
+
}
|
|
2464
|
+
for (const [type, written] of mirrored) {
|
|
2465
|
+
if (paths.has(type)) continue;
|
|
2466
|
+
if (type.metadata.get(KEY) === written) {
|
|
2467
|
+
const original = getDesignTime(type);
|
|
2468
|
+
if (original === void 0) type.metadata.delete(KEY);
|
|
2469
|
+
else type.metadata.set(KEY, original);
|
|
2470
|
+
}
|
|
2471
|
+
mirrored.delete(type);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
/** Lazy fallback for apps that never ran the hook (singletons reused across apps). */
|
|
2475
|
+
function ensurePublished(app) {
|
|
2476
|
+
if (!hasOverview(app)) return;
|
|
2477
|
+
if (!stateOf(app).published) publishDbHttpPaths(app);
|
|
2478
|
+
}
|
|
2479
|
+
/** Registers the publish hook once per app (no-op on mock apps without `addInitHook`). */
|
|
2480
|
+
function addPublishHook(app) {
|
|
2481
|
+
if (!app || typeof app.addInitHook !== "function") return;
|
|
2482
|
+
const s = stateOf(app);
|
|
2483
|
+
if (s.hookAdded) return;
|
|
2484
|
+
s.hookAdded = true;
|
|
2485
|
+
app.addInitHook((a) => publishDbHttpPaths(a), { priority: Number.MIN_SAFE_INTEGER });
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* The `annotationOverrides` contribution for `scope`: the only way a path
|
|
2489
|
+
* reaches the wire. Models the app does not serve pass through at their
|
|
2490
|
+
* design-time value (never another app's mirror).
|
|
2491
|
+
*/
|
|
2492
|
+
function httpPathOverrides(scope, type) {
|
|
2493
|
+
if (scope.paths.has(type)) return { [KEY]: scope.paths.get(type) ?? void 0 };
|
|
2494
|
+
if (mirrored.has(type) && type.metadata.get(KEY) === mirrored.get(type)) return { [KEY]: getDesignTime(type) };
|
|
2495
|
+
}
|
|
2496
|
+
//#endregion
|
|
2110
2497
|
//#region \0@oxc-project+runtime@0.133.0/helpers/esm/decorateMetadata.js
|
|
2111
2498
|
function __decorateMetadata(k, v) {
|
|
2112
2499
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
@@ -2139,19 +2526,29 @@ let AsReadableController = class AsReadableController {
|
|
|
2139
2526
|
app;
|
|
2140
2527
|
/** Cached serialized type definition (lazy, computed on first access). */
|
|
2141
2528
|
_serializedType;
|
|
2529
|
+
/** App scope every cache below was built for; a different scope drops them all ({@link enterScope}). */
|
|
2530
|
+
_cacheScope;
|
|
2531
|
+
/** Scope of the synchronous build in flight — instance-local, cleared in `finally`. */
|
|
2532
|
+
_buildScope;
|
|
2142
2533
|
/** Cached full meta response (computed lazily on first meta() call). */
|
|
2143
2534
|
_metaResponse;
|
|
2144
2535
|
/** {@link metaCacheKey} the cached response was built for. */
|
|
2145
2536
|
_metaResponseKey;
|
|
2146
2537
|
/** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
|
|
2147
2538
|
_formSchemas = /* @__PURE__ */ new Map();
|
|
2148
|
-
|
|
2539
|
+
/**
|
|
2540
|
+
* @param opts.canonical Multi-mount models only: marks (`true`) or excludes
|
|
2541
|
+
* (`false`) this controller as the model's published value-help route
|
|
2542
|
+
* (since 0.1.150). Decorator-bound controllers use the decorator option.
|
|
2543
|
+
*/
|
|
2544
|
+
constructor(boundType, controllerName, app, kindTag = "readable", opts) {
|
|
2149
2545
|
this.boundType = boundType;
|
|
2150
2546
|
this.controllerName = controllerName;
|
|
2151
2547
|
this.app = app;
|
|
2152
2548
|
this.logger = app.getLogger(`db [${controllerName}]`);
|
|
2153
2549
|
this.logger.info(`Initializing ${kindTag} controller`);
|
|
2154
|
-
this.
|
|
2550
|
+
recordBoundType(app, this.constructor, boundType, opts?.canonical);
|
|
2551
|
+
addPublishHook(app);
|
|
2155
2552
|
try {
|
|
2156
2553
|
const p = this.init();
|
|
2157
2554
|
if (p instanceof Promise) p.catch((error) => {
|
|
@@ -2162,22 +2559,40 @@ let AsReadableController = class AsReadableController {
|
|
|
2162
2559
|
throw error;
|
|
2163
2560
|
}
|
|
2164
2561
|
}
|
|
2165
|
-
/**
|
|
2166
|
-
|
|
2167
|
-
|
|
2562
|
+
/**
|
|
2563
|
+
* The app of the current event, through DI — never the one this
|
|
2564
|
+
* (singleton) instance was constructed in, which may be gone (a re-booted
|
|
2565
|
+
* app, a hot reload). Outside an event it is {@link app}.
|
|
2566
|
+
*
|
|
2567
|
+
* @since 0.1.150
|
|
2568
|
+
*/
|
|
2569
|
+
async currentApp() {
|
|
2168
2570
|
try {
|
|
2169
|
-
|
|
2170
|
-
} catch {
|
|
2171
|
-
|
|
2172
|
-
if (prefix) {
|
|
2173
|
-
if (!prefix.startsWith("/")) prefix = `/${prefix}`;
|
|
2174
|
-
this.boundType.metadata.set("db.http.path", prefix);
|
|
2571
|
+
return await useControllerContext().instantiate(Moost);
|
|
2572
|
+
} catch {
|
|
2573
|
+
return this.app;
|
|
2175
2574
|
}
|
|
2176
2575
|
}
|
|
2177
|
-
/** Lazily serializes the bound type (
|
|
2576
|
+
/** Lazily serializes the bound type (dropped with the other caches on a scope change). */
|
|
2178
2577
|
getSerializedType() {
|
|
2179
|
-
|
|
2180
|
-
|
|
2578
|
+
return this._serializedType ??= this.serializeForMeta(this.boundType);
|
|
2579
|
+
}
|
|
2580
|
+
/** Drops every scope-dependent cache when `scope` is not the one they were built for. */
|
|
2581
|
+
enterScope(scope) {
|
|
2582
|
+
if (scope === this._cacheScope) return;
|
|
2583
|
+
this._cacheScope = scope;
|
|
2584
|
+
this._serializedType = void 0;
|
|
2585
|
+
this._metaResponse = void 0;
|
|
2586
|
+
this._formSchemas.clear();
|
|
2587
|
+
}
|
|
2588
|
+
/** Runs a synchronous build with `scope`'s `db.http.path` overrides applied. */
|
|
2589
|
+
buildScoped(scope, build) {
|
|
2590
|
+
this._buildScope = scope;
|
|
2591
|
+
try {
|
|
2592
|
+
return build();
|
|
2593
|
+
} finally {
|
|
2594
|
+
this._buildScope = void 0;
|
|
2595
|
+
}
|
|
2181
2596
|
}
|
|
2182
2597
|
/**
|
|
2183
2598
|
* Serializes a type for the meta surfaces (`/meta`, `/meta/form/:name`)
|
|
@@ -2187,10 +2602,31 @@ let AsReadableController = class AsReadableController {
|
|
|
2187
2602
|
* serialize exactly as before.
|
|
2188
2603
|
*/
|
|
2189
2604
|
serializeForMeta(type) {
|
|
2190
|
-
const options = this.
|
|
2605
|
+
const options = this._effectiveSerializeOptions();
|
|
2191
2606
|
return applyTerminalRefs(serializeAnnotatedType(type, options), type, options);
|
|
2192
2607
|
}
|
|
2193
2608
|
/**
|
|
2609
|
+
* {@link getSerializeOptions} plus the current build's per-app `db.http.path`
|
|
2610
|
+
* overrides (a subclass's own `annotationOverrides` entry wins for the same key).
|
|
2611
|
+
*/
|
|
2612
|
+
_effectiveSerializeOptions() {
|
|
2613
|
+
const base = this.getSerializeOptions();
|
|
2614
|
+
const scope = this._buildScope;
|
|
2615
|
+
if (!scope) return base;
|
|
2616
|
+
const own = base.annotationOverrides;
|
|
2617
|
+
return {
|
|
2618
|
+
...base,
|
|
2619
|
+
annotationOverrides: (t) => {
|
|
2620
|
+
const ours = httpPathOverrides(scope, t);
|
|
2621
|
+
const theirs = own?.(t);
|
|
2622
|
+
return ours || theirs ? {
|
|
2623
|
+
...ours,
|
|
2624
|
+
...theirs
|
|
2625
|
+
} : void 0;
|
|
2626
|
+
}
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
/**
|
|
2194
2630
|
* One-time initialization hook. Override to seed data, register watchers, etc.
|
|
2195
2631
|
*/
|
|
2196
2632
|
init() {}
|
|
@@ -2388,7 +2824,53 @@ let AsReadableController = class AsReadableController {
|
|
|
2388
2824
|
*/
|
|
2389
2825
|
async meta() {
|
|
2390
2826
|
await this.parseRequest("meta");
|
|
2391
|
-
return this.resolveMeta();
|
|
2827
|
+
return this._withOwnHttpPath(await this.resolveMeta());
|
|
2828
|
+
}
|
|
2829
|
+
/**
|
|
2830
|
+
* The root of a controller's own `/meta` carries its own mount as
|
|
2831
|
+
* `db.http.path` (a secondary mount answers with its own route; references to
|
|
2832
|
+
* the model elsewhere carry the canonical one). Left alone for parametric
|
|
2833
|
+
* mounts and when the serialize options strip the key. Never mutates the
|
|
2834
|
+
* cached envelope.
|
|
2835
|
+
*/
|
|
2836
|
+
_withOwnHttpPath(meta) {
|
|
2837
|
+
const KEY = "db.http.path";
|
|
2838
|
+
let prefix;
|
|
2839
|
+
try {
|
|
2840
|
+
prefix = useControllerContext().getPrefix();
|
|
2841
|
+
} catch {
|
|
2842
|
+
return meta;
|
|
2843
|
+
}
|
|
2844
|
+
if (prefix === void 0 || prefix === null) return meta;
|
|
2845
|
+
const own = normalizeHttpPath(prefix);
|
|
2846
|
+
if (isParametricPath(own)) return meta;
|
|
2847
|
+
const options = this.getSerializeOptions();
|
|
2848
|
+
if (options.ignoreAnnotations?.includes(KEY)) return meta;
|
|
2849
|
+
let key = KEY;
|
|
2850
|
+
let value = own;
|
|
2851
|
+
if (options.processAnnotation) {
|
|
2852
|
+
const out = options.processAnnotation({
|
|
2853
|
+
key: KEY,
|
|
2854
|
+
value: own,
|
|
2855
|
+
path: [],
|
|
2856
|
+
kind: "object"
|
|
2857
|
+
});
|
|
2858
|
+
if (!out) return meta;
|
|
2859
|
+
key = out.key;
|
|
2860
|
+
value = out.value;
|
|
2861
|
+
}
|
|
2862
|
+
const type = meta.type;
|
|
2863
|
+
if (!type || type.metadata?.[key] === value) return meta;
|
|
2864
|
+
return {
|
|
2865
|
+
...meta,
|
|
2866
|
+
type: {
|
|
2867
|
+
...meta.type,
|
|
2868
|
+
metadata: {
|
|
2869
|
+
...type.metadata,
|
|
2870
|
+
[key]: value
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
};
|
|
2392
2874
|
}
|
|
2393
2875
|
/**
|
|
2394
2876
|
* The `/meta` payload for the current request — the cached envelope
|
|
@@ -2400,12 +2882,17 @@ let AsReadableController = class AsReadableController {
|
|
|
2400
2882
|
* @since 0.1.143
|
|
2401
2883
|
*/
|
|
2402
2884
|
resolveMeta() {
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
this.
|
|
2407
|
-
|
|
2408
|
-
|
|
2885
|
+
return this.currentApp().then((app) => {
|
|
2886
|
+
ensurePublished(app);
|
|
2887
|
+
const scope = httpPathScopeFor(app);
|
|
2888
|
+
this.enterScope(scope);
|
|
2889
|
+
const key = this.metaCacheKey();
|
|
2890
|
+
if (!this._metaResponse || key !== this._metaResponseKey) {
|
|
2891
|
+
this._metaResponse = this.buildScoped(scope, () => this.buildMetaResponse());
|
|
2892
|
+
this._metaResponseKey = key;
|
|
2893
|
+
}
|
|
2894
|
+
return this.applyMetaOverlay(this._metaResponse);
|
|
2895
|
+
});
|
|
2409
2896
|
}
|
|
2410
2897
|
/**
|
|
2411
2898
|
* Identity of the inputs the cached `/meta` envelope is built from — a new
|
|
@@ -2428,9 +2915,13 @@ let AsReadableController = class AsReadableController {
|
|
|
2428
2915
|
const envelopes = discoverActions(this.constructor, this.app, this.logger);
|
|
2429
2916
|
const formType = getControllerFormType(this.constructor, name);
|
|
2430
2917
|
if (!formType || !await this.authorizeForm(name, envelopes.filter((e) => e.info.inputForm === name).map((e) => e.info.name))) throw new HttpError(404, `Unknown form "${name}"`);
|
|
2918
|
+
const app = await this.currentApp();
|
|
2919
|
+
ensurePublished(app);
|
|
2920
|
+
const scope = httpPathScopeFor(app);
|
|
2921
|
+
this.enterScope(scope);
|
|
2431
2922
|
let cached = this._formSchemas.get(name);
|
|
2432
2923
|
if (!cached) {
|
|
2433
|
-
cached = this.serializeForMeta(formType);
|
|
2924
|
+
cached = this.buildScoped(scope, () => this.serializeForMeta(formType));
|
|
2434
2925
|
this._formSchemas.set(name, cached);
|
|
2435
2926
|
}
|
|
2436
2927
|
return cached;
|
|
@@ -2512,6 +3003,7 @@ AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMeta
|
|
|
2512
3003
|
Object,
|
|
2513
3004
|
String,
|
|
2514
3005
|
typeof Moost === "undefined" ? Object : Moost,
|
|
3006
|
+
Object,
|
|
2515
3007
|
Object
|
|
2516
3008
|
])], AsReadableController);
|
|
2517
3009
|
//#endregion
|
|
@@ -3922,9 +4414,14 @@ var DecorationPlanner = class {
|
|
|
3922
4414
|
if (memoize) memo.meta.set(meta, out);
|
|
3923
4415
|
return out;
|
|
3924
4416
|
}
|
|
3925
|
-
/** The declared interface serialized for `/meta.decorations`, once per class. */
|
|
3926
|
-
serialized(serialize) {
|
|
3927
|
-
|
|
4417
|
+
/** The declared interface serialized for `/meta.decorations`, once per class and app scope. */
|
|
4418
|
+
serialized(scope, serialize) {
|
|
4419
|
+
const { memo } = this.index;
|
|
4420
|
+
if (!memo.serialized || memo.serializedScope !== scope) {
|
|
4421
|
+
memo.serialized = serialize();
|
|
4422
|
+
memo.serializedScope = scope;
|
|
4423
|
+
}
|
|
4424
|
+
return memo.serialized;
|
|
3928
4425
|
}
|
|
3929
4426
|
};
|
|
3930
4427
|
/** Removes what a read must not carry — the unserved declared keys and the hook-only paths — from `rows`. */
|
|
@@ -4011,153 +4508,6 @@ function stripsNothing(read) {
|
|
|
4011
4508
|
return read === void 0 || read.dropKeys.length === 0 && read.dropPaths.length === 0;
|
|
4012
4509
|
}
|
|
4013
4510
|
//#endregion
|
|
4014
|
-
//#region src/decorators.ts
|
|
4015
|
-
/**
|
|
4016
|
-
* DI token under which the {@link AtscriptDbReadable} instance
|
|
4017
|
-
* is exposed to the readable controller's constructor via `@Inject`.
|
|
4018
|
-
*/
|
|
4019
|
-
const READABLE_DEF = "__atscript_db_readable_def";
|
|
4020
|
-
/**
|
|
4021
|
-
* DI token under which the {@link AtscriptDbTable} instance
|
|
4022
|
-
* is exposed to the controller's constructor via `@Inject`.
|
|
4023
|
-
* Points to the same token as READABLE_DEF for backward compatibility.
|
|
4024
|
-
*/
|
|
4025
|
-
const TABLE_DEF = READABLE_DEF;
|
|
4026
|
-
function normalizeOptions(prefixOrOptions) {
|
|
4027
|
-
if (typeof prefixOrOptions === "string") return { prefix: prefixOrOptions };
|
|
4028
|
-
return prefixOrOptions ?? {};
|
|
4029
|
-
}
|
|
4030
|
-
/**
|
|
4031
|
-
* Builds the shared binding metadata for a decorator invocation: classifies
|
|
4032
|
-
* the binding form, computes the static route prefix, and packages a uniform
|
|
4033
|
-
* `resolve()` used by both the DI provide factory and the base controller's
|
|
4034
|
-
* `super(app)` fallback.
|
|
4035
|
-
*/
|
|
4036
|
-
function buildBinding(binding, options, decoratorName) {
|
|
4037
|
-
if (isAnnotatedType(binding)) {
|
|
4038
|
-
const model = binding;
|
|
4039
|
-
const space = options.space ?? model.metadata.get("db.space");
|
|
4040
|
-
const prefix = options.prefix || model.metadata.get("db.http.path") || model.metadata.get("db.table") || model.metadata.get("db.view") || model.id || "";
|
|
4041
|
-
if (!prefix) throw new Error(`[moost-db] @${decoratorName}: cannot derive a route prefix from the model token (no @db.http.path / @db.table / @db.view and no type id). Pass an explicit prefix.`);
|
|
4042
|
-
return {
|
|
4043
|
-
meta: {
|
|
4044
|
-
model,
|
|
4045
|
-
resolve: () => resolveDbSpace(space).get(model)
|
|
4046
|
-
},
|
|
4047
|
-
prefix
|
|
4048
|
-
};
|
|
4049
|
-
}
|
|
4050
|
-
if (typeof binding === "function") {
|
|
4051
|
-
if (!options.prefix) throw new Error(`[moost-db] @${decoratorName}: the lazy factory form needs an explicit route prefix (the readable is not created until app.init()). Pass a prefix, or use the model token form which derives it from @db.http.path / @db.table.`);
|
|
4052
|
-
return {
|
|
4053
|
-
meta: { resolve: binding },
|
|
4054
|
-
prefix: options.prefix
|
|
4055
|
-
};
|
|
4056
|
-
}
|
|
4057
|
-
const readable = binding;
|
|
4058
|
-
const prefix = options.prefix || readable.type.metadata.get("db.http.path") || readable.tableName;
|
|
4059
|
-
return {
|
|
4060
|
-
meta: {
|
|
4061
|
-
model: readable.type,
|
|
4062
|
-
resolve: () => readable
|
|
4063
|
-
},
|
|
4064
|
-
prefix
|
|
4065
|
-
};
|
|
4066
|
-
}
|
|
4067
|
-
function bindReadableController(binding, prefixOrOptions, decoratorName) {
|
|
4068
|
-
const { meta, prefix } = buildBinding(binding, normalizeOptions(prefixOrOptions), decoratorName);
|
|
4069
|
-
return ApplyDecorators(getAtscriptDbMate().decorate((classMeta) => {
|
|
4070
|
-
classMeta.atscript_db_readable_binding = meta;
|
|
4071
|
-
return classMeta;
|
|
4072
|
-
}), Provide(READABLE_DEF, () => meta.resolve()), Controller(prefix), Inherit());
|
|
4073
|
-
}
|
|
4074
|
-
/**
|
|
4075
|
-
* Combines the boilerplate needed to turn an {@link AsDbController}
|
|
4076
|
-
* subclass into a fully wired HTTP controller for a given `@db.table` model.
|
|
4077
|
-
*
|
|
4078
|
-
* Internally applies three decorators:
|
|
4079
|
-
* 1. **Provide** — registers the readable resolver under {@link TABLE_DEF}.
|
|
4080
|
-
* 2. **Controller** — registers the class as a Moost HTTP controller
|
|
4081
|
-
* with an optional route prefix (defaults to `@db.http.path`, then the
|
|
4082
|
-
* table name).
|
|
4083
|
-
* 3. **Inherit** — copies metadata (routes, guards, etc.) from the
|
|
4084
|
-
* parent class so they stay active in the derived controller.
|
|
4085
|
-
*
|
|
4086
|
-
* All three binding forms are supported (see {@link TReadableBinding}):
|
|
4087
|
-
*
|
|
4088
|
-
* ```ts
|
|
4089
|
-
* @TableController(User) // model token (preferred)
|
|
4090
|
-
* @TableController(User, { space: "analytics" })
|
|
4091
|
-
* @TableController(() => db.getTable(User), "users") // lazy factory
|
|
4092
|
-
* @TableController(usersTable) // instance (legacy)
|
|
4093
|
-
* export class UsersController extends AsDbController<typeof User> {}
|
|
4094
|
-
* ```
|
|
4095
|
-
*
|
|
4096
|
-
* Token and factory forms resolve lazily at first controller instantiation
|
|
4097
|
-
* (during `app.init()`), so the `DbSpace` does not have to exist when the
|
|
4098
|
-
* controller module is imported. For the token form, register the space with
|
|
4099
|
-
* `provideDbSpace(db)` before `app.init()`.
|
|
4100
|
-
*
|
|
4101
|
-
* @param binding Model token, lazy factory, or {@link AtscriptDbTable} instance.
|
|
4102
|
-
* @param prefixOrOptions Route prefix string, or {@link TControllerBindingOptions}.
|
|
4103
|
-
*/
|
|
4104
|
-
const TableController = (binding, prefixOrOptions) => bindReadableController(binding, prefixOrOptions, "TableController");
|
|
4105
|
-
/**
|
|
4106
|
-
* Combines the boilerplate needed to turn an {@link AsDbReadableController}
|
|
4107
|
-
* subclass into a fully wired HTTP controller for a given `@db.view` or `@db.table` model.
|
|
4108
|
-
*
|
|
4109
|
-
* Accepts the same three binding forms as {@link TableController}.
|
|
4110
|
-
*
|
|
4111
|
-
* @param binding Model token, lazy factory, or {@link AtscriptDbReadable} instance.
|
|
4112
|
-
* @param prefixOrOptions Route prefix string, or {@link TControllerBindingOptions}.
|
|
4113
|
-
*
|
|
4114
|
-
* @example
|
|
4115
|
-
* ```ts
|
|
4116
|
-
* @ReadableController(ActiveTasks)
|
|
4117
|
-
* export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
|
|
4118
|
-
* ```
|
|
4119
|
-
*/
|
|
4120
|
-
const ReadableController = (binding, prefixOrOptions) => bindReadableController(binding, prefixOrOptions, "ReadableController");
|
|
4121
|
-
/**
|
|
4122
|
-
* Alias for {@link ReadableController} — use with view-backed controllers.
|
|
4123
|
-
*
|
|
4124
|
-
* @example
|
|
4125
|
-
* ```ts
|
|
4126
|
-
* @ViewController(ActiveTasks)
|
|
4127
|
-
* export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
|
|
4128
|
-
* ```
|
|
4129
|
-
*/
|
|
4130
|
-
const ViewController = ReadableController;
|
|
4131
|
-
/**
|
|
4132
|
-
* Finds the readable binding written by `@TableController` /
|
|
4133
|
-
* `@ReadableController` / `@ViewController` on a controller class, walking the
|
|
4134
|
-
* prototype chain so intermediate undecorated classes don't hide the binding
|
|
4135
|
-
* (nearest decorated ancestor wins).
|
|
4136
|
-
*/
|
|
4137
|
-
function findReadableBinding(ctor) {
|
|
4138
|
-
const mate = getAtscriptDbMate();
|
|
4139
|
-
let current = ctor;
|
|
4140
|
-
while (typeof current === "function") {
|
|
4141
|
-
const binding = mate.read(current)?.atscript_db_readable_binding;
|
|
4142
|
-
if (binding) return binding;
|
|
4143
|
-
current = Object.getPrototypeOf(current);
|
|
4144
|
-
}
|
|
4145
|
-
}
|
|
4146
|
-
/**
|
|
4147
|
-
* Resolves the readable bound to a controller class via
|
|
4148
|
-
* {@link findReadableBinding}, throwing with wiring guidance when none is
|
|
4149
|
-
* found.
|
|
4150
|
-
*
|
|
4151
|
-
* Used by {@link AsDbReadableController}'s constructor when `readable` is
|
|
4152
|
-
* `undefined` — i.e. a subclass with its own constructor called
|
|
4153
|
-
* `super(app)` instead of forwarding an injected instance.
|
|
4154
|
-
*/
|
|
4155
|
-
function resolveBoundReadable(ctor) {
|
|
4156
|
-
const binding = findReadableBinding(ctor);
|
|
4157
|
-
if (binding) return binding.resolve();
|
|
4158
|
-
throw new Error(`[moost-db] ${ctor?.name || "controller"}: no readable bound. Either pass a table/view to super(...), or decorate the class with @TableController / @ReadableController (model token, lazy factory, or instance form).`);
|
|
4159
|
-
}
|
|
4160
|
-
//#endregion
|
|
4161
4511
|
//#region src/permissions/crud-controls.ts
|
|
4162
4512
|
/**
|
|
4163
4513
|
* Static control whitelists per read op. Each list is the matching DTO's
|
|
@@ -4808,9 +5158,16 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4808
5158
|
}
|
|
4809
5159
|
return out;
|
|
4810
5160
|
}
|
|
5161
|
+
/**
|
|
5162
|
+
* The index visibility gate applies: {@link hasField} is overridden, or the
|
|
5163
|
+
* model has `@db.writeOnly` fields (never readable through a search hit).
|
|
5164
|
+
*/
|
|
5165
|
+
get _indexGateActive() {
|
|
5166
|
+
return this._hasFieldOverridden || this._writeOnlySet.size > 0;
|
|
5167
|
+
}
|
|
4811
5168
|
/** Every path `entry` reads is visible to this request. */
|
|
4812
5169
|
_indexVisible(entry) {
|
|
4813
|
-
return entry.fields.every(this.fieldVisibility.isVisible);
|
|
5170
|
+
return entry.fields.every((path) => this.fieldVisibility.isVisible(path) && selfOrAncestor(path, this._writeOnlySet) === void 0);
|
|
4814
5171
|
}
|
|
4815
5172
|
/**
|
|
4816
5173
|
* Native text search serves this request: the adapter searches natively
|
|
@@ -4829,7 +5186,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4829
5186
|
}
|
|
4830
5187
|
_resolveNativeSearch(controls) {
|
|
4831
5188
|
if (!this.readable.isSearchable()) return false;
|
|
4832
|
-
if (!this.
|
|
5189
|
+
if (!this._indexGateActive) return true;
|
|
4833
5190
|
if (typeof controls.$index === "string" && controls.$index) return true;
|
|
4834
5191
|
const def = this.indexFieldPaths().find((e) => e.type === "text" && e.isDefault);
|
|
4835
5192
|
return def === void 0 || this._indexVisible(def);
|
|
@@ -4843,7 +5200,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4843
5200
|
* nonexistent index (the core's wording).
|
|
4844
5201
|
*/
|
|
4845
5202
|
_checkIndexGate(controls) {
|
|
4846
|
-
if (!this.
|
|
5203
|
+
if (!this._indexGateActive) return void 0;
|
|
4847
5204
|
const name = typeof controls.$index === "string" ? controls.$index : void 0;
|
|
4848
5205
|
if (controls.$center !== void 0) {
|
|
4849
5206
|
const geoIndexes = this.indexFieldPaths().filter((e) => e.type === "geo");
|
|
@@ -4875,7 +5232,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4875
5232
|
* `@db.column.searchable` fallback when any of its fields is visible).
|
|
4876
5233
|
*/
|
|
4877
5234
|
_applyIndexVisibility(meta) {
|
|
4878
|
-
if (!this.
|
|
5235
|
+
if (!this._indexGateActive) return meta;
|
|
4879
5236
|
const entries = this.indexFieldPaths();
|
|
4880
5237
|
const visibleDefault = (type) => {
|
|
4881
5238
|
const def = entries.find((e) => e.type === type && e.isDefault);
|
|
@@ -5672,6 +6029,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5672
6029
|
for (const fd of this.readable.fieldDescriptors) {
|
|
5673
6030
|
if (fd.ignored) continue;
|
|
5674
6031
|
if (!fd.type?.metadata?.has?.("db.column.searchable")) continue;
|
|
6032
|
+
if (typeof searchMemberKind(fd.type) === "object") continue;
|
|
5675
6033
|
if (!this.capabilities.isPhysicallyFilterable(fd.path)) continue;
|
|
5676
6034
|
out.push(fd.path);
|
|
5677
6035
|
}
|
|
@@ -5744,6 +6102,14 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5744
6102
|
* nothing can apply is ignored. Resolvers (query targets, `resolveQuery`)
|
|
5745
6103
|
* refuse it — a subclass override that applies the term must return a new
|
|
5746
6104
|
* filter object.
|
|
6105
|
+
*
|
|
6106
|
+
* A string field matches when the term is a substring of its text; an
|
|
6107
|
+
* INTEGER field (`number.int` and its sizes, `@expect.int`,
|
|
6108
|
+
* `@db.default.increment`) when it is a substring of the number's decimal
|
|
6109
|
+
* text — `2946` finds `29461277` (since 0.1.150). To search numeric IDs
|
|
6110
|
+
* declare `@db.column.searchable` (fallback) or `@db.index.fulltext`
|
|
6111
|
+
* (native, exact number) on the integer field — override this hook only for
|
|
6112
|
+
* search rules the annotations cannot express.
|
|
5747
6113
|
*/
|
|
5748
6114
|
applySearchFallback(filter, controls) {
|
|
5749
6115
|
const term = controls.$search;
|
|
@@ -5782,6 +6148,51 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5782
6148
|
delete rest.$index;
|
|
5783
6149
|
return rest;
|
|
5784
6150
|
}
|
|
6151
|
+
/**
|
|
6152
|
+
* `$count` of a native text / vector search: the adapter's own count, so it
|
|
6153
|
+
* agrees with `/query` and `/pages`. A text search counts every match (the
|
|
6154
|
+
* single returned row is discarded); a vector search counts the nearest
|
|
6155
|
+
* neighbours up to `$limit` (default 1000), as `/query` returns them.
|
|
6156
|
+
*/
|
|
6157
|
+
async _countSearched(strategy, filter, controls, sealedControls) {
|
|
6158
|
+
const { $count: _count, ...sealed } = sealedControls;
|
|
6159
|
+
if (strategy.kind === "search") {
|
|
6160
|
+
const q = {
|
|
6161
|
+
filter,
|
|
6162
|
+
controls: this._searchReadControls(controls, sealed, sealed.$select, {
|
|
6163
|
+
$skip: void 0,
|
|
6164
|
+
$limit: 1
|
|
6165
|
+
})
|
|
6166
|
+
};
|
|
6167
|
+
const { count } = await this.readable.searchWithCount(strategy.term, q, strategy.index);
|
|
6168
|
+
return count;
|
|
6169
|
+
}
|
|
6170
|
+
const q = {
|
|
6171
|
+
filter,
|
|
6172
|
+
controls: this._searchReadControls(controls, sealed, sealed.$select, {
|
|
6173
|
+
$skip: void 0,
|
|
6174
|
+
$limit: controls.$limit || 1e3
|
|
6175
|
+
})
|
|
6176
|
+
};
|
|
6177
|
+
const { count } = await (strategy.vectorField ? this.readable.vectorSearchWithCount(strategy.vectorField, strategy.vector, q) : this.readable.vectorSearchWithCount(strategy.vector, q));
|
|
6178
|
+
return count;
|
|
6179
|
+
}
|
|
6180
|
+
/**
|
|
6181
|
+
* The controls a native search / vector read is run with: the request's
|
|
6182
|
+
* sealed controls (so every search control — `$fuzzy`, … — reaches the
|
|
6183
|
+
* adapter) plus the select, the threshold and the caller's paging. ONE
|
|
6184
|
+
* builder for `/query`, `/pages` and the native `$count`, so they can't
|
|
6185
|
+
* disagree about what a search matches.
|
|
6186
|
+
*/
|
|
6187
|
+
_searchReadControls(controls, sealed, select, paging) {
|
|
6188
|
+
const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
|
|
6189
|
+
return {
|
|
6190
|
+
...sealed,
|
|
6191
|
+
$select: select,
|
|
6192
|
+
...paging,
|
|
6193
|
+
$threshold: threshold
|
|
6194
|
+
};
|
|
6195
|
+
}
|
|
5785
6196
|
async _resolveReadStrategy(controls) {
|
|
5786
6197
|
const searchTerm = controls.$search;
|
|
5787
6198
|
const indexName = controls.$index;
|
|
@@ -5836,21 +6247,13 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5836
6247
|
if (decorated === void 0) return strip();
|
|
5837
6248
|
return Promise.resolve(decorated).then(strip);
|
|
5838
6249
|
}
|
|
5839
|
-
/**
|
|
5840
|
-
* The app of the current event, through DI — never the one this
|
|
5841
|
-
* (singleton) instance was constructed in, which may be gone (a re-booted
|
|
5842
|
-
* app, a hot reload).
|
|
5843
|
-
*/
|
|
5844
|
-
_currentApp() {
|
|
5845
|
-
return useControllerContext().instantiate(Moost);
|
|
5846
|
-
}
|
|
5847
6250
|
/** The class's `@DbActionsFrom` delegations, validated on first use (per app). */
|
|
5848
6251
|
async _delegations() {
|
|
5849
6252
|
if (!this._hasDelegations) return [];
|
|
5850
6253
|
return discoverDelegations({
|
|
5851
6254
|
ctor: this.constructor,
|
|
5852
6255
|
readable: this.readable,
|
|
5853
|
-
app: await this.
|
|
6256
|
+
app: await this.currentApp(),
|
|
5854
6257
|
logger: this.logger,
|
|
5855
6258
|
instantiate: (ctor) => useControllerContext().instantiate(ctor)
|
|
5856
6259
|
});
|
|
@@ -6208,24 +6611,22 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
6208
6611
|
}
|
|
6209
6612
|
const [transformedFilter, { sealed, finish }] = await Promise.all([this.transformFilter(clientFilter), this._projectRead(controls)]);
|
|
6210
6613
|
const filter = this.applySearchFallback(transformedFilter, controls);
|
|
6211
|
-
if (controls.$count)
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6614
|
+
if (controls.$count) {
|
|
6615
|
+
const strategy = await this._resolveReadStrategy(controls);
|
|
6616
|
+
if (strategy.kind === "plain") return this.readable.count({
|
|
6617
|
+
filter,
|
|
6618
|
+
controls: {
|
|
6619
|
+
...controls,
|
|
6620
|
+
$select: sealed.$select
|
|
6621
|
+
}
|
|
6622
|
+
});
|
|
6623
|
+
return this._countSearched(strategy, filter, controls, sealed);
|
|
6624
|
+
}
|
|
6218
6625
|
const projected = finish();
|
|
6219
6626
|
if (projected instanceof HttpError) return projected;
|
|
6220
|
-
const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
|
|
6221
6627
|
const queryObj = {
|
|
6222
6628
|
filter,
|
|
6223
|
-
controls: {
|
|
6224
|
-
...sealed,
|
|
6225
|
-
$select: projected.select,
|
|
6226
|
-
$limit: controls.$limit || 1e3,
|
|
6227
|
-
$threshold: threshold
|
|
6228
|
-
}
|
|
6629
|
+
controls: this._searchReadControls(controls, sealed, projected.select, { $limit: controls.$limit || 1e3 })
|
|
6229
6630
|
};
|
|
6230
6631
|
return (await this._runReadWithActions("query", queryObj, controls, projected, async (q, strategy) => {
|
|
6231
6632
|
switch (strategy.kind) {
|
|
@@ -6252,16 +6653,12 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
6252
6653
|
const filter = this.applySearchFallback(transformedFilter, controls);
|
|
6253
6654
|
const projected = finish();
|
|
6254
6655
|
if (projected instanceof HttpError) return projected;
|
|
6255
|
-
const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
|
|
6256
6656
|
const query = {
|
|
6257
6657
|
filter,
|
|
6258
|
-
controls: {
|
|
6259
|
-
...sealed,
|
|
6260
|
-
$select: projected.select,
|
|
6658
|
+
controls: this._searchReadControls(controls, sealed, projected.select, {
|
|
6261
6659
|
$skip: skip,
|
|
6262
|
-
$limit: size
|
|
6263
|
-
|
|
6264
|
-
}
|
|
6660
|
+
$limit: size
|
|
6661
|
+
})
|
|
6265
6662
|
};
|
|
6266
6663
|
const result = await this._runReadWithActions("pages", query, controls, projected, async (q, strategy) => {
|
|
6267
6664
|
switch (strategy.kind) {
|
|
@@ -6742,7 +7139,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
6742
7139
|
relations,
|
|
6743
7140
|
fields,
|
|
6744
7141
|
type: this.getSerializedType(),
|
|
6745
|
-
...this._decorations && { decorations: this._planner.serialized(() => this.serializeForMeta(this._decorations.type)) },
|
|
7142
|
+
...this._decorations && { decorations: this._planner.serialized(this._buildScope, () => this.serializeForMeta(this._decorations.type)) },
|
|
6746
7143
|
actions: this.buildActions(),
|
|
6747
7144
|
crud: this.buildCrud(),
|
|
6748
7145
|
versionColumn: this.readable.versionColumn,
|
|
@@ -7025,7 +7422,8 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
|
|
|
7025
7422
|
* Bulk auto-lift: each item carries its own `version` → `$cas`.
|
|
7026
7423
|
* NOTE: per-item conflict disambiguation in the response body is deferred
|
|
7027
7424
|
* (§6.4) — the aggregate `{ matchedCount, modifiedCount }` surfaces partial
|
|
7028
|
-
* application; callers can detect mismatches via `
|
|
7425
|
+
* application; callers can detect mismatches via `matchedCount < N` (`modifiedCount` may
|
|
7426
|
+
* be lower for unchanged values, e.g. a version-exempt patch on MySQL / Mongo).
|
|
7029
7427
|
*/
|
|
7030
7428
|
_resolveBulkCas(rows, versionColumn) {
|
|
7031
7429
|
if (versionColumn === void 0) return;
|
|
@@ -7237,8 +7635,8 @@ let AsValueHelpController = class AsValueHelpController extends AsReadableContro
|
|
|
7237
7635
|
searchableFields;
|
|
7238
7636
|
/** The `@meta.id` field name on the bound interface, if any. */
|
|
7239
7637
|
primaryKey;
|
|
7240
|
-
constructor(boundType, controllerName, app) {
|
|
7241
|
-
super(boundType, controllerName, app, "value-help");
|
|
7638
|
+
constructor(boundType, controllerName, app, opts) {
|
|
7639
|
+
super(boundType, controllerName, app, "value-help", opts);
|
|
7242
7640
|
assertNoValueHelpActions(this.constructor);
|
|
7243
7641
|
const fieldMeta = /* @__PURE__ */ new Map();
|
|
7244
7642
|
const explicitlySearchable = [];
|
|
@@ -7454,7 +7852,8 @@ __decorate([
|
|
|
7454
7852
|
AsValueHelpController = __decorate([Inherit(), __decorateMetadata("design:paramtypes", [
|
|
7455
7853
|
Object,
|
|
7456
7854
|
String,
|
|
7457
|
-
typeof Moost === "undefined" ? Object : Moost
|
|
7855
|
+
typeof Moost === "undefined" ? Object : Moost,
|
|
7856
|
+
Object
|
|
7458
7857
|
])], AsValueHelpController);
|
|
7459
7858
|
registerAsValueHelpController(AsValueHelpController);
|
|
7460
7859
|
//#endregion
|
|
@@ -7462,9 +7861,9 @@ registerAsValueHelpController(AsValueHelpController);
|
|
|
7462
7861
|
let AsJsonValueHelpController = class AsJsonValueHelpController extends AsValueHelpController {
|
|
7463
7862
|
rows;
|
|
7464
7863
|
_pkIndex;
|
|
7465
|
-
constructor(boundType, rows, app, controllerName) {
|
|
7864
|
+
constructor(boundType, rows, app, controllerName, opts) {
|
|
7466
7865
|
const name = controllerName || boundType.metadata.get("db.table") || "value-help";
|
|
7467
|
-
super(boundType, name, app);
|
|
7866
|
+
super(boundType, name, app, opts);
|
|
7468
7867
|
this.rows = rows;
|
|
7469
7868
|
if (this.primaryKey) {
|
|
7470
7869
|
const pk = this.primaryKey;
|
|
@@ -7539,7 +7938,8 @@ AsJsonValueHelpController = __decorate([Inherit(), __decorateMetadata("design:pa
|
|
|
7539
7938
|
Object,
|
|
7540
7939
|
Array,
|
|
7541
7940
|
typeof Moost === "undefined" ? Object : Moost,
|
|
7542
|
-
String
|
|
7941
|
+
String,
|
|
7942
|
+
Object
|
|
7543
7943
|
])], AsJsonValueHelpController);
|
|
7544
7944
|
//#endregion
|
|
7545
7945
|
//#region src/assert-exposed.ts
|
|
@@ -7583,7 +7983,7 @@ function assertExposed(app, models, options) {
|
|
|
7583
7983
|
const missing = [];
|
|
7584
7984
|
for (const model of models) {
|
|
7585
7985
|
if (aliasTargetOf(model)) continue;
|
|
7586
|
-
const httpPath = model
|
|
7986
|
+
const httpPath = designTimeHttpPath(model);
|
|
7587
7987
|
if (!auditAll && httpPath === void 0) continue;
|
|
7588
7988
|
if (excluded.has(model) || exposed.has(model)) continue;
|
|
7589
7989
|
missing.push(model);
|