@atscript/moost-db 0.1.148 → 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 CHANGED
@@ -71,6 +71,13 @@ function insightError(insights, message) {
71
71
  const entry = insightPaths.get(insights);
72
72
  return entry?.message === message ? badRequest(entry.path, message) : new _moostjs_event_http.HttpError(400, message);
73
73
  }
74
+ /**
75
+ * The 400 envelope of a `ValidatorError`; `undefined` for any other error.
76
+ * @internal Not part of the public API (not re-exported from the barrel).
77
+ */
78
+ function validatorErrorToHttp(error) {
79
+ return error instanceof _atscript_typescript_utils.ValidatorError ? errorEnvelope(400, error.message, error.errors) : void 0;
80
+ }
74
81
  //#endregion
75
82
  //#region src/validation-interceptor.ts
76
83
  const dbErrorCodeToStatus = {
@@ -81,7 +88,8 @@ const dbErrorCodeToStatus = {
81
88
  BUCKET_TZ_UNAVAILABLE: 501
82
89
  };
83
90
  function transformValidationError(error, reply) {
84
- if (error instanceof _atscript_typescript_utils.ValidatorError) reply(errorEnvelope(400, error.message, error.errors));
91
+ const validation = validatorErrorToHttp(error);
92
+ if (validation) reply(validation);
85
93
  else if (error instanceof _atscript_db.DbError) reply(errorEnvelope(dbErrorCodeToStatus[error.code] ?? 400, error.message, error.errors));
86
94
  }
87
95
  const validationErrorTransform = () => (0, moost.defineInterceptor)({ error: transformValidationError }, moost.TInterceptorPriority.BEFORE_ALL);
@@ -2100,6 +2108,393 @@ function walk(node, def, shallow) {
2100
2108
  }
2101
2109
  }
2102
2110
  //#endregion
2111
+ //#region src/http-path-design-time.ts
2112
+ const KEY$1 = "db.http.path";
2113
+ /** The `@db.http.path` value a model carried before moost-db first wrote its published path. */
2114
+ const designTime = /* @__PURE__ */ new WeakMap();
2115
+ /**
2116
+ * Remembers the model's own `@db.http.path` (or its absence) before the first
2117
+ * published path is mirrored into its runtime metadata. Idempotent.
2118
+ *
2119
+ * @since 0.1.150
2120
+ */
2121
+ function captureDesignTime(type) {
2122
+ if (!designTime.has(type)) designTime.set(type, type.metadata.get(KEY$1));
2123
+ }
2124
+ /**
2125
+ * The route hint a model declares with `@db.http.path` — never the path
2126
+ * moost-db mirrored into the runtime metadata afterwards. Decorators and
2127
+ * `assertExposed` read this.
2128
+ *
2129
+ * @since 0.1.150
2130
+ */
2131
+ function designTimeHttpPath(type) {
2132
+ return designTime.has(type) ? designTime.get(type) : type.metadata.get(KEY$1);
2133
+ }
2134
+ /** The captured design-time value (`undefined` when none was captured or declared). */
2135
+ function getDesignTime(type) {
2136
+ return designTime.get(type);
2137
+ }
2138
+ //#endregion
2139
+ //#region src/decorators.ts
2140
+ /**
2141
+ * DI token under which the {@link AtscriptDbReadable} instance
2142
+ * is exposed to the readable controller's constructor via `@Inject`.
2143
+ */
2144
+ const READABLE_DEF = "__atscript_db_readable_def";
2145
+ /**
2146
+ * DI token under which the {@link AtscriptDbTable} instance
2147
+ * is exposed to the controller's constructor via `@Inject`.
2148
+ * Points to the same token as READABLE_DEF for backward compatibility.
2149
+ */
2150
+ const TABLE_DEF = READABLE_DEF;
2151
+ function normalizeOptions(prefixOrOptions) {
2152
+ if (typeof prefixOrOptions === "string") return { prefix: prefixOrOptions };
2153
+ return prefixOrOptions ?? {};
2154
+ }
2155
+ /**
2156
+ * Builds the shared binding metadata for a decorator invocation: classifies
2157
+ * the binding form, computes the static route prefix, and packages a uniform
2158
+ * `resolve()` used by both the DI provide factory and the base controller's
2159
+ * `super(app)` fallback.
2160
+ */
2161
+ function buildBinding(binding, options, decoratorName) {
2162
+ const canonical = options.canonical;
2163
+ if ((0, _atscript_typescript_utils.isAnnotatedType)(binding)) {
2164
+ const model = binding;
2165
+ const space = options.space ?? model.metadata.get("db.space");
2166
+ const hinted = designTimeHttpPath(model);
2167
+ const prefix = options.prefix || hinted || model.metadata.get("db.table") || model.metadata.get("db.view") || model.id || "";
2168
+ const prefixSource = options.prefix ? "option" : hinted ? "annotation" : "name";
2169
+ 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.`);
2170
+ return {
2171
+ meta: {
2172
+ model,
2173
+ resolve: () => require_db_space_registry.resolveDbSpace(space).get(model),
2174
+ prefix,
2175
+ prefixSource,
2176
+ canonical
2177
+ },
2178
+ prefix
2179
+ };
2180
+ }
2181
+ if (typeof binding === "function") {
2182
+ 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.`);
2183
+ return {
2184
+ meta: {
2185
+ resolve: binding,
2186
+ prefix: options.prefix,
2187
+ prefixSource: "option",
2188
+ canonical
2189
+ },
2190
+ prefix: options.prefix
2191
+ };
2192
+ }
2193
+ const readable = binding;
2194
+ const hinted = designTimeHttpPath(readable.type);
2195
+ const prefix = options.prefix || hinted || readable.tableName;
2196
+ return {
2197
+ meta: {
2198
+ model: readable.type,
2199
+ resolve: () => readable,
2200
+ prefix,
2201
+ prefixSource: options.prefix ? "option" : hinted ? "annotation" : "name",
2202
+ canonical
2203
+ },
2204
+ prefix
2205
+ };
2206
+ }
2207
+ function bindReadableController(binding, prefixOrOptions, decoratorName) {
2208
+ const { meta, prefix } = buildBinding(binding, normalizeOptions(prefixOrOptions), decoratorName);
2209
+ return (0, moost.ApplyDecorators)(getAtscriptDbMate().decorate((classMeta) => {
2210
+ classMeta.atscript_db_readable_binding = meta;
2211
+ return classMeta;
2212
+ }), (0, moost.Provide)(READABLE_DEF, () => meta.resolve()), (0, moost.Controller)(prefix), (0, moost.Inherit)());
2213
+ }
2214
+ /**
2215
+ * Combines the boilerplate needed to turn an {@link AsDbController}
2216
+ * subclass into a fully wired HTTP controller for a given `@db.table` model.
2217
+ *
2218
+ * Internally applies three decorators:
2219
+ * 1. **Provide** — registers the readable resolver under {@link TABLE_DEF}.
2220
+ * 2. **Controller** — registers the class as a Moost HTTP controller
2221
+ * with an optional route prefix (defaults to `@db.http.path`, then the
2222
+ * table name).
2223
+ * 3. **Inherit** — copies metadata (routes, guards, etc.) from the
2224
+ * parent class so they stay active in the derived controller.
2225
+ *
2226
+ * All three binding forms are supported (see {@link TReadableBinding}):
2227
+ *
2228
+ * ```ts
2229
+ * ‎@TableController(User) // model token (preferred)
2230
+ * ‎@TableController(User, { space: "analytics" })
2231
+ * ‎@TableController(() => db.getTable(User), "users") // lazy factory
2232
+ * ‎@TableController(usersTable) // instance (legacy)
2233
+ * export class UsersController extends AsDbController<typeof User> {}
2234
+ * ```
2235
+ *
2236
+ * Token and factory forms resolve lazily at first controller instantiation
2237
+ * (during `app.init()`), so the `DbSpace` does not have to exist when the
2238
+ * controller module is imported. For the token form, register the space with
2239
+ * `provideDbSpace(db)` before `app.init()`.
2240
+ *
2241
+ * @param binding Model token, lazy factory, or {@link AtscriptDbTable} instance.
2242
+ * @param prefixOrOptions Route prefix string, or {@link TControllerBindingOptions}.
2243
+ */
2244
+ const TableController = (binding, prefixOrOptions) => bindReadableController(binding, prefixOrOptions, "TableController");
2245
+ /**
2246
+ * Combines the boilerplate needed to turn an {@link AsDbReadableController}
2247
+ * subclass into a fully wired HTTP controller for a given `@db.view` or `@db.table` model.
2248
+ *
2249
+ * Accepts the same three binding forms as {@link TableController}.
2250
+ *
2251
+ * @param binding Model token, lazy factory, or {@link AtscriptDbReadable} instance.
2252
+ * @param prefixOrOptions Route prefix string, or {@link TControllerBindingOptions}.
2253
+ *
2254
+ * @example
2255
+ * ```ts
2256
+ * ‎@ReadableController(ActiveTasks)
2257
+ * export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
2258
+ * ```
2259
+ */
2260
+ const ReadableController = (binding, prefixOrOptions) => bindReadableController(binding, prefixOrOptions, "ReadableController");
2261
+ /**
2262
+ * Alias for {@link ReadableController} — use with view-backed controllers.
2263
+ *
2264
+ * @example
2265
+ * ```ts
2266
+ * ‎@ViewController(ActiveTasks)
2267
+ * export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
2268
+ * ```
2269
+ */
2270
+ const ViewController = ReadableController;
2271
+ /**
2272
+ * Finds the readable binding written by `@TableController` /
2273
+ * `@ReadableController` / `@ViewController` on a controller class, walking the
2274
+ * prototype chain so intermediate undecorated classes don't hide the binding
2275
+ * (nearest decorated ancestor wins).
2276
+ */
2277
+ function findReadableBinding(ctor) {
2278
+ const mate = getAtscriptDbMate();
2279
+ let current = ctor;
2280
+ while (typeof current === "function") {
2281
+ const binding = mate.read(current)?.atscript_db_readable_binding;
2282
+ if (binding) return binding;
2283
+ current = Object.getPrototypeOf(current);
2284
+ }
2285
+ }
2286
+ /**
2287
+ * Resolves the readable bound to a controller class via
2288
+ * {@link findReadableBinding}, throwing with wiring guidance when none is
2289
+ * found.
2290
+ *
2291
+ * Used by {@link AsDbReadableController}'s constructor when `readable` is
2292
+ * `undefined` — i.e. a subclass with its own constructor called
2293
+ * `super(app)` instead of forwarding an injected instance.
2294
+ */
2295
+ function resolveBoundReadable(ctor) {
2296
+ const binding = findReadableBinding(ctor);
2297
+ if (binding) return binding.resolve();
2298
+ 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).`);
2299
+ }
2300
+ //#endregion
2301
+ //#region src/http-path.ts
2302
+ const KEY = "db.http.path";
2303
+ /**
2304
+ * Normalizes a controller prefix into a public path: one leading slash, no
2305
+ * empty segments, no trailing slash.
2306
+ *
2307
+ * @since 0.1.150
2308
+ */
2309
+ function normalizeHttpPath(prefix) {
2310
+ return `/${prefix.split("/").filter(Boolean).join("/")}`;
2311
+ }
2312
+ /** A path with a route parameter or a wildcard cannot be a static picker URL. */
2313
+ function isParametricPath(path) {
2314
+ return path.split("/").some((s) => s.startsWith(":") || s.includes("*"));
2315
+ }
2316
+ /** Per app (the constructor's `app` argument): another app's construction never alters this app's records. */
2317
+ const bound = /* @__PURE__ */ new WeakMap();
2318
+ /**
2319
+ * Process-wide records per ctor: the fallback for an app that never
2320
+ * constructed the class itself (a DI singleton reused across apps), read only
2321
+ * when it holds exactly one model.
2322
+ */
2323
+ const boundAny = /* @__PURE__ */ new WeakMap();
2324
+ let boundVersion = 0;
2325
+ /**
2326
+ * Records that `ctor` serves `type` inside `app` (called by the readable base constructor).
2327
+ * Pure bookkeeping — nothing is written to the model. Only a NEW pair bumps
2328
+ * the version that invalidates per-app scopes.
2329
+ */
2330
+ function recordBoundType(app, ctor, type, canonical) {
2331
+ let ofApp = bound.get(app);
2332
+ if (!ofApp) {
2333
+ ofApp = /* @__PURE__ */ new WeakMap();
2334
+ bound.set(app, ofApp);
2335
+ }
2336
+ let models = ofApp.get(ctor);
2337
+ if (!models) {
2338
+ models = /* @__PURE__ */ new Map();
2339
+ ofApp.set(ctor, models);
2340
+ }
2341
+ let any = boundAny.get(ctor);
2342
+ if (!any) {
2343
+ any = /* @__PURE__ */ new Map();
2344
+ boundAny.set(ctor, any);
2345
+ }
2346
+ any.set(type, canonical);
2347
+ if (!models.has(type) || models.get(type) !== canonical) {
2348
+ models.set(type, canonical);
2349
+ boundVersion++;
2350
+ }
2351
+ }
2352
+ const apps = /* @__PURE__ */ new WeakMap();
2353
+ function stateOf(app) {
2354
+ let s = apps.get(app);
2355
+ if (!s) {
2356
+ s = {
2357
+ version: -1,
2358
+ published: false,
2359
+ hookAdded: false,
2360
+ warned: /* @__PURE__ */ new Set()
2361
+ };
2362
+ apps.set(app, s);
2363
+ }
2364
+ return s;
2365
+ }
2366
+ const idOf = (type) => type.id ?? "(unnamed model)";
2367
+ const hasOverview = (app) => !!app && typeof app.getControllersOverview === "function";
2368
+ function endsWithSegments(path, suffix) {
2369
+ const a = path.split("/").filter(Boolean);
2370
+ const b = suffix.split("/").filter(Boolean);
2371
+ if (b.length > a.length) return false;
2372
+ return b.every((seg, i) => a[a.length - b.length + i] === seg);
2373
+ }
2374
+ function warnOnce(app, s, key, message) {
2375
+ if (s.warned.has(key)) return;
2376
+ s.warned.add(key);
2377
+ try {
2378
+ app.getLogger?.("moost-db")?.warn(message);
2379
+ } catch {}
2380
+ }
2381
+ function buildScope(app, s) {
2382
+ const mounts = /* @__PURE__ */ new Map();
2383
+ for (const o of app.getControllersOverview()) {
2384
+ if (typeof o.computedPrefix !== "string") continue;
2385
+ const ctor = o.type;
2386
+ const binding = findReadableBinding(ctor);
2387
+ let recorded = bound.get(app)?.get(ctor);
2388
+ if (!recorded) {
2389
+ const any = boundAny.get(ctor);
2390
+ if (any?.size === 1) recorded = any;
2391
+ }
2392
+ const models = recorded?.size ? [...recorded] : binding?.model ? [[binding.model, void 0]] : [];
2393
+ if (models.length === 0) continue;
2394
+ if (models.length > 1) {
2395
+ 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).`);
2396
+ continue;
2397
+ }
2398
+ const [model, recordedCanonical] = models[0];
2399
+ const path = normalizeHttpPath(o.computedPrefix);
2400
+ const ownPrefix = o.meta?.controller?.prefix ?? "";
2401
+ const fromAnnotation = binding?.prefixSource === "annotation" && ownPrefix === binding.prefix && endsWithSegments(path, binding.prefix ?? "");
2402
+ const list = mounts.get(model) ?? [];
2403
+ list.push({
2404
+ ctor,
2405
+ path,
2406
+ canonical: binding?.canonical ?? recordedCanonical,
2407
+ fromAnnotation,
2408
+ parametric: isParametricPath(path)
2409
+ });
2410
+ mounts.set(model, list);
2411
+ }
2412
+ const paths = /* @__PURE__ */ new Map();
2413
+ for (const [model, list] of mounts) paths.set(model, resolveCanonical(app, s, model, list));
2414
+ return { paths };
2415
+ }
2416
+ const distinct = (list) => [...new Set(list.map((m) => m.path))];
2417
+ function resolveCanonical(app, s, model, list) {
2418
+ const cands = list.filter((m) => !m.parametric && m.canonical !== false);
2419
+ const marked = distinct(cands.filter((m) => m.canonical === true));
2420
+ if (marked.length === 1) return marked[0];
2421
+ if (marked.length > 1) return ambiguous(app, s, model, cands, "has several canonical mounts");
2422
+ const all = distinct(cands);
2423
+ if (all.length === 0) return null;
2424
+ if (all.length === 1) return all[0];
2425
+ const derived = distinct(cands.filter((m) => m.fromAnnotation));
2426
+ if (derived.length === 1) return derived[0];
2427
+ return ambiguous(app, s, model, cands, "is served by several controllers on different routes");
2428
+ }
2429
+ function ambiguous(app, s, model, cands, what) {
2430
+ const where = cands.map((m) => `${m.ctor.name} at ${m.path}`);
2431
+ const id = idOf(model);
2432
+ 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.`);
2433
+ return null;
2434
+ }
2435
+ /**
2436
+ * The scope of `app`: built lazily, rebuilt on publish or when a new
2437
+ * controller → model pair was recorded. A hit never polls the overview.
2438
+ * `undefined` when `app` has no controller overview (mock apps).
2439
+ */
2440
+ function httpPathScopeFor(app) {
2441
+ if (!hasOverview(app)) return void 0;
2442
+ const s = stateOf(app);
2443
+ if (!s.scope || s.version !== boundVersion) {
2444
+ s.scope = buildScope(app, s);
2445
+ s.version = boundVersion;
2446
+ }
2447
+ return s.scope;
2448
+ }
2449
+ /** Models whose runtime metadata a publish wrote, with the value written. */
2450
+ const mirrored = /* @__PURE__ */ new Map();
2451
+ /** (Re)builds the scope of `app`, warns on ambiguity and writes the global compat mirror. */
2452
+ function publishDbHttpPaths(app) {
2453
+ if (!hasOverview(app)) return;
2454
+ const s = stateOf(app);
2455
+ s.published = true;
2456
+ s.scope = buildScope(app, s);
2457
+ s.version = boundVersion;
2458
+ const { paths } = s.scope;
2459
+ for (const [type, value] of paths) {
2460
+ captureDesignTime(type);
2461
+ if (value === null) type.metadata.delete(KEY);
2462
+ else type.metadata.set(KEY, value);
2463
+ mirrored.set(type, value ?? void 0);
2464
+ }
2465
+ for (const [type, written] of mirrored) {
2466
+ if (paths.has(type)) continue;
2467
+ if (type.metadata.get(KEY) === written) {
2468
+ const original = getDesignTime(type);
2469
+ if (original === void 0) type.metadata.delete(KEY);
2470
+ else type.metadata.set(KEY, original);
2471
+ }
2472
+ mirrored.delete(type);
2473
+ }
2474
+ }
2475
+ /** Lazy fallback for apps that never ran the hook (singletons reused across apps). */
2476
+ function ensurePublished(app) {
2477
+ if (!hasOverview(app)) return;
2478
+ if (!stateOf(app).published) publishDbHttpPaths(app);
2479
+ }
2480
+ /** Registers the publish hook once per app (no-op on mock apps without `addInitHook`). */
2481
+ function addPublishHook(app) {
2482
+ if (!app || typeof app.addInitHook !== "function") return;
2483
+ const s = stateOf(app);
2484
+ if (s.hookAdded) return;
2485
+ s.hookAdded = true;
2486
+ app.addInitHook((a) => publishDbHttpPaths(a), { priority: Number.MIN_SAFE_INTEGER });
2487
+ }
2488
+ /**
2489
+ * The `annotationOverrides` contribution for `scope`: the only way a path
2490
+ * reaches the wire. Models the app does not serve pass through at their
2491
+ * design-time value (never another app's mirror).
2492
+ */
2493
+ function httpPathOverrides(scope, type) {
2494
+ if (scope.paths.has(type)) return { [KEY]: scope.paths.get(type) ?? void 0 };
2495
+ if (mirrored.has(type) && type.metadata.get(KEY) === mirrored.get(type)) return { [KEY]: getDesignTime(type) };
2496
+ }
2497
+ //#endregion
2103
2498
  //#region \0@oxc-project+runtime@0.133.0/helpers/esm/decorateMetadata.js
2104
2499
  function __decorateMetadata(k, v) {
2105
2500
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
@@ -2132,19 +2527,29 @@ let AsReadableController = class AsReadableController {
2132
2527
  app;
2133
2528
  /** Cached serialized type definition (lazy, computed on first access). */
2134
2529
  _serializedType;
2530
+ /** App scope every cache below was built for; a different scope drops them all ({@link enterScope}). */
2531
+ _cacheScope;
2532
+ /** Scope of the synchronous build in flight — instance-local, cleared in `finally`. */
2533
+ _buildScope;
2135
2534
  /** Cached full meta response (computed lazily on first meta() call). */
2136
2535
  _metaResponse;
2137
2536
  /** {@link metaCacheKey} the cached response was built for. */
2138
2537
  _metaResponseKey;
2139
2538
  /** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
2140
2539
  _formSchemas = /* @__PURE__ */ new Map();
2141
- constructor(boundType, controllerName, app, kindTag = "readable") {
2540
+ /**
2541
+ * @param opts.canonical Multi-mount models only: marks (`true`) or excludes
2542
+ * (`false`) this controller as the model's published value-help route
2543
+ * (since 0.1.150). Decorator-bound controllers use the decorator option.
2544
+ */
2545
+ constructor(boundType, controllerName, app, kindTag = "readable", opts) {
2142
2546
  this.boundType = boundType;
2143
2547
  this.controllerName = controllerName;
2144
2548
  this.app = app;
2145
2549
  this.logger = app.getLogger(`db [${controllerName}]`);
2146
2550
  this.logger.info(`Initializing ${kindTag} controller`);
2147
- this._resolveHttpPath();
2551
+ recordBoundType(app, this.constructor, boundType, opts?.canonical);
2552
+ addPublishHook(app);
2148
2553
  try {
2149
2554
  const p = this.init();
2150
2555
  if (p instanceof Promise) p.catch((error) => {
@@ -2155,22 +2560,40 @@ let AsReadableController = class AsReadableController {
2155
2560
  throw error;
2156
2561
  }
2157
2562
  }
2158
- /** Sets @db.http.path on the type metadata from the controller's computed prefix. */
2159
- _resolveHttpPath() {
2160
- let prefix;
2563
+ /**
2564
+ * The app of the current event, through DI — never the one this
2565
+ * (singleton) instance was constructed in, which may be gone (a re-booted
2566
+ * app, a hot reload). Outside an event it is {@link app}.
2567
+ *
2568
+ * @since 0.1.150
2569
+ */
2570
+ async currentApp() {
2161
2571
  try {
2162
- prefix = (0, moost.useControllerContext)().getPrefix();
2163
- } catch {}
2164
- if (!prefix) prefix = (this.app.getControllersOverview?.()?.find((o) => o.type === this.constructor))?.computedPrefix;
2165
- if (prefix) {
2166
- if (!prefix.startsWith("/")) prefix = `/${prefix}`;
2167
- this.boundType.metadata.set("db.http.path", prefix);
2572
+ return await (0, moost.useControllerContext)().instantiate(moost.Moost);
2573
+ } catch {
2574
+ return this.app;
2168
2575
  }
2169
2576
  }
2170
- /** Lazily serializes the bound type (after all controllers have set @db.http.path). */
2577
+ /** Lazily serializes the bound type (dropped with the other caches on a scope change). */
2171
2578
  getSerializedType() {
2172
- if (!this._serializedType) this._serializedType = this.serializeForMeta(this.boundType);
2173
- return this._serializedType;
2579
+ return this._serializedType ??= this.serializeForMeta(this.boundType);
2580
+ }
2581
+ /** Drops every scope-dependent cache when `scope` is not the one they were built for. */
2582
+ enterScope(scope) {
2583
+ if (scope === this._cacheScope) return;
2584
+ this._cacheScope = scope;
2585
+ this._serializedType = void 0;
2586
+ this._metaResponse = void 0;
2587
+ this._formSchemas.clear();
2588
+ }
2589
+ /** Runs a synchronous build with `scope`'s `db.http.path` overrides applied. */
2590
+ buildScoped(scope, build) {
2591
+ this._buildScope = scope;
2592
+ try {
2593
+ return build();
2594
+ } finally {
2595
+ this._buildScope = void 0;
2596
+ }
2174
2597
  }
2175
2598
  /**
2176
2599
  * Serializes a type for the meta surfaces (`/meta`, `/meta/form/:name`)
@@ -2180,10 +2603,31 @@ let AsReadableController = class AsReadableController {
2180
2603
  * serialize exactly as before.
2181
2604
  */
2182
2605
  serializeForMeta(type) {
2183
- const options = this.getSerializeOptions();
2606
+ const options = this._effectiveSerializeOptions();
2184
2607
  return applyTerminalRefs((0, _atscript_typescript_utils.serializeAnnotatedType)(type, options), type, options);
2185
2608
  }
2186
2609
  /**
2610
+ * {@link getSerializeOptions} plus the current build's per-app `db.http.path`
2611
+ * overrides (a subclass's own `annotationOverrides` entry wins for the same key).
2612
+ */
2613
+ _effectiveSerializeOptions() {
2614
+ const base = this.getSerializeOptions();
2615
+ const scope = this._buildScope;
2616
+ if (!scope) return base;
2617
+ const own = base.annotationOverrides;
2618
+ return {
2619
+ ...base,
2620
+ annotationOverrides: (t) => {
2621
+ const ours = httpPathOverrides(scope, t);
2622
+ const theirs = own?.(t);
2623
+ return ours || theirs ? {
2624
+ ...ours,
2625
+ ...theirs
2626
+ } : void 0;
2627
+ }
2628
+ };
2629
+ }
2630
+ /**
2187
2631
  * One-time initialization hook. Override to seed data, register watchers, etc.
2188
2632
  */
2189
2633
  init() {}
@@ -2381,7 +2825,53 @@ let AsReadableController = class AsReadableController {
2381
2825
  */
2382
2826
  async meta() {
2383
2827
  await this.parseRequest("meta");
2384
- return this.resolveMeta();
2828
+ return this._withOwnHttpPath(await this.resolveMeta());
2829
+ }
2830
+ /**
2831
+ * The root of a controller's own `/meta` carries its own mount as
2832
+ * `db.http.path` (a secondary mount answers with its own route; references to
2833
+ * the model elsewhere carry the canonical one). Left alone for parametric
2834
+ * mounts and when the serialize options strip the key. Never mutates the
2835
+ * cached envelope.
2836
+ */
2837
+ _withOwnHttpPath(meta) {
2838
+ const KEY = "db.http.path";
2839
+ let prefix;
2840
+ try {
2841
+ prefix = (0, moost.useControllerContext)().getPrefix();
2842
+ } catch {
2843
+ return meta;
2844
+ }
2845
+ if (prefix === void 0 || prefix === null) return meta;
2846
+ const own = normalizeHttpPath(prefix);
2847
+ if (isParametricPath(own)) return meta;
2848
+ const options = this.getSerializeOptions();
2849
+ if (options.ignoreAnnotations?.includes(KEY)) return meta;
2850
+ let key = KEY;
2851
+ let value = own;
2852
+ if (options.processAnnotation) {
2853
+ const out = options.processAnnotation({
2854
+ key: KEY,
2855
+ value: own,
2856
+ path: [],
2857
+ kind: "object"
2858
+ });
2859
+ if (!out) return meta;
2860
+ key = out.key;
2861
+ value = out.value;
2862
+ }
2863
+ const type = meta.type;
2864
+ if (!type || type.metadata?.[key] === value) return meta;
2865
+ return {
2866
+ ...meta,
2867
+ type: {
2868
+ ...meta.type,
2869
+ metadata: {
2870
+ ...type.metadata,
2871
+ [key]: value
2872
+ }
2873
+ }
2874
+ };
2385
2875
  }
2386
2876
  /**
2387
2877
  * The `/meta` payload for the current request — the cached envelope
@@ -2393,12 +2883,17 @@ let AsReadableController = class AsReadableController {
2393
2883
  * @since 0.1.143
2394
2884
  */
2395
2885
  resolveMeta() {
2396
- const key = this.metaCacheKey();
2397
- if (!this._metaResponse || key !== this._metaResponseKey) {
2398
- this._metaResponse = this.buildMetaResponse();
2399
- this._metaResponseKey = key;
2400
- }
2401
- return this.applyMetaOverlay(this._metaResponse);
2886
+ return this.currentApp().then((app) => {
2887
+ ensurePublished(app);
2888
+ const scope = httpPathScopeFor(app);
2889
+ this.enterScope(scope);
2890
+ const key = this.metaCacheKey();
2891
+ if (!this._metaResponse || key !== this._metaResponseKey) {
2892
+ this._metaResponse = this.buildScoped(scope, () => this.buildMetaResponse());
2893
+ this._metaResponseKey = key;
2894
+ }
2895
+ return this.applyMetaOverlay(this._metaResponse);
2896
+ });
2402
2897
  }
2403
2898
  /**
2404
2899
  * Identity of the inputs the cached `/meta` envelope is built from — a new
@@ -2421,9 +2916,13 @@ let AsReadableController = class AsReadableController {
2421
2916
  const envelopes = discoverActions(this.constructor, this.app, this.logger);
2422
2917
  const formType = getControllerFormType(this.constructor, name);
2423
2918
  if (!formType || !await this.authorizeForm(name, envelopes.filter((e) => e.info.inputForm === name).map((e) => e.info.name))) throw new _moostjs_event_http.HttpError(404, `Unknown form "${name}"`);
2919
+ const app = await this.currentApp();
2920
+ ensurePublished(app);
2921
+ const scope = httpPathScopeFor(app);
2922
+ this.enterScope(scope);
2424
2923
  let cached = this._formSchemas.get(name);
2425
2924
  if (!cached) {
2426
- cached = this.serializeForMeta(formType);
2925
+ cached = this.buildScoped(scope, () => this.serializeForMeta(formType));
2427
2926
  this._formSchemas.set(name, cached);
2428
2927
  }
2429
2928
  return cached;
@@ -2505,6 +3004,7 @@ AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMeta
2505
3004
  Object,
2506
3005
  String,
2507
3006
  typeof moost.Moost === "undefined" ? Object : moost.Moost,
3007
+ Object,
2508
3008
  Object
2509
3009
  ])], AsReadableController);
2510
3010
  //#endregion
@@ -3917,9 +4417,14 @@ var DecorationPlanner = class {
3917
4417
  if (memoize) memo.meta.set(meta, out);
3918
4418
  return out;
3919
4419
  }
3920
- /** The declared interface serialized for `/meta.decorations`, once per class. */
3921
- serialized(serialize) {
3922
- return this.index.memo.serialized ??= serialize();
4420
+ /** The declared interface serialized for `/meta.decorations`, once per class and app scope. */
4421
+ serialized(scope, serialize) {
4422
+ const { memo } = this.index;
4423
+ if (!memo.serialized || memo.serializedScope !== scope) {
4424
+ memo.serialized = serialize();
4425
+ memo.serializedScope = scope;
4426
+ }
4427
+ return memo.serialized;
3923
4428
  }
3924
4429
  };
3925
4430
  /** Removes what a read must not carry — the unserved declared keys and the hook-only paths — from `rows`. */
@@ -4006,153 +4511,6 @@ function stripsNothing(read) {
4006
4511
  return read === void 0 || read.dropKeys.length === 0 && read.dropPaths.length === 0;
4007
4512
  }
4008
4513
  //#endregion
4009
- //#region src/decorators.ts
4010
- /**
4011
- * DI token under which the {@link AtscriptDbReadable} instance
4012
- * is exposed to the readable controller's constructor via `@Inject`.
4013
- */
4014
- const READABLE_DEF = "__atscript_db_readable_def";
4015
- /**
4016
- * DI token under which the {@link AtscriptDbTable} instance
4017
- * is exposed to the controller's constructor via `@Inject`.
4018
- * Points to the same token as READABLE_DEF for backward compatibility.
4019
- */
4020
- const TABLE_DEF = READABLE_DEF;
4021
- function normalizeOptions(prefixOrOptions) {
4022
- if (typeof prefixOrOptions === "string") return { prefix: prefixOrOptions };
4023
- return prefixOrOptions ?? {};
4024
- }
4025
- /**
4026
- * Builds the shared binding metadata for a decorator invocation: classifies
4027
- * the binding form, computes the static route prefix, and packages a uniform
4028
- * `resolve()` used by both the DI provide factory and the base controller's
4029
- * `super(app)` fallback.
4030
- */
4031
- function buildBinding(binding, options, decoratorName) {
4032
- if ((0, _atscript_typescript_utils.isAnnotatedType)(binding)) {
4033
- const model = binding;
4034
- const space = options.space ?? model.metadata.get("db.space");
4035
- const prefix = options.prefix || model.metadata.get("db.http.path") || model.metadata.get("db.table") || model.metadata.get("db.view") || model.id || "";
4036
- 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.`);
4037
- return {
4038
- meta: {
4039
- model,
4040
- resolve: () => require_db_space_registry.resolveDbSpace(space).get(model)
4041
- },
4042
- prefix
4043
- };
4044
- }
4045
- if (typeof binding === "function") {
4046
- 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.`);
4047
- return {
4048
- meta: { resolve: binding },
4049
- prefix: options.prefix
4050
- };
4051
- }
4052
- const readable = binding;
4053
- const prefix = options.prefix || readable.type.metadata.get("db.http.path") || readable.tableName;
4054
- return {
4055
- meta: {
4056
- model: readable.type,
4057
- resolve: () => readable
4058
- },
4059
- prefix
4060
- };
4061
- }
4062
- function bindReadableController(binding, prefixOrOptions, decoratorName) {
4063
- const { meta, prefix } = buildBinding(binding, normalizeOptions(prefixOrOptions), decoratorName);
4064
- return (0, moost.ApplyDecorators)(getAtscriptDbMate().decorate((classMeta) => {
4065
- classMeta.atscript_db_readable_binding = meta;
4066
- return classMeta;
4067
- }), (0, moost.Provide)(READABLE_DEF, () => meta.resolve()), (0, moost.Controller)(prefix), (0, moost.Inherit)());
4068
- }
4069
- /**
4070
- * Combines the boilerplate needed to turn an {@link AsDbController}
4071
- * subclass into a fully wired HTTP controller for a given `@db.table` model.
4072
- *
4073
- * Internally applies three decorators:
4074
- * 1. **Provide** — registers the readable resolver under {@link TABLE_DEF}.
4075
- * 2. **Controller** — registers the class as a Moost HTTP controller
4076
- * with an optional route prefix (defaults to `@db.http.path`, then the
4077
- * table name).
4078
- * 3. **Inherit** — copies metadata (routes, guards, etc.) from the
4079
- * parent class so they stay active in the derived controller.
4080
- *
4081
- * All three binding forms are supported (see {@link TReadableBinding}):
4082
- *
4083
- * ```ts
4084
- * ‎@TableController(User) // model token (preferred)
4085
- * ‎@TableController(User, { space: "analytics" })
4086
- * ‎@TableController(() => db.getTable(User), "users") // lazy factory
4087
- * ‎@TableController(usersTable) // instance (legacy)
4088
- * export class UsersController extends AsDbController<typeof User> {}
4089
- * ```
4090
- *
4091
- * Token and factory forms resolve lazily at first controller instantiation
4092
- * (during `app.init()`), so the `DbSpace` does not have to exist when the
4093
- * controller module is imported. For the token form, register the space with
4094
- * `provideDbSpace(db)` before `app.init()`.
4095
- *
4096
- * @param binding Model token, lazy factory, or {@link AtscriptDbTable} instance.
4097
- * @param prefixOrOptions Route prefix string, or {@link TControllerBindingOptions}.
4098
- */
4099
- const TableController = (binding, prefixOrOptions) => bindReadableController(binding, prefixOrOptions, "TableController");
4100
- /**
4101
- * Combines the boilerplate needed to turn an {@link AsDbReadableController}
4102
- * subclass into a fully wired HTTP controller for a given `@db.view` or `@db.table` model.
4103
- *
4104
- * Accepts the same three binding forms as {@link TableController}.
4105
- *
4106
- * @param binding Model token, lazy factory, or {@link AtscriptDbReadable} instance.
4107
- * @param prefixOrOptions Route prefix string, or {@link TControllerBindingOptions}.
4108
- *
4109
- * @example
4110
- * ```ts
4111
- * ‎@ReadableController(ActiveTasks)
4112
- * export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
4113
- * ```
4114
- */
4115
- const ReadableController = (binding, prefixOrOptions) => bindReadableController(binding, prefixOrOptions, "ReadableController");
4116
- /**
4117
- * Alias for {@link ReadableController} — use with view-backed controllers.
4118
- *
4119
- * @example
4120
- * ```ts
4121
- * ‎@ViewController(ActiveTasks)
4122
- * export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
4123
- * ```
4124
- */
4125
- const ViewController = ReadableController;
4126
- /**
4127
- * Finds the readable binding written by `@TableController` /
4128
- * `@ReadableController` / `@ViewController` on a controller class, walking the
4129
- * prototype chain so intermediate undecorated classes don't hide the binding
4130
- * (nearest decorated ancestor wins).
4131
- */
4132
- function findReadableBinding(ctor) {
4133
- const mate = getAtscriptDbMate();
4134
- let current = ctor;
4135
- while (typeof current === "function") {
4136
- const binding = mate.read(current)?.atscript_db_readable_binding;
4137
- if (binding) return binding;
4138
- current = Object.getPrototypeOf(current);
4139
- }
4140
- }
4141
- /**
4142
- * Resolves the readable bound to a controller class via
4143
- * {@link findReadableBinding}, throwing with wiring guidance when none is
4144
- * found.
4145
- *
4146
- * Used by {@link AsDbReadableController}'s constructor when `readable` is
4147
- * `undefined` — i.e. a subclass with its own constructor called
4148
- * `super(app)` instead of forwarding an injected instance.
4149
- */
4150
- function resolveBoundReadable(ctor) {
4151
- const binding = findReadableBinding(ctor);
4152
- if (binding) return binding.resolve();
4153
- 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).`);
4154
- }
4155
- //#endregion
4156
4514
  //#region src/permissions/crud-controls.ts
4157
4515
  /**
4158
4516
  * Static control whitelists per read op. Each list is the matching DTO's
@@ -4192,6 +4550,17 @@ const PATH_OPS = [
4192
4550
  "aggregate",
4193
4551
  "bucket"
4194
4552
  ];
4553
+ /** The controller of the event that owns the route params (nearest ancestor that set them). */
4554
+ function routedController(ctx) {
4555
+ for (let c = ctx; c; c = c.parent) {
4556
+ try {
4557
+ c.getOwn(_wooksjs_event_core.routeParamsKey);
4558
+ } catch {
4559
+ continue;
4560
+ }
4561
+ return controllerOf(c);
4562
+ }
4563
+ }
4195
4564
  /** The 400 of a filter / sort on a `@db.writeOnly` field. */
4196
4565
  function writeOnlyError(path, op) {
4197
4566
  const verdict = writeOnlyVerdict(path, op);
@@ -4467,7 +4836,9 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
4467
4836
  * (`$vector`) or a geo index (`/geo`, `$index`) reading a hidden path
4468
4837
  * answers exactly like a nonexistent index (400); a hidden DEFAULT text
4469
4838
  * index falls back to the `@db.column.searchable` substring search over
4470
- * visible fields (or ignores the term when there are none). A
4839
+ * visible fields (or ignores the term when there are none — on list
4840
+ * endpoints; query targets, delegated targets and {@link resolveQuery}
4841
+ * answer 400 `TARGET_INVALID` instead). A
4471
4842
  * `@db.column.derived` field is visible only while its source path is,
4472
4843
  * and one whose source is hidden is sealed out of every read projection
4473
4844
  * for the request, like a `@db.writeOnly` field. The same holds for a
@@ -4790,9 +5161,16 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
4790
5161
  }
4791
5162
  return out;
4792
5163
  }
5164
+ /**
5165
+ * The index visibility gate applies: {@link hasField} is overridden, or the
5166
+ * model has `@db.writeOnly` fields (never readable through a search hit).
5167
+ */
5168
+ get _indexGateActive() {
5169
+ return this._hasFieldOverridden || this._writeOnlySet.size > 0;
5170
+ }
4793
5171
  /** Every path `entry` reads is visible to this request. */
4794
5172
  _indexVisible(entry) {
4795
- return entry.fields.every(this.fieldVisibility.isVisible);
5173
+ return entry.fields.every((path) => this.fieldVisibility.isVisible(path) && (0, _atscript_db.selfOrAncestor)(path, this._writeOnlySet) === void 0);
4796
5174
  }
4797
5175
  /**
4798
5176
  * Native text search serves this request: the adapter searches natively
@@ -4811,7 +5189,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
4811
5189
  }
4812
5190
  _resolveNativeSearch(controls) {
4813
5191
  if (!this.readable.isSearchable()) return false;
4814
- if (!this._hasFieldOverridden) return true;
5192
+ if (!this._indexGateActive) return true;
4815
5193
  if (typeof controls.$index === "string" && controls.$index) return true;
4816
5194
  const def = this.indexFieldPaths().find((e) => e.type === "text" && e.isDefault);
4817
5195
  return def === void 0 || this._indexVisible(def);
@@ -4825,7 +5203,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
4825
5203
  * nonexistent index (the core's wording).
4826
5204
  */
4827
5205
  _checkIndexGate(controls) {
4828
- if (!this._hasFieldOverridden) return void 0;
5206
+ if (!this._indexGateActive) return void 0;
4829
5207
  const name = typeof controls.$index === "string" ? controls.$index : void 0;
4830
5208
  if (controls.$center !== void 0) {
4831
5209
  const geoIndexes = this.indexFieldPaths().filter((e) => e.type === "geo");
@@ -4857,7 +5235,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
4857
5235
  * `@db.column.searchable` fallback when any of its fields is visible).
4858
5236
  */
4859
5237
  _applyIndexVisibility(meta) {
4860
- if (!this._hasFieldOverridden) return meta;
5238
+ if (!this._indexGateActive) return meta;
4861
5239
  const entries = this.indexFieldPaths();
4862
5240
  const visibleDefault = (type) => {
4863
5241
  const def = entries.find((e) => e.type === type && e.isDefault);
@@ -5070,6 +5448,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
5070
5448
  * {@link checkCapabilities}, {@link hasField}), where the client
5071
5449
  * predicates' {@link transformRelationFilter} also runs: a query target
5072
5450
  * never filters on, nor counts by, a field the caller can't read.
5451
+ * {@link resolveQuery} does not call it (there is no action of this
5452
+ * controller to scope).
5073
5453
  *
5074
5454
  * @since 0.1.147
5075
5455
  */
@@ -5406,18 +5786,76 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
5406
5786
  async [RESOLVE_TARGET](req) {
5407
5787
  const { action } = req;
5408
5788
  const body = parseQueryTargetBody(action, req.query);
5789
+ const { rows, filter, findMany, exclude, visibleOf, cap } = await this._resolveMatching({
5790
+ label: action,
5791
+ body,
5792
+ cap: req.cap,
5793
+ maxExclude: req.maxExclude,
5794
+ overlay: req.overlay,
5795
+ scopeByAction: true,
5796
+ select: [...new Set(req.select)],
5797
+ sortBy: req.select,
5798
+ excludeShapes: req.excludeShapes,
5799
+ visibleOf: req.visibleOf
5800
+ });
5801
+ const byIds = (read, ids, scope, select) => findRowsByIds({ findMany: (q) => read({
5802
+ ...q,
5803
+ controls: {
5804
+ ...q.controls,
5805
+ $limit: Math.max(ids.length, cap + 1)
5806
+ }
5807
+ }) }, ids, scope, select);
5808
+ const snapshotFields = new Set(req.select);
5809
+ let first = true;
5810
+ return {
5811
+ matched: rows.length,
5812
+ rows,
5813
+ dryRun: body.dryRun === true,
5814
+ exclude,
5815
+ visibleOf,
5816
+ load: (ids, select) => {
5817
+ if (!first) return byIds(findMany, ids, filter, select);
5818
+ first = false;
5819
+ const fields = [...select];
5820
+ if (!fields.every((f) => snapshotFields.has(f))) {
5821
+ const plain = (q) => this.readable.findMany(q);
5822
+ return byIds(plain, ids, void 0, fields);
5823
+ }
5824
+ return Promise.resolve(alignRowsToIds(rows, ids).map((row, i) => row ? projectRow$2(row, new Set([...fields, ...Object.keys(ids[i])])) : void 0));
5825
+ }
5826
+ };
5827
+ }
5828
+ /**
5829
+ * The shared resolver behind query targets and {@link resolveQuery}: the
5830
+ * query body validated, checked and run as a READ of this controller, then
5831
+ * ONE read of `select` ordered by `sort` — `filter (+ $search) ∧ overlay ∧
5832
+ * scope ∧ ¬exclude`, at most `cap + 1` rows. More than `cap` → 400
5833
+ * `TARGET_TOO_LARGE`; a count other than `expectCount` → 409
5834
+ * `TARGET_CHANGED`.
5835
+ *
5836
+ * Everything that depends on the read's visibility — the `$search`
5837
+ * fallback, the native-search memo, the un-appliable-term refusal and the
5838
+ * delegated identity — is computed INSIDE the read child, where the
5839
+ * permission layer's per-request state is the read's.
5840
+ */
5841
+ async _resolveMatching(spec) {
5842
+ const { label, body } = spec;
5843
+ const cap = Math.min(spec.cap, body.maxRows ?? Infinity);
5409
5844
  const parsed = this.parseUrlOr400(body.q.startsWith("?") ? body.q.slice(1) : body.q);
5410
5845
  const controls = {};
5411
5846
  for (const [k, v] of Object.entries(parsed.controls ?? {})) {
5412
5847
  if (v === void 0) continue;
5413
- if (k !== "$search" && k !== "$index") throw targetInvalid(action, `A query target takes a filter, $search and $index only — "${k}" is not accepted`);
5414
- if (k === "$search" && typeof v !== "string" && typeof v !== "number") throw targetInvalid(action, "$search must be a search term");
5848
+ if (k !== "$search" && k !== "$index") throw targetInvalid(label, `A query target takes a filter, $search and $index only — "${k}" is not accepted`);
5849
+ if (k === "$search" && typeof v !== "string" && typeof v !== "number") throw targetInvalid(label, "$search must be a search term");
5415
5850
  controls[k] = k === "$search" ? `${v}` : v;
5416
5851
  }
5417
- if (controls.$index !== void 0 && typeof controls.$index !== "string") throw targetInvalid(action, "$index must be an index name");
5852
+ if (controls.$index !== void 0 && typeof controls.$index !== "string") throw targetInvalid(label, "$index must be an index name");
5418
5853
  const exclude = body.exclude ?? [];
5419
- const shapes = req.excludeShapes ?? [];
5854
+ const shapes = spec.excludeShapes ?? [];
5855
+ const sealedSet = () => new Set([...this.fieldVisibility.sealedFor(this.readable), ...this._leavesOf(this.readable).filter((leaf) => !this.fieldVisibility.isVisible(leaf))]);
5420
5856
  const check = () => {
5857
+ let sealed;
5858
+ const readableLeaf = (leaf) => !(sealed ??= sealedSet()).has(leaf);
5421
5859
  const controlsError = this.validateControls(controls, "query");
5422
5860
  if (controlsError) throw new _moostjs_event_http.HttpError(400, controlsError);
5423
5861
  const gateError = this.checkCapabilities({
@@ -5425,71 +5863,160 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
5425
5863
  controls
5426
5864
  });
5427
5865
  if (gateError) throw gateError;
5428
- if (exclude.length > 0) validateMultiId(exclude, shapes.length === 0 ? this.idSource : {
5429
- identifications: [...this.idSource.identifications, ...shapes.map((fields) => ({
5430
- fields,
5431
- source: "target"
5432
- }))],
5433
- fieldDescriptors: this.readable.fieldDescriptors
5434
- }, req.maxExclude);
5866
+ if (exclude.length > 0) {
5867
+ const source = shapes.length === 0 ? this.idSource : {
5868
+ identifications: [...this.idSource.identifications, ...shapes.map((fields) => ({
5869
+ fields,
5870
+ source: "target"
5871
+ }))],
5872
+ fieldDescriptors: this.readable.fieldDescriptors
5873
+ };
5874
+ try {
5875
+ validateMultiId(exclude, source, spec.maxExclude);
5876
+ } catch (error) {
5877
+ throw validatorErrorToHttp(error) ?? error;
5878
+ }
5879
+ }
5880
+ const nav = FieldCapabilityIndex.navPathsOf(this.readable);
5881
+ for (const path of spec.selectGate ?? []) {
5882
+ if (this.capabilities.decorationCap(path) || (0, _atscript_db.selfOrAncestor)(path, nav) !== void 0) throw badRequest(path, `Unknown field "${path}"`);
5883
+ const verdict = this.capabilities.check(path, "select", this.fieldVisibility.isVisible);
5884
+ if (verdict) throw badRequest(verdict.path, verdict.message);
5885
+ if (this._writeOnlySet.has(path)) throw badRequest(path, `Field "${path}" is @db.writeOnly`);
5886
+ const prefix = `${path}.`;
5887
+ const leaves = this._leavesOf(this.readable).filter((leaf) => leaf.startsWith(prefix));
5888
+ if (leaves.length > 0 && !leaves.some((leaf) => readableLeaf(leaf))) throw badRequest(path, `Unknown field "${path}"`);
5889
+ }
5890
+ for (const path of spec.sortGate ?? []) {
5891
+ const verdict = this.capabilities.check(path, "sort", this.fieldVisibility.isVisible);
5892
+ if (verdict) throw targetInvalid(label, verdict.message);
5893
+ }
5435
5894
  };
5436
- const ownScope = req.overlay === "action" || this.queryTargetScope !== _AsDbReadableController.prototype.queryTargetScope;
5437
- const [[base, scope], overlay] = await Promise.all([this._asRead(controls, parsed.filter, async () => {
5895
+ const ownScope = spec.scopeByAction === true && (spec.overlay === "action" || this.queryTargetScope !== _AsDbReadableController.prototype.queryTargetScope);
5896
+ const [read, overlay] = await Promise.all([this._asRead(controls, parsed.filter, async () => {
5438
5897
  check();
5439
- const [clientFilter, readScope] = await Promise.all([this._relationOverlay(parsed), ownScope ? this.queryTargetScope(action) : void 0]);
5440
- return [req.overlay === "read" ? await this.transformFilter(clientFilter ?? {}) : clientFilter, readScope];
5441
- }), req.overlay === "action" ? this.rowOverlay() : void 0]);
5442
- const filter = (0, _atscript_db.andFilters)(this.applySearchFallback(base, controls), overlay, scope, exclude.length > 0 ? { $not: { $or: exclude } } : void 0);
5443
- const strategy = await this._resolveReadStrategy(controls);
5898
+ const [clientFilter, readScope, strategy] = await Promise.all([
5899
+ this._relationOverlay(parsed),
5900
+ ownScope ? this.queryTargetScope(label) : void 0,
5901
+ this._resolveReadStrategy(controls)
5902
+ ]);
5903
+ const base = spec.overlay === "action" ? clientFilter : await this.transformFilter(clientFilter ?? {});
5904
+ const searched = this.applySearchFallback(base, controls);
5905
+ if (controls.$search && strategy.kind !== "search" && searched === base) throw targetInvalid(label, "$search is not available here");
5906
+ const visibleOf = spec.visibleOf?.filter((f) => this.fieldVisibility.isVisible(f));
5907
+ let select = spec.select;
5908
+ if (spec.selectGate) {
5909
+ const sealed = sealedSet();
5910
+ for (const id of spec.identity ?? []) sealed.delete(id);
5911
+ select = this._sealSelect([...spec.select], sealed, this.readable);
5912
+ }
5913
+ return {
5914
+ searched,
5915
+ readScope,
5916
+ strategy,
5917
+ visibleOf,
5918
+ select
5919
+ };
5920
+ }, spec.routeParams), spec.overlay === "action" ? this.rowOverlay() : void 0]);
5921
+ const { strategy } = read;
5922
+ const filter = (0, _atscript_db.andFilters)(read.searched, overlay, read.readScope, spec.scope, exclude.length > 0 ? { $not: { $or: exclude } } : void 0);
5444
5923
  const findMany = (q) => strategy.kind === "search" ? this.readable.search(strategy.term, q, strategy.index) : this.readable.findMany(q);
5445
- const cap = Math.min(req.cap, body.maxRows ?? Infinity);
5446
- const sort = {};
5447
- for (const f of req.select) sort[f] = 1;
5448
5924
  const rows = await findMany({
5449
5925
  filter,
5450
5926
  controls: {
5451
- $select: [...new Set(req.select)],
5452
- $sort: sort,
5927
+ $select: [...read.select],
5928
+ $sort: Object.fromEntries(spec.sortBy.map((f) => [f, 1])),
5453
5929
  $limit: cap + 1
5454
5930
  }
5455
5931
  });
5456
- if (rows.length > cap) throw new ActionTargetError("TARGET_TOO_LARGE", action, `The query matches more than ${cap} rows`, { cap });
5457
- if (body.expectCount !== void 0 && body.expectCount !== rows.length) throw new ActionTargetError("TARGET_CHANGED", action, `The query now matches ${rows.length} rows (expected ${body.expectCount})`, { matched: rows.length });
5458
- const byIds = (read, ids, scope, select) => findRowsByIds({ findMany: (q) => read({
5459
- ...q,
5460
- controls: {
5461
- ...q.controls,
5462
- $limit: Math.max(ids.length, cap + 1)
5463
- }
5464
- }) }, ids, scope, select);
5465
- const snapshotFields = new Set(req.select);
5466
- let first = true;
5932
+ if (rows.length > cap) throw new ActionTargetError("TARGET_TOO_LARGE", label, `The query matches more than ${cap} rows`, { cap });
5933
+ if (body.expectCount !== void 0 && body.expectCount !== rows.length) throw new ActionTargetError("TARGET_CHANGED", label, `The query now matches ${rows.length} rows (expected ${body.expectCount})`, { matched: rows.length });
5467
5934
  return {
5468
- matched: rows.length,
5469
5935
  rows,
5470
- dryRun: body.dryRun === true,
5936
+ filter,
5937
+ findMany,
5471
5938
  exclude,
5472
- load: (ids, select) => {
5473
- if (!first) return byIds(findMany, ids, filter, select);
5474
- first = false;
5475
- const fields = [...select];
5476
- if (!fields.every((f) => snapshotFields.has(f))) {
5477
- const plain = (q) => this.readable.findMany(q);
5478
- return byIds(plain, ids, void 0, fields);
5479
- }
5480
- return Promise.resolve(alignRowsToIds(rows, ids).map((row, i) => row ? projectRow$2(row, new Set([...fields, ...Object.keys(ids[i])])) : void 0));
5481
- }
5939
+ visibleOf: read.visibleOf,
5940
+ cap,
5941
+ select: read.select
5482
5942
  };
5483
5943
  }
5484
5944
  /**
5945
+ * Rows of THIS controller matching `q`, resolved as a READ of it for the
5946
+ * current event's caller (since 0.1.149) — from your own command, e.g. to
5947
+ * act on "every issue matching this search". `q` is a `GET /query` string
5948
+ * (`$search` / `$index` and a filter only) or a query-target envelope
5949
+ * `{ q, exclude?, expectCount?, maxRows? }` (no `dryRun`).
5950
+ *
5951
+ * The read runs under this controller's full read policy
5952
+ * ({@link prepareRequest} with `endpoint: "query"`, {@link hasField},
5953
+ * {@link validateControls}, the capability / index gate and the
5954
+ * {@link transformFilter} overlay) with the current event's identity.
5955
+ * Route interceptors and guards of the `query` route do not run; read
5956
+ * authorization belongs in {@link prepareRequest}. {@link queryTargetScope}
5957
+ * is not called. Hooks see no route params of the caller (only a call from
5958
+ * the routed event's own controller instance keeps its params); pass route-derived
5959
+ * restrictions as `opts.scope`. Joins the caller's open transaction.
5960
+ *
5961
+ * Rows are ordered by identity (`preferredId`, else the primary key) and
5962
+ * carry the identity fields plus `opts.select` (gated like `/query`
5963
+ * `$select`; `transformProjection` is not applied — hide fields with
5964
+ * {@link hasField}; decoration keys and navigation paths are refused). An
5965
+ * identity-less readable is ordered by `select` (each path sortable, else
5966
+ * `TARGET_INVALID`). More than `opts.cap` (default 1000) rows → 400
5967
+ * `TARGET_TOO_LARGE`; a count other than `expectCount` → 409
5968
+ * `TARGET_CHANGED`; a `$search` that can't be applied → 400
5969
+ * `TARGET_INVALID`. Must be awaited inside a running event handler.
5970
+ */
5971
+ async resolveQuery(q, opts = {}) {
5972
+ let caller;
5973
+ try {
5974
+ caller = (0, _wooksjs_event_core.current)();
5975
+ } catch {
5976
+ throw new Error("[moost-db] resolveQuery must be awaited inside an event handler");
5977
+ }
5978
+ const cap = opts.cap ?? 1e3;
5979
+ if (!Number.isInteger(cap) || cap < 1) throw new Error("[moost-db] resolveQuery: `cap` must be a positive integer");
5980
+ const select = opts.select ?? [];
5981
+ if (!Array.isArray(select) || select.some((p) => typeof p !== "string")) throw new Error("[moost-db] resolveQuery: `select` must be an array of field paths");
5982
+ const ids = this.readable.preferredId?.length ? this.readable.preferredId : this.readable.primaryKeys;
5983
+ const order = ids.length > 0 ? ids : select;
5984
+ if (order.length === 0) throw new Error("[moost-db] resolveQuery: this readable has no identity — pass `select` (rows are ordered by it)");
5985
+ const label = readCurrentActionMeta(caller)?.name ?? "";
5986
+ const body = parseQueryTargetBody(label, typeof q === "string" ? { q } : q);
5987
+ if (body.dryRun !== void 0) throw targetInvalid(label, "resolveQuery takes no `dryRun` — read `query.dryRun` yourself");
5988
+ let sameRoute = false;
5989
+ try {
5990
+ sameRoute = controllerOf(caller) === this && routedController(caller) === this;
5991
+ } catch {}
5992
+ const fields = [...new Set([...ids, ...select])];
5993
+ const { rows, select: sealedSelect } = await this._resolveMatching({
5994
+ label,
5995
+ body,
5996
+ cap,
5997
+ maxExclude: DEFAULT_MAX_ACTION_IDS,
5998
+ overlay: "read",
5999
+ scope: opts.scope,
6000
+ select: fields,
6001
+ sortBy: order,
6002
+ selectGate: select,
6003
+ identity: ids,
6004
+ sortGate: ids.length > 0 ? void 0 : order,
6005
+ routeParams: sameRoute ? void 0 : {}
6006
+ });
6007
+ return rows.map((row) => projectRow$2(row, sealedSelect));
6008
+ }
6009
+ /**
5485
6010
  * Runs `fn` as a READ of this controller (since 0.1.147): in a child of
5486
6011
  * the current event whose controller context is this controller's `query`
5487
6012
  * handler, after `prepareRequest({ endpoint: "query", controls, filter })` — the
5488
6013
  * request-scoped state a permission layer builds there (read grant, field
5489
- * visibility) is the read's and stays in the child.
6014
+ * visibility) is the read's and stays in the child. `routeParams` (since
6015
+ * 0.1.149) replaces the route params the child's hooks read.
5490
6016
  */
5491
- _asRead(controls, filter, fn) {
6017
+ _asRead(controls, filter, fn, routeParams) {
5492
6018
  return runAsController(this, "query", async () => {
6019
+ if (routeParams) (0, _wooksjs_event_core.current)().set(_wooksjs_event_core.routeParamsKey, routeParams);
5493
6020
  if (typeof this.prepareRequest === "function") await this.prepareRequest(readRequestContext("query", controls, filter));
5494
6021
  return fn();
5495
6022
  });
@@ -5505,6 +6032,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
5505
6032
  for (const fd of this.readable.fieldDescriptors) {
5506
6033
  if (fd.ignored) continue;
5507
6034
  if (!fd.type?.metadata?.has?.("db.column.searchable")) continue;
6035
+ if (typeof (0, _atscript_db.searchMemberKind)(fd.type) === "object") continue;
5508
6036
  if (!this.capabilities.isPhysicallyFilterable(fd.path)) continue;
5509
6037
  out.push(fd.path);
5510
6038
  }
@@ -5573,7 +6101,18 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
5573
6101
  * with the existing filter. Applies only when native search does not serve
5574
6102
  * the request (no native search, or — since 0.1.143 — its default index
5575
6103
  * reads a field {@link hasField} hides) and the request isn't a vector
5576
- * search (`$vector` consumes the term).
6104
+ * search (`$vector` consumes the term). Lenient on list endpoints: a term
6105
+ * nothing can apply is ignored. Resolvers (query targets, `resolveQuery`)
6106
+ * refuse it — a subclass override that applies the term must return a new
6107
+ * filter object.
6108
+ *
6109
+ * A string field matches when the term is a substring of its text; an
6110
+ * INTEGER field (`number.int` and its sizes, `@expect.int`,
6111
+ * `@db.default.increment`) when it is a substring of the number's decimal
6112
+ * text — `2946` finds `29461277` (since 0.1.150). To search numeric IDs
6113
+ * declare `@db.column.searchable` (fallback) or `@db.index.fulltext`
6114
+ * (native, exact number) on the integer field — override this hook only for
6115
+ * search rules the annotations cannot express.
5577
6116
  */
5578
6117
  applySearchFallback(filter, controls) {
5579
6118
  const term = controls.$search;
@@ -5612,6 +6151,51 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
5612
6151
  delete rest.$index;
5613
6152
  return rest;
5614
6153
  }
6154
+ /**
6155
+ * `$count` of a native text / vector search: the adapter's own count, so it
6156
+ * agrees with `/query` and `/pages`. A text search counts every match (the
6157
+ * single returned row is discarded); a vector search counts the nearest
6158
+ * neighbours up to `$limit` (default 1000), as `/query` returns them.
6159
+ */
6160
+ async _countSearched(strategy, filter, controls, sealedControls) {
6161
+ const { $count: _count, ...sealed } = sealedControls;
6162
+ if (strategy.kind === "search") {
6163
+ const q = {
6164
+ filter,
6165
+ controls: this._searchReadControls(controls, sealed, sealed.$select, {
6166
+ $skip: void 0,
6167
+ $limit: 1
6168
+ })
6169
+ };
6170
+ const { count } = await this.readable.searchWithCount(strategy.term, q, strategy.index);
6171
+ return count;
6172
+ }
6173
+ const q = {
6174
+ filter,
6175
+ controls: this._searchReadControls(controls, sealed, sealed.$select, {
6176
+ $skip: void 0,
6177
+ $limit: controls.$limit || 1e3
6178
+ })
6179
+ };
6180
+ const { count } = await (strategy.vectorField ? this.readable.vectorSearchWithCount(strategy.vectorField, strategy.vector, q) : this.readable.vectorSearchWithCount(strategy.vector, q));
6181
+ return count;
6182
+ }
6183
+ /**
6184
+ * The controls a native search / vector read is run with: the request's
6185
+ * sealed controls (so every search control — `$fuzzy`, … — reaches the
6186
+ * adapter) plus the select, the threshold and the caller's paging. ONE
6187
+ * builder for `/query`, `/pages` and the native `$count`, so they can't
6188
+ * disagree about what a search matches.
6189
+ */
6190
+ _searchReadControls(controls, sealed, select, paging) {
6191
+ const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
6192
+ return {
6193
+ ...sealed,
6194
+ $select: select,
6195
+ ...paging,
6196
+ $threshold: threshold
6197
+ };
6198
+ }
5615
6199
  async _resolveReadStrategy(controls) {
5616
6200
  const searchTerm = controls.$search;
5617
6201
  const indexName = controls.$index;
@@ -5666,21 +6250,13 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
5666
6250
  if (decorated === void 0) return strip();
5667
6251
  return Promise.resolve(decorated).then(strip);
5668
6252
  }
5669
- /**
5670
- * The app of the current event, through DI — never the one this
5671
- * (singleton) instance was constructed in, which may be gone (a re-booted
5672
- * app, a hot reload).
5673
- */
5674
- _currentApp() {
5675
- return (0, moost.useControllerContext)().instantiate(moost.Moost);
5676
- }
5677
6253
  /** The class's `@DbActionsFrom` delegations, validated on first use (per app). */
5678
6254
  async _delegations() {
5679
6255
  if (!this._hasDelegations) return [];
5680
6256
  return discoverDelegations({
5681
6257
  ctor: this.constructor,
5682
6258
  readable: this.readable,
5683
- app: await this._currentApp(),
6259
+ app: await this.currentApp(),
5684
6260
  logger: this.logger,
5685
6261
  instantiate: (ctor) => (0, moost.useControllerContext)().instantiate(ctor)
5686
6262
  });
@@ -6038,24 +6614,22 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
6038
6614
  }
6039
6615
  const [transformedFilter, { sealed, finish }] = await Promise.all([this.transformFilter(clientFilter), this._projectRead(controls)]);
6040
6616
  const filter = this.applySearchFallback(transformedFilter, controls);
6041
- if (controls.$count) return this.readable.count({
6042
- filter,
6043
- controls: {
6044
- ...controls,
6045
- $select: sealed.$select
6046
- }
6047
- });
6617
+ if (controls.$count) {
6618
+ const strategy = await this._resolveReadStrategy(controls);
6619
+ if (strategy.kind === "plain") return this.readable.count({
6620
+ filter,
6621
+ controls: {
6622
+ ...controls,
6623
+ $select: sealed.$select
6624
+ }
6625
+ });
6626
+ return this._countSearched(strategy, filter, controls, sealed);
6627
+ }
6048
6628
  const projected = finish();
6049
6629
  if (projected instanceof _moostjs_event_http.HttpError) return projected;
6050
- const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
6051
6630
  const queryObj = {
6052
6631
  filter,
6053
- controls: {
6054
- ...sealed,
6055
- $select: projected.select,
6056
- $limit: controls.$limit || 1e3,
6057
- $threshold: threshold
6058
- }
6632
+ controls: this._searchReadControls(controls, sealed, projected.select, { $limit: controls.$limit || 1e3 })
6059
6633
  };
6060
6634
  return (await this._runReadWithActions("query", queryObj, controls, projected, async (q, strategy) => {
6061
6635
  switch (strategy.kind) {
@@ -6082,16 +6656,12 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
6082
6656
  const filter = this.applySearchFallback(transformedFilter, controls);
6083
6657
  const projected = finish();
6084
6658
  if (projected instanceof _moostjs_event_http.HttpError) return projected;
6085
- const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
6086
6659
  const query = {
6087
6660
  filter,
6088
- controls: {
6089
- ...sealed,
6090
- $select: projected.select,
6661
+ controls: this._searchReadControls(controls, sealed, projected.select, {
6091
6662
  $skip: skip,
6092
- $limit: size,
6093
- $threshold: threshold
6094
- }
6663
+ $limit: size
6664
+ })
6095
6665
  };
6096
6666
  const result = await this._runReadWithActions("pages", query, controls, projected, async (q, strategy) => {
6097
6667
  switch (strategy.kind) {
@@ -6363,7 +6933,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
6363
6933
  maxExclude: limits.maxIds,
6364
6934
  overlay: "read",
6365
6935
  select: [...new Set([...identity, ...delegation.paths])],
6366
- excludeShapes: [delegation.paths]
6936
+ excludeShapes: [delegation.paths],
6937
+ visibleOf: identity
6367
6938
  });
6368
6939
  if (resolved.dryRun) return { matched: resolved.matched };
6369
6940
  const summary = {
@@ -6373,7 +6944,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
6373
6944
  failed: []
6374
6945
  };
6375
6946
  const { ids, index } = mapToSourceIds(resolved.rows, delegation.idMap);
6376
- const visibleIdentity = identity.filter((f) => this.fieldVisibility.isVisible(f));
6947
+ const visibleIdentity = resolved.visibleOf ?? [];
6377
6948
  for (let i = 0; i < index.length; i++) {
6378
6949
  if (index[i] >= 0) continue;
6379
6950
  const row = resolved.rows[i];
@@ -6571,7 +7142,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
6571
7142
  relations,
6572
7143
  fields,
6573
7144
  type: this.getSerializedType(),
6574
- ...this._decorations && { decorations: this._planner.serialized(() => this.serializeForMeta(this._decorations.type)) },
7145
+ ...this._decorations && { decorations: this._planner.serialized(this._buildScope, () => this.serializeForMeta(this._decorations.type)) },
6575
7146
  actions: this.buildActions(),
6576
7147
  crud: this.buildCrud(),
6577
7148
  versionColumn: this.readable.versionColumn,
@@ -6854,7 +7425,8 @@ let AsDbController = _AsDbController = class AsDbController extends AsDbReadable
6854
7425
  * Bulk auto-lift: each item carries its own `version` → `$cas`.
6855
7426
  * NOTE: per-item conflict disambiguation in the response body is deferred
6856
7427
  * (§6.4) — the aggregate `{ matchedCount, modifiedCount }` surfaces partial
6857
- * application; callers can detect mismatches via `modifiedCount < N`.
7428
+ * application; callers can detect mismatches via `matchedCount < N` (`modifiedCount` may
7429
+ * be lower for unchanged values, e.g. a version-exempt patch on MySQL / Mongo).
6858
7430
  */
6859
7431
  _resolveBulkCas(rows, versionColumn) {
6860
7432
  if (versionColumn === void 0) return;
@@ -7066,8 +7638,8 @@ let AsValueHelpController = class AsValueHelpController extends AsReadableContro
7066
7638
  searchableFields;
7067
7639
  /** The `@meta.id` field name on the bound interface, if any. */
7068
7640
  primaryKey;
7069
- constructor(boundType, controllerName, app) {
7070
- super(boundType, controllerName, app, "value-help");
7641
+ constructor(boundType, controllerName, app, opts) {
7642
+ super(boundType, controllerName, app, "value-help", opts);
7071
7643
  assertNoValueHelpActions(this.constructor);
7072
7644
  const fieldMeta = /* @__PURE__ */ new Map();
7073
7645
  const explicitlySearchable = [];
@@ -7283,7 +7855,8 @@ __decorate([
7283
7855
  AsValueHelpController = __decorate([(0, moost.Inherit)(), __decorateMetadata("design:paramtypes", [
7284
7856
  Object,
7285
7857
  String,
7286
- typeof moost.Moost === "undefined" ? Object : moost.Moost
7858
+ typeof moost.Moost === "undefined" ? Object : moost.Moost,
7859
+ Object
7287
7860
  ])], AsValueHelpController);
7288
7861
  registerAsValueHelpController(AsValueHelpController);
7289
7862
  //#endregion
@@ -7291,9 +7864,9 @@ registerAsValueHelpController(AsValueHelpController);
7291
7864
  let AsJsonValueHelpController = class AsJsonValueHelpController extends AsValueHelpController {
7292
7865
  rows;
7293
7866
  _pkIndex;
7294
- constructor(boundType, rows, app, controllerName) {
7867
+ constructor(boundType, rows, app, controllerName, opts) {
7295
7868
  const name = controllerName || boundType.metadata.get("db.table") || "value-help";
7296
- super(boundType, name, app);
7869
+ super(boundType, name, app, opts);
7297
7870
  this.rows = rows;
7298
7871
  if (this.primaryKey) {
7299
7872
  const pk = this.primaryKey;
@@ -7368,7 +7941,8 @@ AsJsonValueHelpController = __decorate([(0, moost.Inherit)(), __decorateMetadata
7368
7941
  Object,
7369
7942
  Array,
7370
7943
  typeof moost.Moost === "undefined" ? Object : moost.Moost,
7371
- String
7944
+ String,
7945
+ Object
7372
7946
  ])], AsJsonValueHelpController);
7373
7947
  //#endregion
7374
7948
  //#region src/assert-exposed.ts
@@ -7412,7 +7986,7 @@ function assertExposed(app, models, options) {
7412
7986
  const missing = [];
7413
7987
  for (const model of models) {
7414
7988
  if ((0, _atscript_db.aliasTargetOf)(model)) continue;
7415
- const httpPath = model.metadata.get("db.http.path");
7989
+ const httpPath = designTimeHttpPath(model);
7416
7990
  if (!auditAll && httpPath === void 0) continue;
7417
7991
  if (excluded.has(model) || exposed.has(model)) continue;
7418
7992
  missing.push(model);