@atscript/moost-db 0.1.148 → 0.1.149
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 +227 -53
- package/dist/index.d.cts +73 -4
- package/dist/index.d.mts +73 -4
- package/dist/index.mjs +228 -54
- package/package.json +4 -4
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
|
-
|
|
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);
|
|
@@ -4192,6 +4200,17 @@ const PATH_OPS = [
|
|
|
4192
4200
|
"aggregate",
|
|
4193
4201
|
"bucket"
|
|
4194
4202
|
];
|
|
4203
|
+
/** The controller of the event that owns the route params (nearest ancestor that set them). */
|
|
4204
|
+
function routedController(ctx) {
|
|
4205
|
+
for (let c = ctx; c; c = c.parent) {
|
|
4206
|
+
try {
|
|
4207
|
+
c.getOwn(_wooksjs_event_core.routeParamsKey);
|
|
4208
|
+
} catch {
|
|
4209
|
+
continue;
|
|
4210
|
+
}
|
|
4211
|
+
return controllerOf(c);
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4195
4214
|
/** The 400 of a filter / sort on a `@db.writeOnly` field. */
|
|
4196
4215
|
function writeOnlyError(path, op) {
|
|
4197
4216
|
const verdict = writeOnlyVerdict(path, op);
|
|
@@ -4467,7 +4486,9 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4467
4486
|
* (`$vector`) or a geo index (`/geo`, `$index`) reading a hidden path
|
|
4468
4487
|
* answers exactly like a nonexistent index (400); a hidden DEFAULT text
|
|
4469
4488
|
* index falls back to the `@db.column.searchable` substring search over
|
|
4470
|
-
* visible fields (or ignores the term when there are none
|
|
4489
|
+
* visible fields (or ignores the term when there are none — on list
|
|
4490
|
+
* endpoints; query targets, delegated targets and {@link resolveQuery}
|
|
4491
|
+
* answer 400 `TARGET_INVALID` instead). A
|
|
4471
4492
|
* `@db.column.derived` field is visible only while its source path is,
|
|
4472
4493
|
* and one whose source is hidden is sealed out of every read projection
|
|
4473
4494
|
* for the request, like a `@db.writeOnly` field. The same holds for a
|
|
@@ -5070,6 +5091,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5070
5091
|
* {@link checkCapabilities}, {@link hasField}), where the client
|
|
5071
5092
|
* predicates' {@link transformRelationFilter} also runs: a query target
|
|
5072
5093
|
* never filters on, nor counts by, a field the caller can't read.
|
|
5094
|
+
* {@link resolveQuery} does not call it (there is no action of this
|
|
5095
|
+
* controller to scope).
|
|
5073
5096
|
*
|
|
5074
5097
|
* @since 0.1.147
|
|
5075
5098
|
*/
|
|
@@ -5406,18 +5429,76 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5406
5429
|
async [RESOLVE_TARGET](req) {
|
|
5407
5430
|
const { action } = req;
|
|
5408
5431
|
const body = parseQueryTargetBody(action, req.query);
|
|
5432
|
+
const { rows, filter, findMany, exclude, visibleOf, cap } = await this._resolveMatching({
|
|
5433
|
+
label: action,
|
|
5434
|
+
body,
|
|
5435
|
+
cap: req.cap,
|
|
5436
|
+
maxExclude: req.maxExclude,
|
|
5437
|
+
overlay: req.overlay,
|
|
5438
|
+
scopeByAction: true,
|
|
5439
|
+
select: [...new Set(req.select)],
|
|
5440
|
+
sortBy: req.select,
|
|
5441
|
+
excludeShapes: req.excludeShapes,
|
|
5442
|
+
visibleOf: req.visibleOf
|
|
5443
|
+
});
|
|
5444
|
+
const byIds = (read, ids, scope, select) => findRowsByIds({ findMany: (q) => read({
|
|
5445
|
+
...q,
|
|
5446
|
+
controls: {
|
|
5447
|
+
...q.controls,
|
|
5448
|
+
$limit: Math.max(ids.length, cap + 1)
|
|
5449
|
+
}
|
|
5450
|
+
}) }, ids, scope, select);
|
|
5451
|
+
const snapshotFields = new Set(req.select);
|
|
5452
|
+
let first = true;
|
|
5453
|
+
return {
|
|
5454
|
+
matched: rows.length,
|
|
5455
|
+
rows,
|
|
5456
|
+
dryRun: body.dryRun === true,
|
|
5457
|
+
exclude,
|
|
5458
|
+
visibleOf,
|
|
5459
|
+
load: (ids, select) => {
|
|
5460
|
+
if (!first) return byIds(findMany, ids, filter, select);
|
|
5461
|
+
first = false;
|
|
5462
|
+
const fields = [...select];
|
|
5463
|
+
if (!fields.every((f) => snapshotFields.has(f))) {
|
|
5464
|
+
const plain = (q) => this.readable.findMany(q);
|
|
5465
|
+
return byIds(plain, ids, void 0, fields);
|
|
5466
|
+
}
|
|
5467
|
+
return Promise.resolve(alignRowsToIds(rows, ids).map((row, i) => row ? projectRow$2(row, new Set([...fields, ...Object.keys(ids[i])])) : void 0));
|
|
5468
|
+
}
|
|
5469
|
+
};
|
|
5470
|
+
}
|
|
5471
|
+
/**
|
|
5472
|
+
* The shared resolver behind query targets and {@link resolveQuery}: the
|
|
5473
|
+
* query body validated, checked and run as a READ of this controller, then
|
|
5474
|
+
* ONE read of `select` ordered by `sort` — `filter (+ $search) ∧ overlay ∧
|
|
5475
|
+
* scope ∧ ¬exclude`, at most `cap + 1` rows. More than `cap` → 400
|
|
5476
|
+
* `TARGET_TOO_LARGE`; a count other than `expectCount` → 409
|
|
5477
|
+
* `TARGET_CHANGED`.
|
|
5478
|
+
*
|
|
5479
|
+
* Everything that depends on the read's visibility — the `$search`
|
|
5480
|
+
* fallback, the native-search memo, the un-appliable-term refusal and the
|
|
5481
|
+
* delegated identity — is computed INSIDE the read child, where the
|
|
5482
|
+
* permission layer's per-request state is the read's.
|
|
5483
|
+
*/
|
|
5484
|
+
async _resolveMatching(spec) {
|
|
5485
|
+
const { label, body } = spec;
|
|
5486
|
+
const cap = Math.min(spec.cap, body.maxRows ?? Infinity);
|
|
5409
5487
|
const parsed = this.parseUrlOr400(body.q.startsWith("?") ? body.q.slice(1) : body.q);
|
|
5410
5488
|
const controls = {};
|
|
5411
5489
|
for (const [k, v] of Object.entries(parsed.controls ?? {})) {
|
|
5412
5490
|
if (v === void 0) continue;
|
|
5413
|
-
if (k !== "$search" && k !== "$index") throw targetInvalid(
|
|
5414
|
-
if (k === "$search" && typeof v !== "string" && typeof v !== "number") throw targetInvalid(
|
|
5491
|
+
if (k !== "$search" && k !== "$index") throw targetInvalid(label, `A query target takes a filter, $search and $index only — "${k}" is not accepted`);
|
|
5492
|
+
if (k === "$search" && typeof v !== "string" && typeof v !== "number") throw targetInvalid(label, "$search must be a search term");
|
|
5415
5493
|
controls[k] = k === "$search" ? `${v}` : v;
|
|
5416
5494
|
}
|
|
5417
|
-
if (controls.$index !== void 0 && typeof controls.$index !== "string") throw targetInvalid(
|
|
5495
|
+
if (controls.$index !== void 0 && typeof controls.$index !== "string") throw targetInvalid(label, "$index must be an index name");
|
|
5418
5496
|
const exclude = body.exclude ?? [];
|
|
5419
|
-
const shapes =
|
|
5497
|
+
const shapes = spec.excludeShapes ?? [];
|
|
5498
|
+
const sealedSet = () => new Set([...this.fieldVisibility.sealedFor(this.readable), ...this._leavesOf(this.readable).filter((leaf) => !this.fieldVisibility.isVisible(leaf))]);
|
|
5420
5499
|
const check = () => {
|
|
5500
|
+
let sealed;
|
|
5501
|
+
const readableLeaf = (leaf) => !(sealed ??= sealedSet()).has(leaf);
|
|
5421
5502
|
const controlsError = this.validateControls(controls, "query");
|
|
5422
5503
|
if (controlsError) throw new _moostjs_event_http.HttpError(400, controlsError);
|
|
5423
5504
|
const gateError = this.checkCapabilities({
|
|
@@ -5425,71 +5506,160 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5425
5506
|
controls
|
|
5426
5507
|
});
|
|
5427
5508
|
if (gateError) throw gateError;
|
|
5428
|
-
if (exclude.length > 0)
|
|
5429
|
-
|
|
5430
|
-
fields
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5509
|
+
if (exclude.length > 0) {
|
|
5510
|
+
const source = shapes.length === 0 ? this.idSource : {
|
|
5511
|
+
identifications: [...this.idSource.identifications, ...shapes.map((fields) => ({
|
|
5512
|
+
fields,
|
|
5513
|
+
source: "target"
|
|
5514
|
+
}))],
|
|
5515
|
+
fieldDescriptors: this.readable.fieldDescriptors
|
|
5516
|
+
};
|
|
5517
|
+
try {
|
|
5518
|
+
validateMultiId(exclude, source, spec.maxExclude);
|
|
5519
|
+
} catch (error) {
|
|
5520
|
+
throw validatorErrorToHttp(error) ?? error;
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
5523
|
+
const nav = FieldCapabilityIndex.navPathsOf(this.readable);
|
|
5524
|
+
for (const path of spec.selectGate ?? []) {
|
|
5525
|
+
if (this.capabilities.decorationCap(path) || (0, _atscript_db.selfOrAncestor)(path, nav) !== void 0) throw badRequest(path, `Unknown field "${path}"`);
|
|
5526
|
+
const verdict = this.capabilities.check(path, "select", this.fieldVisibility.isVisible);
|
|
5527
|
+
if (verdict) throw badRequest(verdict.path, verdict.message);
|
|
5528
|
+
if (this._writeOnlySet.has(path)) throw badRequest(path, `Field "${path}" is @db.writeOnly`);
|
|
5529
|
+
const prefix = `${path}.`;
|
|
5530
|
+
const leaves = this._leavesOf(this.readable).filter((leaf) => leaf.startsWith(prefix));
|
|
5531
|
+
if (leaves.length > 0 && !leaves.some((leaf) => readableLeaf(leaf))) throw badRequest(path, `Unknown field "${path}"`);
|
|
5532
|
+
}
|
|
5533
|
+
for (const path of spec.sortGate ?? []) {
|
|
5534
|
+
const verdict = this.capabilities.check(path, "sort", this.fieldVisibility.isVisible);
|
|
5535
|
+
if (verdict) throw targetInvalid(label, verdict.message);
|
|
5536
|
+
}
|
|
5435
5537
|
};
|
|
5436
|
-
const ownScope =
|
|
5437
|
-
const [
|
|
5538
|
+
const ownScope = spec.scopeByAction === true && (spec.overlay === "action" || this.queryTargetScope !== _AsDbReadableController.prototype.queryTargetScope);
|
|
5539
|
+
const [read, overlay] = await Promise.all([this._asRead(controls, parsed.filter, async () => {
|
|
5438
5540
|
check();
|
|
5439
|
-
const [clientFilter, readScope] = await Promise.all([
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5541
|
+
const [clientFilter, readScope, strategy] = await Promise.all([
|
|
5542
|
+
this._relationOverlay(parsed),
|
|
5543
|
+
ownScope ? this.queryTargetScope(label) : void 0,
|
|
5544
|
+
this._resolveReadStrategy(controls)
|
|
5545
|
+
]);
|
|
5546
|
+
const base = spec.overlay === "action" ? clientFilter : await this.transformFilter(clientFilter ?? {});
|
|
5547
|
+
const searched = this.applySearchFallback(base, controls);
|
|
5548
|
+
if (controls.$search && strategy.kind !== "search" && searched === base) throw targetInvalid(label, "$search is not available here");
|
|
5549
|
+
const visibleOf = spec.visibleOf?.filter((f) => this.fieldVisibility.isVisible(f));
|
|
5550
|
+
let select = spec.select;
|
|
5551
|
+
if (spec.selectGate) {
|
|
5552
|
+
const sealed = sealedSet();
|
|
5553
|
+
for (const id of spec.identity ?? []) sealed.delete(id);
|
|
5554
|
+
select = this._sealSelect([...spec.select], sealed, this.readable);
|
|
5555
|
+
}
|
|
5556
|
+
return {
|
|
5557
|
+
searched,
|
|
5558
|
+
readScope,
|
|
5559
|
+
strategy,
|
|
5560
|
+
visibleOf,
|
|
5561
|
+
select
|
|
5562
|
+
};
|
|
5563
|
+
}, spec.routeParams), spec.overlay === "action" ? this.rowOverlay() : void 0]);
|
|
5564
|
+
const { strategy } = read;
|
|
5565
|
+
const filter = (0, _atscript_db.andFilters)(read.searched, overlay, read.readScope, spec.scope, exclude.length > 0 ? { $not: { $or: exclude } } : void 0);
|
|
5444
5566
|
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
5567
|
const rows = await findMany({
|
|
5449
5568
|
filter,
|
|
5450
5569
|
controls: {
|
|
5451
|
-
$select: [...
|
|
5452
|
-
$sort:
|
|
5570
|
+
$select: [...read.select],
|
|
5571
|
+
$sort: Object.fromEntries(spec.sortBy.map((f) => [f, 1])),
|
|
5453
5572
|
$limit: cap + 1
|
|
5454
5573
|
}
|
|
5455
5574
|
});
|
|
5456
|
-
if (rows.length > cap) throw new ActionTargetError("TARGET_TOO_LARGE",
|
|
5457
|
-
if (body.expectCount !== void 0 && body.expectCount !== rows.length) throw new ActionTargetError("TARGET_CHANGED",
|
|
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;
|
|
5575
|
+
if (rows.length > cap) throw new ActionTargetError("TARGET_TOO_LARGE", label, `The query matches more than ${cap} rows`, { cap });
|
|
5576
|
+
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
5577
|
return {
|
|
5468
|
-
matched: rows.length,
|
|
5469
5578
|
rows,
|
|
5470
|
-
|
|
5579
|
+
filter,
|
|
5580
|
+
findMany,
|
|
5471
5581
|
exclude,
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
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
|
-
}
|
|
5582
|
+
visibleOf: read.visibleOf,
|
|
5583
|
+
cap,
|
|
5584
|
+
select: read.select
|
|
5482
5585
|
};
|
|
5483
5586
|
}
|
|
5484
5587
|
/**
|
|
5588
|
+
* Rows of THIS controller matching `q`, resolved as a READ of it for the
|
|
5589
|
+
* current event's caller (since 0.1.149) — from your own command, e.g. to
|
|
5590
|
+
* act on "every issue matching this search". `q` is a `GET /query` string
|
|
5591
|
+
* (`$search` / `$index` and a filter only) or a query-target envelope
|
|
5592
|
+
* `{ q, exclude?, expectCount?, maxRows? }` (no `dryRun`).
|
|
5593
|
+
*
|
|
5594
|
+
* The read runs under this controller's full read policy
|
|
5595
|
+
* ({@link prepareRequest} with `endpoint: "query"`, {@link hasField},
|
|
5596
|
+
* {@link validateControls}, the capability / index gate and the
|
|
5597
|
+
* {@link transformFilter} overlay) with the current event's identity.
|
|
5598
|
+
* Route interceptors and guards of the `query` route do not run; read
|
|
5599
|
+
* authorization belongs in {@link prepareRequest}. {@link queryTargetScope}
|
|
5600
|
+
* is not called. Hooks see no route params of the caller (only a call from
|
|
5601
|
+
* the routed event's own controller instance keeps its params); pass route-derived
|
|
5602
|
+
* restrictions as `opts.scope`. Joins the caller's open transaction.
|
|
5603
|
+
*
|
|
5604
|
+
* Rows are ordered by identity (`preferredId`, else the primary key) and
|
|
5605
|
+
* carry the identity fields plus `opts.select` (gated like `/query`
|
|
5606
|
+
* `$select`; `transformProjection` is not applied — hide fields with
|
|
5607
|
+
* {@link hasField}; decoration keys and navigation paths are refused). An
|
|
5608
|
+
* identity-less readable is ordered by `select` (each path sortable, else
|
|
5609
|
+
* `TARGET_INVALID`). More than `opts.cap` (default 1000) rows → 400
|
|
5610
|
+
* `TARGET_TOO_LARGE`; a count other than `expectCount` → 409
|
|
5611
|
+
* `TARGET_CHANGED`; a `$search` that can't be applied → 400
|
|
5612
|
+
* `TARGET_INVALID`. Must be awaited inside a running event handler.
|
|
5613
|
+
*/
|
|
5614
|
+
async resolveQuery(q, opts = {}) {
|
|
5615
|
+
let caller;
|
|
5616
|
+
try {
|
|
5617
|
+
caller = (0, _wooksjs_event_core.current)();
|
|
5618
|
+
} catch {
|
|
5619
|
+
throw new Error("[moost-db] resolveQuery must be awaited inside an event handler");
|
|
5620
|
+
}
|
|
5621
|
+
const cap = opts.cap ?? 1e3;
|
|
5622
|
+
if (!Number.isInteger(cap) || cap < 1) throw new Error("[moost-db] resolveQuery: `cap` must be a positive integer");
|
|
5623
|
+
const select = opts.select ?? [];
|
|
5624
|
+
if (!Array.isArray(select) || select.some((p) => typeof p !== "string")) throw new Error("[moost-db] resolveQuery: `select` must be an array of field paths");
|
|
5625
|
+
const ids = this.readable.preferredId?.length ? this.readable.preferredId : this.readable.primaryKeys;
|
|
5626
|
+
const order = ids.length > 0 ? ids : select;
|
|
5627
|
+
if (order.length === 0) throw new Error("[moost-db] resolveQuery: this readable has no identity — pass `select` (rows are ordered by it)");
|
|
5628
|
+
const label = readCurrentActionMeta(caller)?.name ?? "";
|
|
5629
|
+
const body = parseQueryTargetBody(label, typeof q === "string" ? { q } : q);
|
|
5630
|
+
if (body.dryRun !== void 0) throw targetInvalid(label, "resolveQuery takes no `dryRun` — read `query.dryRun` yourself");
|
|
5631
|
+
let sameRoute = false;
|
|
5632
|
+
try {
|
|
5633
|
+
sameRoute = controllerOf(caller) === this && routedController(caller) === this;
|
|
5634
|
+
} catch {}
|
|
5635
|
+
const fields = [...new Set([...ids, ...select])];
|
|
5636
|
+
const { rows, select: sealedSelect } = await this._resolveMatching({
|
|
5637
|
+
label,
|
|
5638
|
+
body,
|
|
5639
|
+
cap,
|
|
5640
|
+
maxExclude: DEFAULT_MAX_ACTION_IDS,
|
|
5641
|
+
overlay: "read",
|
|
5642
|
+
scope: opts.scope,
|
|
5643
|
+
select: fields,
|
|
5644
|
+
sortBy: order,
|
|
5645
|
+
selectGate: select,
|
|
5646
|
+
identity: ids,
|
|
5647
|
+
sortGate: ids.length > 0 ? void 0 : order,
|
|
5648
|
+
routeParams: sameRoute ? void 0 : {}
|
|
5649
|
+
});
|
|
5650
|
+
return rows.map((row) => projectRow$2(row, sealedSelect));
|
|
5651
|
+
}
|
|
5652
|
+
/**
|
|
5485
5653
|
* Runs `fn` as a READ of this controller (since 0.1.147): in a child of
|
|
5486
5654
|
* the current event whose controller context is this controller's `query`
|
|
5487
5655
|
* handler, after `prepareRequest({ endpoint: "query", controls, filter })` — the
|
|
5488
5656
|
* request-scoped state a permission layer builds there (read grant, field
|
|
5489
|
-
* visibility) is the read's and stays in the child.
|
|
5657
|
+
* visibility) is the read's and stays in the child. `routeParams` (since
|
|
5658
|
+
* 0.1.149) replaces the route params the child's hooks read.
|
|
5490
5659
|
*/
|
|
5491
|
-
_asRead(controls, filter, fn) {
|
|
5660
|
+
_asRead(controls, filter, fn, routeParams) {
|
|
5492
5661
|
return runAsController(this, "query", async () => {
|
|
5662
|
+
if (routeParams) (0, _wooksjs_event_core.current)().set(_wooksjs_event_core.routeParamsKey, routeParams);
|
|
5493
5663
|
if (typeof this.prepareRequest === "function") await this.prepareRequest(readRequestContext("query", controls, filter));
|
|
5494
5664
|
return fn();
|
|
5495
5665
|
});
|
|
@@ -5573,7 +5743,10 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5573
5743
|
* with the existing filter. Applies only when native search does not serve
|
|
5574
5744
|
* the request (no native search, or — since 0.1.143 — its default index
|
|
5575
5745
|
* reads a field {@link hasField} hides) and the request isn't a vector
|
|
5576
|
-
* search (`$vector` consumes the term).
|
|
5746
|
+
* search (`$vector` consumes the term). Lenient on list endpoints: a term
|
|
5747
|
+
* nothing can apply is ignored. Resolvers (query targets, `resolveQuery`)
|
|
5748
|
+
* refuse it — a subclass override that applies the term must return a new
|
|
5749
|
+
* filter object.
|
|
5577
5750
|
*/
|
|
5578
5751
|
applySearchFallback(filter, controls) {
|
|
5579
5752
|
const term = controls.$search;
|
|
@@ -6363,7 +6536,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
6363
6536
|
maxExclude: limits.maxIds,
|
|
6364
6537
|
overlay: "read",
|
|
6365
6538
|
select: [...new Set([...identity, ...delegation.paths])],
|
|
6366
|
-
excludeShapes: [delegation.paths]
|
|
6539
|
+
excludeShapes: [delegation.paths],
|
|
6540
|
+
visibleOf: identity
|
|
6367
6541
|
});
|
|
6368
6542
|
if (resolved.dryRun) return { matched: resolved.matched };
|
|
6369
6543
|
const summary = {
|
|
@@ -6373,7 +6547,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
6373
6547
|
failed: []
|
|
6374
6548
|
};
|
|
6375
6549
|
const { ids, index } = mapToSourceIds(resolved.rows, delegation.idMap);
|
|
6376
|
-
const visibleIdentity =
|
|
6550
|
+
const visibleIdentity = resolved.visibleOf ?? [];
|
|
6377
6551
|
for (let i = 0; i < index.length; i++) {
|
|
6378
6552
|
if (index[i] >= 0) continue;
|
|
6379
6553
|
const row = resolved.rows[i];
|
package/dist/index.d.cts
CHANGED
|
@@ -385,6 +385,12 @@ type TDbQueryTargetOpts = boolean | {
|
|
|
385
385
|
* registered symbol, like `ACTION_OVERLAY`).
|
|
386
386
|
*/
|
|
387
387
|
declare const RESOLVE_TARGET: unique symbol;
|
|
388
|
+
/**
|
|
389
|
+
* `q` of {@link AsDbReadableController.resolveQuery}: a `GET /query` string, or a
|
|
390
|
+
* query-target envelope without `dryRun`.
|
|
391
|
+
* @since 0.1.149
|
|
392
|
+
*/
|
|
393
|
+
type TDbResolveQueryInput = string | Omit<TDbActionQueryTarget, "dryRun">;
|
|
388
394
|
/** What {@link RESOLVE_TARGET} resolves. */
|
|
389
395
|
interface TTargetRequest {
|
|
390
396
|
action: string;
|
|
@@ -400,6 +406,8 @@ interface TTargetRequest {
|
|
|
400
406
|
* rows); `"read"` — `transformFilter` (a view resolving rows it delegates).
|
|
401
407
|
*/
|
|
402
408
|
overlay: "action" | "read";
|
|
409
|
+
/** Identity fields to report the READ-visible subset of ({@link TResolvedTarget.visibleOf}). */
|
|
410
|
+
visibleOf?: readonly string[];
|
|
403
411
|
/** Fields of the phase-1 read (sorted by the first identification's fields). */
|
|
404
412
|
select: readonly string[];
|
|
405
413
|
/** Key sets `exclude` entries may use besides the controller's identifications. */
|
|
@@ -414,6 +422,8 @@ interface TResolvedTarget {
|
|
|
414
422
|
dryRun: boolean;
|
|
415
423
|
/** The validated `exclude` entries (already applied to {@link rows}). */
|
|
416
424
|
exclude: Record<string, unknown>[];
|
|
425
|
+
/** {@link TTargetRequest.visibleOf} filtered by the read's field visibility. */
|
|
426
|
+
visibleOf?: readonly string[];
|
|
417
427
|
/**
|
|
418
428
|
* The rows `ids` address that STILL match the target (filter, search,
|
|
419
429
|
* overlay, `queryTargetScope`, `exclude`), aligned with `ids`; `select`
|
|
@@ -940,6 +950,15 @@ declare class FieldCapabilityIndex implements TQueryPathSource {
|
|
|
940
950
|
}
|
|
941
951
|
//#endregion
|
|
942
952
|
//#region src/as-db-readable.controller.d.ts
|
|
953
|
+
/** Options of {@link AsDbReadableController.resolveQuery}. @since 0.1.149 */
|
|
954
|
+
interface TDbResolveQueryOpts<K extends string = string> {
|
|
955
|
+
/** Fields to return besides the identity. Gated like `/query` `$select` under `hasField`. */
|
|
956
|
+
select?: readonly K[];
|
|
957
|
+
/** Most rows (default 1000); `q.maxRows` can only lower it. */
|
|
958
|
+
cap?: number;
|
|
959
|
+
/** Server-side restriction, ANDed in. Trusted: not gated, not shown to `prepareRequest`. */
|
|
960
|
+
scope?: FilterExpr;
|
|
961
|
+
}
|
|
943
962
|
/** Read endpoint a {@link AsDbReadableController.decorateRows} call serves. */
|
|
944
963
|
type TDbDecorateEndpoint = "query" | "pages" | "geo" | "one";
|
|
945
964
|
/**
|
|
@@ -1168,7 +1187,9 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
1168
1187
|
* (`$vector`) or a geo index (`/geo`, `$index`) reading a hidden path
|
|
1169
1188
|
* answers exactly like a nonexistent index (400); a hidden DEFAULT text
|
|
1170
1189
|
* index falls back to the `@db.column.searchable` substring search over
|
|
1171
|
-
* visible fields (or ignores the term when there are none
|
|
1190
|
+
* visible fields (or ignores the term when there are none — on list
|
|
1191
|
+
* endpoints; query targets, delegated targets and {@link resolveQuery}
|
|
1192
|
+
* answer 400 `TARGET_INVALID` instead). A
|
|
1172
1193
|
* `@db.column.derived` field is visible only while its source path is,
|
|
1173
1194
|
* and one whose source is hidden is sealed out of every read projection
|
|
1174
1195
|
* for the request, like a `@db.writeOnly` field. The same holds for a
|
|
@@ -1451,6 +1472,8 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
1451
1472
|
* {@link checkCapabilities}, {@link hasField}), where the client
|
|
1452
1473
|
* predicates' {@link transformRelationFilter} also runs: a query target
|
|
1453
1474
|
* never filters on, nor counts by, a field the caller can't read.
|
|
1475
|
+
* {@link resolveQuery} does not call it (there is no action of this
|
|
1476
|
+
* controller to scope).
|
|
1454
1477
|
*
|
|
1455
1478
|
* @since 0.1.147
|
|
1456
1479
|
*/
|
|
@@ -1567,12 +1590,55 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
1567
1590
|
* the first batch, which directly follows the snapshot.
|
|
1568
1591
|
*/
|
|
1569
1592
|
[RESOLVE_TARGET](req: TTargetRequest): Promise<TResolvedTarget>;
|
|
1593
|
+
/**
|
|
1594
|
+
* The shared resolver behind query targets and {@link resolveQuery}: the
|
|
1595
|
+
* query body validated, checked and run as a READ of this controller, then
|
|
1596
|
+
* ONE read of `select` ordered by `sort` — `filter (+ $search) ∧ overlay ∧
|
|
1597
|
+
* scope ∧ ¬exclude`, at most `cap + 1` rows. More than `cap` → 400
|
|
1598
|
+
* `TARGET_TOO_LARGE`; a count other than `expectCount` → 409
|
|
1599
|
+
* `TARGET_CHANGED`.
|
|
1600
|
+
*
|
|
1601
|
+
* Everything that depends on the read's visibility — the `$search`
|
|
1602
|
+
* fallback, the native-search memo, the un-appliable-term refusal and the
|
|
1603
|
+
* delegated identity — is computed INSIDE the read child, where the
|
|
1604
|
+
* permission layer's per-request state is the read's.
|
|
1605
|
+
*/
|
|
1606
|
+
private _resolveMatching;
|
|
1607
|
+
/**
|
|
1608
|
+
* Rows of THIS controller matching `q`, resolved as a READ of it for the
|
|
1609
|
+
* current event's caller (since 0.1.149) — from your own command, e.g. to
|
|
1610
|
+
* act on "every issue matching this search". `q` is a `GET /query` string
|
|
1611
|
+
* (`$search` / `$index` and a filter only) or a query-target envelope
|
|
1612
|
+
* `{ q, exclude?, expectCount?, maxRows? }` (no `dryRun`).
|
|
1613
|
+
*
|
|
1614
|
+
* The read runs under this controller's full read policy
|
|
1615
|
+
* ({@link prepareRequest} with `endpoint: "query"`, {@link hasField},
|
|
1616
|
+
* {@link validateControls}, the capability / index gate and the
|
|
1617
|
+
* {@link transformFilter} overlay) with the current event's identity.
|
|
1618
|
+
* Route interceptors and guards of the `query` route do not run; read
|
|
1619
|
+
* authorization belongs in {@link prepareRequest}. {@link queryTargetScope}
|
|
1620
|
+
* is not called. Hooks see no route params of the caller (only a call from
|
|
1621
|
+
* the routed event's own controller instance keeps its params); pass route-derived
|
|
1622
|
+
* restrictions as `opts.scope`. Joins the caller's open transaction.
|
|
1623
|
+
*
|
|
1624
|
+
* Rows are ordered by identity (`preferredId`, else the primary key) and
|
|
1625
|
+
* carry the identity fields plus `opts.select` (gated like `/query`
|
|
1626
|
+
* `$select`; `transformProjection` is not applied — hide fields with
|
|
1627
|
+
* {@link hasField}; decoration keys and navigation paths are refused). An
|
|
1628
|
+
* identity-less readable is ordered by `select` (each path sortable, else
|
|
1629
|
+
* `TARGET_INVALID`). More than `opts.cap` (default 1000) rows → 400
|
|
1630
|
+
* `TARGET_TOO_LARGE`; a count other than `expectCount` → 409
|
|
1631
|
+
* `TARGET_CHANGED`; a `$search` that can't be applied → 400
|
|
1632
|
+
* `TARGET_INVALID`. Must be awaited inside a running event handler.
|
|
1633
|
+
*/
|
|
1634
|
+
resolveQuery<K extends string = never>(q: TDbResolveQueryInput, opts?: TDbResolveQueryOpts<K>): Promise<Array<Pick<DataType, K & keyof DataType> & Record<string, unknown>>>;
|
|
1570
1635
|
/**
|
|
1571
1636
|
* Runs `fn` as a READ of this controller (since 0.1.147): in a child of
|
|
1572
1637
|
* the current event whose controller context is this controller's `query`
|
|
1573
1638
|
* handler, after `prepareRequest({ endpoint: "query", controls, filter })` — the
|
|
1574
1639
|
* request-scoped state a permission layer builds there (read grant, field
|
|
1575
|
-
* visibility) is the read's and stays in the child.
|
|
1640
|
+
* visibility) is the read's and stays in the child. `routeParams` (since
|
|
1641
|
+
* 0.1.149) replaces the route params the child's hooks read.
|
|
1576
1642
|
*/
|
|
1577
1643
|
private _asRead;
|
|
1578
1644
|
/**
|
|
@@ -1598,7 +1664,10 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
1598
1664
|
* with the existing filter. Applies only when native search does not serve
|
|
1599
1665
|
* the request (no native search, or — since 0.1.143 — its default index
|
|
1600
1666
|
* reads a field {@link hasField} hides) and the request isn't a vector
|
|
1601
|
-
* search (`$vector` consumes the term).
|
|
1667
|
+
* search (`$vector` consumes the term). Lenient on list endpoints: a term
|
|
1668
|
+
* nothing can apply is ignored. Resolvers (query targets, `resolveQuery`)
|
|
1669
|
+
* refuse it — a subclass override that applies the term must return a new
|
|
1670
|
+
* filter object.
|
|
1602
1671
|
*/
|
|
1603
1672
|
protected applySearchFallback(filter: FilterExpr | undefined, controls: Record<string, unknown>): FilterExpr | undefined;
|
|
1604
1673
|
/**
|
|
@@ -3419,4 +3488,4 @@ declare const REL_FILTER_CLIENT_MAX_DEPTH = 3;
|
|
|
3419
3488
|
*/
|
|
3420
3489
|
declare const REL_FILTER_CLIENT_MAX_NODES = 8;
|
|
3421
3490
|
//#endregion
|
|
3422
|
-
export { ActionDisabledError, type ActionDisabledErrorBody, ActionTargetError, type ActionTargetErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, type AtscriptDbMate, type AtscriptDbMeta, type AtscriptDbParamsMeta, DB_CRUD_HANDLERS, DEFAULT_DB_SPACE, DbAction, DbActionDefault, type DbActionEnvelope, DbActionID, DbActionIDs, type DbActionOpts, DbActionRow, DbActionRows, DbActionTarget, DbActions, DbActionsFrom, DbDecorations, DbRowActions, DbRowsActions, DbTableActions, FieldCapabilityIndex, type IdValidationSource, InputForm, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, REL_FILTER_CLIENT_MAX_DEPTH, REL_FILTER_CLIENT_MAX_NODES, ReadableController, TABLE_DEF, type TActionTargetErrorCode, TAssertExposedOptions, type TCapabilityReadable, type TCapabilityVerdict, TControllerBindingOptions, type TCrudOp, type TCrudPermissions, type TDbActionDisabledVerdict, type TDbActionEnvelope, type TDbActionInfo, type TDbActionInputFormMeta, type TDbActionIntent, type TDbActionLevel, type TDbActionMeta, type TDbActionParamKind, type TDbActionProcessor, type TDbActionQueryTarget, type TDbActionScopeContext, type TDbActionScopePurpose, type TDbActionTarget, type TDbActionTargetSummary, type TDbActionsEntry, type TDbActionsEntryUnpinned, type TDbActionsFromMeta, type TDbActionsFromOpts, type TDbAvailableActions, type TDbClassActionMeta, TDbControlsType, TDbDecorateContext, TDbDecorateEndpoint, type TDbDecorationsOpts, TDbFieldVisibility, TDbIndexFieldPaths, TDbParsedRequest, type TDbQueryTargetOpts, type TDbRemoveGuardContext, TDbRequestContext, TDbRequestEndpoint, type TDbRowIdInput, type TDbRowIdPurpose, type TDbRowIdsContext, type TDbWriteAction, type TDbWriteCheckContext, type TDbWriteGuardContext, type TFieldCapability, type TGateOp, type THttpErrorEntry, type TQueryPathOp, type TQueryPathRefs, TReadableBinding, type TReadableBindingMeta, type TTerminalRef, TableController, UseValidationErrorTransform, VALUE_HELP_CRUD_HANDLERS, ValueHelpQuery, ValueHelpSelect, ViewController, applyTerminalRefs, assertExposed, badRequest, clearDbSpaces, closeDbSpaces, collectQueryPaths, dbActionBodySlot, dbActionInputSlot, discoverActions, discoverRowLevelActions, errorEnvelope, findReadableBinding, getAtscriptDbMate, getControllerFormType, getDbEndpoint, hasActionDelegations, perRow, provideDbSpace, resolveBoundReadable, resolveDbSpace, resolveProp, resolveTerminalRef, unknownRelationError, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, useDbActionTarget, validationErrorTransform };
|
|
3491
|
+
export { ActionDisabledError, type ActionDisabledErrorBody, ActionTargetError, type ActionTargetErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, type AtscriptDbMate, type AtscriptDbMeta, type AtscriptDbParamsMeta, DB_CRUD_HANDLERS, DEFAULT_DB_SPACE, DbAction, DbActionDefault, type DbActionEnvelope, DbActionID, DbActionIDs, type DbActionOpts, DbActionRow, DbActionRows, DbActionTarget, DbActions, DbActionsFrom, DbDecorations, DbRowActions, DbRowsActions, DbTableActions, FieldCapabilityIndex, type IdValidationSource, InputForm, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, REL_FILTER_CLIENT_MAX_DEPTH, REL_FILTER_CLIENT_MAX_NODES, ReadableController, TABLE_DEF, type TActionTargetErrorCode, TAssertExposedOptions, type TCapabilityReadable, type TCapabilityVerdict, TControllerBindingOptions, type TCrudOp, type TCrudPermissions, type TDbActionDisabledVerdict, type TDbActionEnvelope, type TDbActionInfo, type TDbActionInputFormMeta, type TDbActionIntent, type TDbActionLevel, type TDbActionMeta, type TDbActionParamKind, type TDbActionProcessor, type TDbActionQueryTarget, type TDbActionScopeContext, type TDbActionScopePurpose, type TDbActionTarget, type TDbActionTargetSummary, type TDbActionsEntry, type TDbActionsEntryUnpinned, type TDbActionsFromMeta, type TDbActionsFromOpts, type TDbAvailableActions, type TDbClassActionMeta, TDbControlsType, TDbDecorateContext, TDbDecorateEndpoint, type TDbDecorationsOpts, TDbFieldVisibility, TDbIndexFieldPaths, TDbParsedRequest, type TDbQueryTargetOpts, type TDbRemoveGuardContext, TDbRequestContext, TDbRequestEndpoint, type TDbResolveQueryInput, TDbResolveQueryOpts, type TDbRowIdInput, type TDbRowIdPurpose, type TDbRowIdsContext, type TDbWriteAction, type TDbWriteCheckContext, type TDbWriteGuardContext, type TFieldCapability, type TGateOp, type THttpErrorEntry, type TQueryPathOp, type TQueryPathRefs, TReadableBinding, type TReadableBindingMeta, type TTerminalRef, TableController, UseValidationErrorTransform, VALUE_HELP_CRUD_HANDLERS, ValueHelpQuery, ValueHelpSelect, ViewController, applyTerminalRefs, assertExposed, badRequest, clearDbSpaces, closeDbSpaces, collectQueryPaths, dbActionBodySlot, dbActionInputSlot, discoverActions, discoverRowLevelActions, errorEnvelope, findReadableBinding, getAtscriptDbMate, getControllerFormType, getDbEndpoint, hasActionDelegations, perRow, provideDbSpace, resolveBoundReadable, resolveDbSpace, resolveProp, resolveTerminalRef, unknownRelationError, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, useDbActionTarget, validationErrorTransform };
|
package/dist/index.d.mts
CHANGED
|
@@ -385,6 +385,12 @@ type TDbQueryTargetOpts = boolean | {
|
|
|
385
385
|
* registered symbol, like `ACTION_OVERLAY`).
|
|
386
386
|
*/
|
|
387
387
|
declare const RESOLVE_TARGET: unique symbol;
|
|
388
|
+
/**
|
|
389
|
+
* `q` of {@link AsDbReadableController.resolveQuery}: a `GET /query` string, or a
|
|
390
|
+
* query-target envelope without `dryRun`.
|
|
391
|
+
* @since 0.1.149
|
|
392
|
+
*/
|
|
393
|
+
type TDbResolveQueryInput = string | Omit<TDbActionQueryTarget, "dryRun">;
|
|
388
394
|
/** What {@link RESOLVE_TARGET} resolves. */
|
|
389
395
|
interface TTargetRequest {
|
|
390
396
|
action: string;
|
|
@@ -400,6 +406,8 @@ interface TTargetRequest {
|
|
|
400
406
|
* rows); `"read"` — `transformFilter` (a view resolving rows it delegates).
|
|
401
407
|
*/
|
|
402
408
|
overlay: "action" | "read";
|
|
409
|
+
/** Identity fields to report the READ-visible subset of ({@link TResolvedTarget.visibleOf}). */
|
|
410
|
+
visibleOf?: readonly string[];
|
|
403
411
|
/** Fields of the phase-1 read (sorted by the first identification's fields). */
|
|
404
412
|
select: readonly string[];
|
|
405
413
|
/** Key sets `exclude` entries may use besides the controller's identifications. */
|
|
@@ -414,6 +422,8 @@ interface TResolvedTarget {
|
|
|
414
422
|
dryRun: boolean;
|
|
415
423
|
/** The validated `exclude` entries (already applied to {@link rows}). */
|
|
416
424
|
exclude: Record<string, unknown>[];
|
|
425
|
+
/** {@link TTargetRequest.visibleOf} filtered by the read's field visibility. */
|
|
426
|
+
visibleOf?: readonly string[];
|
|
417
427
|
/**
|
|
418
428
|
* The rows `ids` address that STILL match the target (filter, search,
|
|
419
429
|
* overlay, `queryTargetScope`, `exclude`), aligned with `ids`; `select`
|
|
@@ -940,6 +950,15 @@ declare class FieldCapabilityIndex implements TQueryPathSource {
|
|
|
940
950
|
}
|
|
941
951
|
//#endregion
|
|
942
952
|
//#region src/as-db-readable.controller.d.ts
|
|
953
|
+
/** Options of {@link AsDbReadableController.resolveQuery}. @since 0.1.149 */
|
|
954
|
+
interface TDbResolveQueryOpts<K extends string = string> {
|
|
955
|
+
/** Fields to return besides the identity. Gated like `/query` `$select` under `hasField`. */
|
|
956
|
+
select?: readonly K[];
|
|
957
|
+
/** Most rows (default 1000); `q.maxRows` can only lower it. */
|
|
958
|
+
cap?: number;
|
|
959
|
+
/** Server-side restriction, ANDed in. Trusted: not gated, not shown to `prepareRequest`. */
|
|
960
|
+
scope?: FilterExpr;
|
|
961
|
+
}
|
|
943
962
|
/** Read endpoint a {@link AsDbReadableController.decorateRows} call serves. */
|
|
944
963
|
type TDbDecorateEndpoint = "query" | "pages" | "geo" | "one";
|
|
945
964
|
/**
|
|
@@ -1168,7 +1187,9 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
1168
1187
|
* (`$vector`) or a geo index (`/geo`, `$index`) reading a hidden path
|
|
1169
1188
|
* answers exactly like a nonexistent index (400); a hidden DEFAULT text
|
|
1170
1189
|
* index falls back to the `@db.column.searchable` substring search over
|
|
1171
|
-
* visible fields (or ignores the term when there are none
|
|
1190
|
+
* visible fields (or ignores the term when there are none — on list
|
|
1191
|
+
* endpoints; query targets, delegated targets and {@link resolveQuery}
|
|
1192
|
+
* answer 400 `TARGET_INVALID` instead). A
|
|
1172
1193
|
* `@db.column.derived` field is visible only while its source path is,
|
|
1173
1194
|
* and one whose source is hidden is sealed out of every read projection
|
|
1174
1195
|
* for the request, like a `@db.writeOnly` field. The same holds for a
|
|
@@ -1451,6 +1472,8 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
1451
1472
|
* {@link checkCapabilities}, {@link hasField}), where the client
|
|
1452
1473
|
* predicates' {@link transformRelationFilter} also runs: a query target
|
|
1453
1474
|
* never filters on, nor counts by, a field the caller can't read.
|
|
1475
|
+
* {@link resolveQuery} does not call it (there is no action of this
|
|
1476
|
+
* controller to scope).
|
|
1454
1477
|
*
|
|
1455
1478
|
* @since 0.1.147
|
|
1456
1479
|
*/
|
|
@@ -1567,12 +1590,55 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
1567
1590
|
* the first batch, which directly follows the snapshot.
|
|
1568
1591
|
*/
|
|
1569
1592
|
[RESOLVE_TARGET](req: TTargetRequest): Promise<TResolvedTarget>;
|
|
1593
|
+
/**
|
|
1594
|
+
* The shared resolver behind query targets and {@link resolveQuery}: the
|
|
1595
|
+
* query body validated, checked and run as a READ of this controller, then
|
|
1596
|
+
* ONE read of `select` ordered by `sort` — `filter (+ $search) ∧ overlay ∧
|
|
1597
|
+
* scope ∧ ¬exclude`, at most `cap + 1` rows. More than `cap` → 400
|
|
1598
|
+
* `TARGET_TOO_LARGE`; a count other than `expectCount` → 409
|
|
1599
|
+
* `TARGET_CHANGED`.
|
|
1600
|
+
*
|
|
1601
|
+
* Everything that depends on the read's visibility — the `$search`
|
|
1602
|
+
* fallback, the native-search memo, the un-appliable-term refusal and the
|
|
1603
|
+
* delegated identity — is computed INSIDE the read child, where the
|
|
1604
|
+
* permission layer's per-request state is the read's.
|
|
1605
|
+
*/
|
|
1606
|
+
private _resolveMatching;
|
|
1607
|
+
/**
|
|
1608
|
+
* Rows of THIS controller matching `q`, resolved as a READ of it for the
|
|
1609
|
+
* current event's caller (since 0.1.149) — from your own command, e.g. to
|
|
1610
|
+
* act on "every issue matching this search". `q` is a `GET /query` string
|
|
1611
|
+
* (`$search` / `$index` and a filter only) or a query-target envelope
|
|
1612
|
+
* `{ q, exclude?, expectCount?, maxRows? }` (no `dryRun`).
|
|
1613
|
+
*
|
|
1614
|
+
* The read runs under this controller's full read policy
|
|
1615
|
+
* ({@link prepareRequest} with `endpoint: "query"`, {@link hasField},
|
|
1616
|
+
* {@link validateControls}, the capability / index gate and the
|
|
1617
|
+
* {@link transformFilter} overlay) with the current event's identity.
|
|
1618
|
+
* Route interceptors and guards of the `query` route do not run; read
|
|
1619
|
+
* authorization belongs in {@link prepareRequest}. {@link queryTargetScope}
|
|
1620
|
+
* is not called. Hooks see no route params of the caller (only a call from
|
|
1621
|
+
* the routed event's own controller instance keeps its params); pass route-derived
|
|
1622
|
+
* restrictions as `opts.scope`. Joins the caller's open transaction.
|
|
1623
|
+
*
|
|
1624
|
+
* Rows are ordered by identity (`preferredId`, else the primary key) and
|
|
1625
|
+
* carry the identity fields plus `opts.select` (gated like `/query`
|
|
1626
|
+
* `$select`; `transformProjection` is not applied — hide fields with
|
|
1627
|
+
* {@link hasField}; decoration keys and navigation paths are refused). An
|
|
1628
|
+
* identity-less readable is ordered by `select` (each path sortable, else
|
|
1629
|
+
* `TARGET_INVALID`). More than `opts.cap` (default 1000) rows → 400
|
|
1630
|
+
* `TARGET_TOO_LARGE`; a count other than `expectCount` → 409
|
|
1631
|
+
* `TARGET_CHANGED`; a `$search` that can't be applied → 400
|
|
1632
|
+
* `TARGET_INVALID`. Must be awaited inside a running event handler.
|
|
1633
|
+
*/
|
|
1634
|
+
resolveQuery<K extends string = never>(q: TDbResolveQueryInput, opts?: TDbResolveQueryOpts<K>): Promise<Array<Pick<DataType, K & keyof DataType> & Record<string, unknown>>>;
|
|
1570
1635
|
/**
|
|
1571
1636
|
* Runs `fn` as a READ of this controller (since 0.1.147): in a child of
|
|
1572
1637
|
* the current event whose controller context is this controller's `query`
|
|
1573
1638
|
* handler, after `prepareRequest({ endpoint: "query", controls, filter })` — the
|
|
1574
1639
|
* request-scoped state a permission layer builds there (read grant, field
|
|
1575
|
-
* visibility) is the read's and stays in the child.
|
|
1640
|
+
* visibility) is the read's and stays in the child. `routeParams` (since
|
|
1641
|
+
* 0.1.149) replaces the route params the child's hooks read.
|
|
1576
1642
|
*/
|
|
1577
1643
|
private _asRead;
|
|
1578
1644
|
/**
|
|
@@ -1598,7 +1664,10 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
1598
1664
|
* with the existing filter. Applies only when native search does not serve
|
|
1599
1665
|
* the request (no native search, or — since 0.1.143 — its default index
|
|
1600
1666
|
* reads a field {@link hasField} hides) and the request isn't a vector
|
|
1601
|
-
* search (`$vector` consumes the term).
|
|
1667
|
+
* search (`$vector` consumes the term). Lenient on list endpoints: a term
|
|
1668
|
+
* nothing can apply is ignored. Resolvers (query targets, `resolveQuery`)
|
|
1669
|
+
* refuse it — a subclass override that applies the term must return a new
|
|
1670
|
+
* filter object.
|
|
1602
1671
|
*/
|
|
1603
1672
|
protected applySearchFallback(filter: FilterExpr | undefined, controls: Record<string, unknown>): FilterExpr | undefined;
|
|
1604
1673
|
/**
|
|
@@ -3419,4 +3488,4 @@ declare const REL_FILTER_CLIENT_MAX_DEPTH = 3;
|
|
|
3419
3488
|
*/
|
|
3420
3489
|
declare const REL_FILTER_CLIENT_MAX_NODES = 8;
|
|
3421
3490
|
//#endregion
|
|
3422
|
-
export { ActionDisabledError, type ActionDisabledErrorBody, ActionTargetError, type ActionTargetErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, type AtscriptDbMate, type AtscriptDbMeta, type AtscriptDbParamsMeta, DB_CRUD_HANDLERS, DEFAULT_DB_SPACE, DbAction, DbActionDefault, type DbActionEnvelope, DbActionID, DbActionIDs, type DbActionOpts, DbActionRow, DbActionRows, DbActionTarget, DbActions, DbActionsFrom, DbDecorations, DbRowActions, DbRowsActions, DbTableActions, FieldCapabilityIndex, type IdValidationSource, InputForm, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, REL_FILTER_CLIENT_MAX_DEPTH, REL_FILTER_CLIENT_MAX_NODES, ReadableController, TABLE_DEF, type TActionTargetErrorCode, TAssertExposedOptions, type TCapabilityReadable, type TCapabilityVerdict, TControllerBindingOptions, type TCrudOp, type TCrudPermissions, type TDbActionDisabledVerdict, type TDbActionEnvelope, type TDbActionInfo, type TDbActionInputFormMeta, type TDbActionIntent, type TDbActionLevel, type TDbActionMeta, type TDbActionParamKind, type TDbActionProcessor, type TDbActionQueryTarget, type TDbActionScopeContext, type TDbActionScopePurpose, type TDbActionTarget, type TDbActionTargetSummary, type TDbActionsEntry, type TDbActionsEntryUnpinned, type TDbActionsFromMeta, type TDbActionsFromOpts, type TDbAvailableActions, type TDbClassActionMeta, TDbControlsType, TDbDecorateContext, TDbDecorateEndpoint, type TDbDecorationsOpts, TDbFieldVisibility, TDbIndexFieldPaths, TDbParsedRequest, type TDbQueryTargetOpts, type TDbRemoveGuardContext, TDbRequestContext, TDbRequestEndpoint, type TDbRowIdInput, type TDbRowIdPurpose, type TDbRowIdsContext, type TDbWriteAction, type TDbWriteCheckContext, type TDbWriteGuardContext, type TFieldCapability, type TGateOp, type THttpErrorEntry, type TQueryPathOp, type TQueryPathRefs, TReadableBinding, type TReadableBindingMeta, type TTerminalRef, TableController, UseValidationErrorTransform, VALUE_HELP_CRUD_HANDLERS, ValueHelpQuery, ValueHelpSelect, ViewController, applyTerminalRefs, assertExposed, badRequest, clearDbSpaces, closeDbSpaces, collectQueryPaths, dbActionBodySlot, dbActionInputSlot, discoverActions, discoverRowLevelActions, errorEnvelope, findReadableBinding, getAtscriptDbMate, getControllerFormType, getDbEndpoint, hasActionDelegations, perRow, provideDbSpace, resolveBoundReadable, resolveDbSpace, resolveProp, resolveTerminalRef, unknownRelationError, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, useDbActionTarget, validationErrorTransform };
|
|
3491
|
+
export { ActionDisabledError, type ActionDisabledErrorBody, ActionTargetError, type ActionTargetErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, type AtscriptDbMate, type AtscriptDbMeta, type AtscriptDbParamsMeta, DB_CRUD_HANDLERS, DEFAULT_DB_SPACE, DbAction, DbActionDefault, type DbActionEnvelope, DbActionID, DbActionIDs, type DbActionOpts, DbActionRow, DbActionRows, DbActionTarget, DbActions, DbActionsFrom, DbDecorations, DbRowActions, DbRowsActions, DbTableActions, FieldCapabilityIndex, type IdValidationSource, InputForm, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, REL_FILTER_CLIENT_MAX_DEPTH, REL_FILTER_CLIENT_MAX_NODES, ReadableController, TABLE_DEF, type TActionTargetErrorCode, TAssertExposedOptions, type TCapabilityReadable, type TCapabilityVerdict, TControllerBindingOptions, type TCrudOp, type TCrudPermissions, type TDbActionDisabledVerdict, type TDbActionEnvelope, type TDbActionInfo, type TDbActionInputFormMeta, type TDbActionIntent, type TDbActionLevel, type TDbActionMeta, type TDbActionParamKind, type TDbActionProcessor, type TDbActionQueryTarget, type TDbActionScopeContext, type TDbActionScopePurpose, type TDbActionTarget, type TDbActionTargetSummary, type TDbActionsEntry, type TDbActionsEntryUnpinned, type TDbActionsFromMeta, type TDbActionsFromOpts, type TDbAvailableActions, type TDbClassActionMeta, TDbControlsType, TDbDecorateContext, TDbDecorateEndpoint, type TDbDecorationsOpts, TDbFieldVisibility, TDbIndexFieldPaths, TDbParsedRequest, type TDbQueryTargetOpts, type TDbRemoveGuardContext, TDbRequestContext, TDbRequestEndpoint, type TDbResolveQueryInput, TDbResolveQueryOpts, type TDbRowIdInput, type TDbRowIdPurpose, type TDbRowIdsContext, type TDbWriteAction, type TDbWriteCheckContext, type TDbWriteGuardContext, type TFieldCapability, type TGateOp, type THttpErrorEntry, type TQueryPathOp, type TQueryPathRefs, TReadableBinding, type TReadableBindingMeta, type TTerminalRef, TableController, UseValidationErrorTransform, VALUE_HELP_CRUD_HANDLERS, ValueHelpQuery, ValueHelpSelect, ViewController, applyTerminalRefs, assertExposed, badRequest, clearDbSpaces, closeDbSpaces, collectQueryPaths, dbActionBodySlot, dbActionInputSlot, discoverActions, discoverRowLevelActions, errorEnvelope, findReadableBinding, getAtscriptDbMate, getControllerFormType, getDbEndpoint, hasActionDelegations, perRow, provideDbSpace, resolveBoundReadable, resolveDbSpace, resolveProp, resolveTerminalRef, unknownRelationError, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, useDbActionTarget, validationErrorTransform };
|
package/dist/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { Body, Delete, Get, HttpError, MoostHttp, Patch, Post, Put, Query, Url }
|
|
|
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
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";
|
|
7
|
-
import { cached, current, defineWook, key, useRouteParams } from "@wooksjs/event-core";
|
|
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";
|
|
10
10
|
import { buildMemoryPredicate, projectRow, sortRows } from "@atscript/db-memory";
|
|
@@ -70,6 +70,13 @@ function insightError(insights, message) {
|
|
|
70
70
|
const entry = insightPaths.get(insights);
|
|
71
71
|
return entry?.message === message ? badRequest(entry.path, message) : new HttpError(400, message);
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The 400 envelope of a `ValidatorError`; `undefined` for any other error.
|
|
75
|
+
* @internal Not part of the public API (not re-exported from the barrel).
|
|
76
|
+
*/
|
|
77
|
+
function validatorErrorToHttp(error) {
|
|
78
|
+
return error instanceof ValidatorError ? errorEnvelope(400, error.message, error.errors) : void 0;
|
|
79
|
+
}
|
|
73
80
|
//#endregion
|
|
74
81
|
//#region src/validation-interceptor.ts
|
|
75
82
|
const dbErrorCodeToStatus = {
|
|
@@ -80,7 +87,8 @@ const dbErrorCodeToStatus = {
|
|
|
80
87
|
BUCKET_TZ_UNAVAILABLE: 501
|
|
81
88
|
};
|
|
82
89
|
function transformValidationError(error, reply) {
|
|
83
|
-
|
|
90
|
+
const validation = validatorErrorToHttp(error);
|
|
91
|
+
if (validation) reply(validation);
|
|
84
92
|
else if (error instanceof DbError) reply(errorEnvelope(dbErrorCodeToStatus[error.code] ?? 400, error.message, error.errors));
|
|
85
93
|
}
|
|
86
94
|
const validationErrorTransform = () => defineInterceptor({ error: transformValidationError }, TInterceptorPriority.BEFORE_ALL);
|
|
@@ -4189,6 +4197,17 @@ const PATH_OPS = [
|
|
|
4189
4197
|
"aggregate",
|
|
4190
4198
|
"bucket"
|
|
4191
4199
|
];
|
|
4200
|
+
/** The controller of the event that owns the route params (nearest ancestor that set them). */
|
|
4201
|
+
function routedController(ctx) {
|
|
4202
|
+
for (let c = ctx; c; c = c.parent) {
|
|
4203
|
+
try {
|
|
4204
|
+
c.getOwn(routeParamsKey);
|
|
4205
|
+
} catch {
|
|
4206
|
+
continue;
|
|
4207
|
+
}
|
|
4208
|
+
return controllerOf(c);
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4192
4211
|
/** The 400 of a filter / sort on a `@db.writeOnly` field. */
|
|
4193
4212
|
function writeOnlyError(path, op) {
|
|
4194
4213
|
const verdict = writeOnlyVerdict(path, op);
|
|
@@ -4464,7 +4483,9 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
4464
4483
|
* (`$vector`) or a geo index (`/geo`, `$index`) reading a hidden path
|
|
4465
4484
|
* answers exactly like a nonexistent index (400); a hidden DEFAULT text
|
|
4466
4485
|
* index falls back to the `@db.column.searchable` substring search over
|
|
4467
|
-
* visible fields (or ignores the term when there are none
|
|
4486
|
+
* visible fields (or ignores the term when there are none — on list
|
|
4487
|
+
* endpoints; query targets, delegated targets and {@link resolveQuery}
|
|
4488
|
+
* answer 400 `TARGET_INVALID` instead). A
|
|
4468
4489
|
* `@db.column.derived` field is visible only while its source path is,
|
|
4469
4490
|
* and one whose source is hidden is sealed out of every read projection
|
|
4470
4491
|
* for the request, like a `@db.writeOnly` field. The same holds for a
|
|
@@ -5067,6 +5088,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5067
5088
|
* {@link checkCapabilities}, {@link hasField}), where the client
|
|
5068
5089
|
* predicates' {@link transformRelationFilter} also runs: a query target
|
|
5069
5090
|
* never filters on, nor counts by, a field the caller can't read.
|
|
5091
|
+
* {@link resolveQuery} does not call it (there is no action of this
|
|
5092
|
+
* controller to scope).
|
|
5070
5093
|
*
|
|
5071
5094
|
* @since 0.1.147
|
|
5072
5095
|
*/
|
|
@@ -5403,18 +5426,76 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5403
5426
|
async [RESOLVE_TARGET](req) {
|
|
5404
5427
|
const { action } = req;
|
|
5405
5428
|
const body = parseQueryTargetBody(action, req.query);
|
|
5429
|
+
const { rows, filter, findMany, exclude, visibleOf, cap } = await this._resolveMatching({
|
|
5430
|
+
label: action,
|
|
5431
|
+
body,
|
|
5432
|
+
cap: req.cap,
|
|
5433
|
+
maxExclude: req.maxExclude,
|
|
5434
|
+
overlay: req.overlay,
|
|
5435
|
+
scopeByAction: true,
|
|
5436
|
+
select: [...new Set(req.select)],
|
|
5437
|
+
sortBy: req.select,
|
|
5438
|
+
excludeShapes: req.excludeShapes,
|
|
5439
|
+
visibleOf: req.visibleOf
|
|
5440
|
+
});
|
|
5441
|
+
const byIds = (read, ids, scope, select) => findRowsByIds({ findMany: (q) => read({
|
|
5442
|
+
...q,
|
|
5443
|
+
controls: {
|
|
5444
|
+
...q.controls,
|
|
5445
|
+
$limit: Math.max(ids.length, cap + 1)
|
|
5446
|
+
}
|
|
5447
|
+
}) }, ids, scope, select);
|
|
5448
|
+
const snapshotFields = new Set(req.select);
|
|
5449
|
+
let first = true;
|
|
5450
|
+
return {
|
|
5451
|
+
matched: rows.length,
|
|
5452
|
+
rows,
|
|
5453
|
+
dryRun: body.dryRun === true,
|
|
5454
|
+
exclude,
|
|
5455
|
+
visibleOf,
|
|
5456
|
+
load: (ids, select) => {
|
|
5457
|
+
if (!first) return byIds(findMany, ids, filter, select);
|
|
5458
|
+
first = false;
|
|
5459
|
+
const fields = [...select];
|
|
5460
|
+
if (!fields.every((f) => snapshotFields.has(f))) {
|
|
5461
|
+
const plain = (q) => this.readable.findMany(q);
|
|
5462
|
+
return byIds(plain, ids, void 0, fields);
|
|
5463
|
+
}
|
|
5464
|
+
return Promise.resolve(alignRowsToIds(rows, ids).map((row, i) => row ? projectRow$1(row, new Set([...fields, ...Object.keys(ids[i])])) : void 0));
|
|
5465
|
+
}
|
|
5466
|
+
};
|
|
5467
|
+
}
|
|
5468
|
+
/**
|
|
5469
|
+
* The shared resolver behind query targets and {@link resolveQuery}: the
|
|
5470
|
+
* query body validated, checked and run as a READ of this controller, then
|
|
5471
|
+
* ONE read of `select` ordered by `sort` — `filter (+ $search) ∧ overlay ∧
|
|
5472
|
+
* scope ∧ ¬exclude`, at most `cap + 1` rows. More than `cap` → 400
|
|
5473
|
+
* `TARGET_TOO_LARGE`; a count other than `expectCount` → 409
|
|
5474
|
+
* `TARGET_CHANGED`.
|
|
5475
|
+
*
|
|
5476
|
+
* Everything that depends on the read's visibility — the `$search`
|
|
5477
|
+
* fallback, the native-search memo, the un-appliable-term refusal and the
|
|
5478
|
+
* delegated identity — is computed INSIDE the read child, where the
|
|
5479
|
+
* permission layer's per-request state is the read's.
|
|
5480
|
+
*/
|
|
5481
|
+
async _resolveMatching(spec) {
|
|
5482
|
+
const { label, body } = spec;
|
|
5483
|
+
const cap = Math.min(spec.cap, body.maxRows ?? Infinity);
|
|
5406
5484
|
const parsed = this.parseUrlOr400(body.q.startsWith("?") ? body.q.slice(1) : body.q);
|
|
5407
5485
|
const controls = {};
|
|
5408
5486
|
for (const [k, v] of Object.entries(parsed.controls ?? {})) {
|
|
5409
5487
|
if (v === void 0) continue;
|
|
5410
|
-
if (k !== "$search" && k !== "$index") throw targetInvalid(
|
|
5411
|
-
if (k === "$search" && typeof v !== "string" && typeof v !== "number") throw targetInvalid(
|
|
5488
|
+
if (k !== "$search" && k !== "$index") throw targetInvalid(label, `A query target takes a filter, $search and $index only — "${k}" is not accepted`);
|
|
5489
|
+
if (k === "$search" && typeof v !== "string" && typeof v !== "number") throw targetInvalid(label, "$search must be a search term");
|
|
5412
5490
|
controls[k] = k === "$search" ? `${v}` : v;
|
|
5413
5491
|
}
|
|
5414
|
-
if (controls.$index !== void 0 && typeof controls.$index !== "string") throw targetInvalid(
|
|
5492
|
+
if (controls.$index !== void 0 && typeof controls.$index !== "string") throw targetInvalid(label, "$index must be an index name");
|
|
5415
5493
|
const exclude = body.exclude ?? [];
|
|
5416
|
-
const shapes =
|
|
5494
|
+
const shapes = spec.excludeShapes ?? [];
|
|
5495
|
+
const sealedSet = () => new Set([...this.fieldVisibility.sealedFor(this.readable), ...this._leavesOf(this.readable).filter((leaf) => !this.fieldVisibility.isVisible(leaf))]);
|
|
5417
5496
|
const check = () => {
|
|
5497
|
+
let sealed;
|
|
5498
|
+
const readableLeaf = (leaf) => !(sealed ??= sealedSet()).has(leaf);
|
|
5418
5499
|
const controlsError = this.validateControls(controls, "query");
|
|
5419
5500
|
if (controlsError) throw new HttpError(400, controlsError);
|
|
5420
5501
|
const gateError = this.checkCapabilities({
|
|
@@ -5422,71 +5503,160 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5422
5503
|
controls
|
|
5423
5504
|
});
|
|
5424
5505
|
if (gateError) throw gateError;
|
|
5425
|
-
if (exclude.length > 0)
|
|
5426
|
-
|
|
5427
|
-
fields
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5506
|
+
if (exclude.length > 0) {
|
|
5507
|
+
const source = shapes.length === 0 ? this.idSource : {
|
|
5508
|
+
identifications: [...this.idSource.identifications, ...shapes.map((fields) => ({
|
|
5509
|
+
fields,
|
|
5510
|
+
source: "target"
|
|
5511
|
+
}))],
|
|
5512
|
+
fieldDescriptors: this.readable.fieldDescriptors
|
|
5513
|
+
};
|
|
5514
|
+
try {
|
|
5515
|
+
validateMultiId(exclude, source, spec.maxExclude);
|
|
5516
|
+
} catch (error) {
|
|
5517
|
+
throw validatorErrorToHttp(error) ?? error;
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
const nav = FieldCapabilityIndex.navPathsOf(this.readable);
|
|
5521
|
+
for (const path of spec.selectGate ?? []) {
|
|
5522
|
+
if (this.capabilities.decorationCap(path) || selfOrAncestor(path, nav) !== void 0) throw badRequest(path, `Unknown field "${path}"`);
|
|
5523
|
+
const verdict = this.capabilities.check(path, "select", this.fieldVisibility.isVisible);
|
|
5524
|
+
if (verdict) throw badRequest(verdict.path, verdict.message);
|
|
5525
|
+
if (this._writeOnlySet.has(path)) throw badRequest(path, `Field "${path}" is @db.writeOnly`);
|
|
5526
|
+
const prefix = `${path}.`;
|
|
5527
|
+
const leaves = this._leavesOf(this.readable).filter((leaf) => leaf.startsWith(prefix));
|
|
5528
|
+
if (leaves.length > 0 && !leaves.some((leaf) => readableLeaf(leaf))) throw badRequest(path, `Unknown field "${path}"`);
|
|
5529
|
+
}
|
|
5530
|
+
for (const path of spec.sortGate ?? []) {
|
|
5531
|
+
const verdict = this.capabilities.check(path, "sort", this.fieldVisibility.isVisible);
|
|
5532
|
+
if (verdict) throw targetInvalid(label, verdict.message);
|
|
5533
|
+
}
|
|
5432
5534
|
};
|
|
5433
|
-
const ownScope =
|
|
5434
|
-
const [
|
|
5535
|
+
const ownScope = spec.scopeByAction === true && (spec.overlay === "action" || this.queryTargetScope !== _AsDbReadableController.prototype.queryTargetScope);
|
|
5536
|
+
const [read, overlay] = await Promise.all([this._asRead(controls, parsed.filter, async () => {
|
|
5435
5537
|
check();
|
|
5436
|
-
const [clientFilter, readScope] = await Promise.all([
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5538
|
+
const [clientFilter, readScope, strategy] = await Promise.all([
|
|
5539
|
+
this._relationOverlay(parsed),
|
|
5540
|
+
ownScope ? this.queryTargetScope(label) : void 0,
|
|
5541
|
+
this._resolveReadStrategy(controls)
|
|
5542
|
+
]);
|
|
5543
|
+
const base = spec.overlay === "action" ? clientFilter : await this.transformFilter(clientFilter ?? {});
|
|
5544
|
+
const searched = this.applySearchFallback(base, controls);
|
|
5545
|
+
if (controls.$search && strategy.kind !== "search" && searched === base) throw targetInvalid(label, "$search is not available here");
|
|
5546
|
+
const visibleOf = spec.visibleOf?.filter((f) => this.fieldVisibility.isVisible(f));
|
|
5547
|
+
let select = spec.select;
|
|
5548
|
+
if (spec.selectGate) {
|
|
5549
|
+
const sealed = sealedSet();
|
|
5550
|
+
for (const id of spec.identity ?? []) sealed.delete(id);
|
|
5551
|
+
select = this._sealSelect([...spec.select], sealed, this.readable);
|
|
5552
|
+
}
|
|
5553
|
+
return {
|
|
5554
|
+
searched,
|
|
5555
|
+
readScope,
|
|
5556
|
+
strategy,
|
|
5557
|
+
visibleOf,
|
|
5558
|
+
select
|
|
5559
|
+
};
|
|
5560
|
+
}, spec.routeParams), spec.overlay === "action" ? this.rowOverlay() : void 0]);
|
|
5561
|
+
const { strategy } = read;
|
|
5562
|
+
const filter = andFilters(read.searched, overlay, read.readScope, spec.scope, exclude.length > 0 ? { $not: { $or: exclude } } : void 0);
|
|
5441
5563
|
const findMany = (q) => strategy.kind === "search" ? this.readable.search(strategy.term, q, strategy.index) : this.readable.findMany(q);
|
|
5442
|
-
const cap = Math.min(req.cap, body.maxRows ?? Infinity);
|
|
5443
|
-
const sort = {};
|
|
5444
|
-
for (const f of req.select) sort[f] = 1;
|
|
5445
5564
|
const rows = await findMany({
|
|
5446
5565
|
filter,
|
|
5447
5566
|
controls: {
|
|
5448
|
-
$select: [...
|
|
5449
|
-
$sort:
|
|
5567
|
+
$select: [...read.select],
|
|
5568
|
+
$sort: Object.fromEntries(spec.sortBy.map((f) => [f, 1])),
|
|
5450
5569
|
$limit: cap + 1
|
|
5451
5570
|
}
|
|
5452
5571
|
});
|
|
5453
|
-
if (rows.length > cap) throw new ActionTargetError("TARGET_TOO_LARGE",
|
|
5454
|
-
if (body.expectCount !== void 0 && body.expectCount !== rows.length) throw new ActionTargetError("TARGET_CHANGED",
|
|
5455
|
-
const byIds = (read, ids, scope, select) => findRowsByIds({ findMany: (q) => read({
|
|
5456
|
-
...q,
|
|
5457
|
-
controls: {
|
|
5458
|
-
...q.controls,
|
|
5459
|
-
$limit: Math.max(ids.length, cap + 1)
|
|
5460
|
-
}
|
|
5461
|
-
}) }, ids, scope, select);
|
|
5462
|
-
const snapshotFields = new Set(req.select);
|
|
5463
|
-
let first = true;
|
|
5572
|
+
if (rows.length > cap) throw new ActionTargetError("TARGET_TOO_LARGE", label, `The query matches more than ${cap} rows`, { cap });
|
|
5573
|
+
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 });
|
|
5464
5574
|
return {
|
|
5465
|
-
matched: rows.length,
|
|
5466
5575
|
rows,
|
|
5467
|
-
|
|
5576
|
+
filter,
|
|
5577
|
+
findMany,
|
|
5468
5578
|
exclude,
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
const fields = [...select];
|
|
5473
|
-
if (!fields.every((f) => snapshotFields.has(f))) {
|
|
5474
|
-
const plain = (q) => this.readable.findMany(q);
|
|
5475
|
-
return byIds(plain, ids, void 0, fields);
|
|
5476
|
-
}
|
|
5477
|
-
return Promise.resolve(alignRowsToIds(rows, ids).map((row, i) => row ? projectRow$1(row, new Set([...fields, ...Object.keys(ids[i])])) : void 0));
|
|
5478
|
-
}
|
|
5579
|
+
visibleOf: read.visibleOf,
|
|
5580
|
+
cap,
|
|
5581
|
+
select: read.select
|
|
5479
5582
|
};
|
|
5480
5583
|
}
|
|
5481
5584
|
/**
|
|
5585
|
+
* Rows of THIS controller matching `q`, resolved as a READ of it for the
|
|
5586
|
+
* current event's caller (since 0.1.149) — from your own command, e.g. to
|
|
5587
|
+
* act on "every issue matching this search". `q` is a `GET /query` string
|
|
5588
|
+
* (`$search` / `$index` and a filter only) or a query-target envelope
|
|
5589
|
+
* `{ q, exclude?, expectCount?, maxRows? }` (no `dryRun`).
|
|
5590
|
+
*
|
|
5591
|
+
* The read runs under this controller's full read policy
|
|
5592
|
+
* ({@link prepareRequest} with `endpoint: "query"`, {@link hasField},
|
|
5593
|
+
* {@link validateControls}, the capability / index gate and the
|
|
5594
|
+
* {@link transformFilter} overlay) with the current event's identity.
|
|
5595
|
+
* Route interceptors and guards of the `query` route do not run; read
|
|
5596
|
+
* authorization belongs in {@link prepareRequest}. {@link queryTargetScope}
|
|
5597
|
+
* is not called. Hooks see no route params of the caller (only a call from
|
|
5598
|
+
* the routed event's own controller instance keeps its params); pass route-derived
|
|
5599
|
+
* restrictions as `opts.scope`. Joins the caller's open transaction.
|
|
5600
|
+
*
|
|
5601
|
+
* Rows are ordered by identity (`preferredId`, else the primary key) and
|
|
5602
|
+
* carry the identity fields plus `opts.select` (gated like `/query`
|
|
5603
|
+
* `$select`; `transformProjection` is not applied — hide fields with
|
|
5604
|
+
* {@link hasField}; decoration keys and navigation paths are refused). An
|
|
5605
|
+
* identity-less readable is ordered by `select` (each path sortable, else
|
|
5606
|
+
* `TARGET_INVALID`). More than `opts.cap` (default 1000) rows → 400
|
|
5607
|
+
* `TARGET_TOO_LARGE`; a count other than `expectCount` → 409
|
|
5608
|
+
* `TARGET_CHANGED`; a `$search` that can't be applied → 400
|
|
5609
|
+
* `TARGET_INVALID`. Must be awaited inside a running event handler.
|
|
5610
|
+
*/
|
|
5611
|
+
async resolveQuery(q, opts = {}) {
|
|
5612
|
+
let caller;
|
|
5613
|
+
try {
|
|
5614
|
+
caller = current();
|
|
5615
|
+
} catch {
|
|
5616
|
+
throw new Error("[moost-db] resolveQuery must be awaited inside an event handler");
|
|
5617
|
+
}
|
|
5618
|
+
const cap = opts.cap ?? 1e3;
|
|
5619
|
+
if (!Number.isInteger(cap) || cap < 1) throw new Error("[moost-db] resolveQuery: `cap` must be a positive integer");
|
|
5620
|
+
const select = opts.select ?? [];
|
|
5621
|
+
if (!Array.isArray(select) || select.some((p) => typeof p !== "string")) throw new Error("[moost-db] resolveQuery: `select` must be an array of field paths");
|
|
5622
|
+
const ids = this.readable.preferredId?.length ? this.readable.preferredId : this.readable.primaryKeys;
|
|
5623
|
+
const order = ids.length > 0 ? ids : select;
|
|
5624
|
+
if (order.length === 0) throw new Error("[moost-db] resolveQuery: this readable has no identity — pass `select` (rows are ordered by it)");
|
|
5625
|
+
const label = readCurrentActionMeta(caller)?.name ?? "";
|
|
5626
|
+
const body = parseQueryTargetBody(label, typeof q === "string" ? { q } : q);
|
|
5627
|
+
if (body.dryRun !== void 0) throw targetInvalid(label, "resolveQuery takes no `dryRun` — read `query.dryRun` yourself");
|
|
5628
|
+
let sameRoute = false;
|
|
5629
|
+
try {
|
|
5630
|
+
sameRoute = controllerOf(caller) === this && routedController(caller) === this;
|
|
5631
|
+
} catch {}
|
|
5632
|
+
const fields = [...new Set([...ids, ...select])];
|
|
5633
|
+
const { rows, select: sealedSelect } = await this._resolveMatching({
|
|
5634
|
+
label,
|
|
5635
|
+
body,
|
|
5636
|
+
cap,
|
|
5637
|
+
maxExclude: DEFAULT_MAX_ACTION_IDS,
|
|
5638
|
+
overlay: "read",
|
|
5639
|
+
scope: opts.scope,
|
|
5640
|
+
select: fields,
|
|
5641
|
+
sortBy: order,
|
|
5642
|
+
selectGate: select,
|
|
5643
|
+
identity: ids,
|
|
5644
|
+
sortGate: ids.length > 0 ? void 0 : order,
|
|
5645
|
+
routeParams: sameRoute ? void 0 : {}
|
|
5646
|
+
});
|
|
5647
|
+
return rows.map((row) => projectRow$1(row, sealedSelect));
|
|
5648
|
+
}
|
|
5649
|
+
/**
|
|
5482
5650
|
* Runs `fn` as a READ of this controller (since 0.1.147): in a child of
|
|
5483
5651
|
* the current event whose controller context is this controller's `query`
|
|
5484
5652
|
* handler, after `prepareRequest({ endpoint: "query", controls, filter })` — the
|
|
5485
5653
|
* request-scoped state a permission layer builds there (read grant, field
|
|
5486
|
-
* visibility) is the read's and stays in the child.
|
|
5654
|
+
* visibility) is the read's and stays in the child. `routeParams` (since
|
|
5655
|
+
* 0.1.149) replaces the route params the child's hooks read.
|
|
5487
5656
|
*/
|
|
5488
|
-
_asRead(controls, filter, fn) {
|
|
5657
|
+
_asRead(controls, filter, fn, routeParams) {
|
|
5489
5658
|
return runAsController(this, "query", async () => {
|
|
5659
|
+
if (routeParams) current().set(routeParamsKey, routeParams);
|
|
5490
5660
|
if (typeof this.prepareRequest === "function") await this.prepareRequest(readRequestContext("query", controls, filter));
|
|
5491
5661
|
return fn();
|
|
5492
5662
|
});
|
|
@@ -5570,7 +5740,10 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
5570
5740
|
* with the existing filter. Applies only when native search does not serve
|
|
5571
5741
|
* the request (no native search, or — since 0.1.143 — its default index
|
|
5572
5742
|
* reads a field {@link hasField} hides) and the request isn't a vector
|
|
5573
|
-
* search (`$vector` consumes the term).
|
|
5743
|
+
* search (`$vector` consumes the term). Lenient on list endpoints: a term
|
|
5744
|
+
* nothing can apply is ignored. Resolvers (query targets, `resolveQuery`)
|
|
5745
|
+
* refuse it — a subclass override that applies the term must return a new
|
|
5746
|
+
* filter object.
|
|
5574
5747
|
*/
|
|
5575
5748
|
applySearchFallback(filter, controls) {
|
|
5576
5749
|
const term = controls.$search;
|
|
@@ -6360,7 +6533,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
6360
6533
|
maxExclude: limits.maxIds,
|
|
6361
6534
|
overlay: "read",
|
|
6362
6535
|
select: [...new Set([...identity, ...delegation.paths])],
|
|
6363
|
-
excludeShapes: [delegation.paths]
|
|
6536
|
+
excludeShapes: [delegation.paths],
|
|
6537
|
+
visibleOf: identity
|
|
6364
6538
|
});
|
|
6365
6539
|
if (resolved.dryRun) return { matched: resolved.matched };
|
|
6366
6540
|
const summary = {
|
|
@@ -6370,7 +6544,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
6370
6544
|
failed: []
|
|
6371
6545
|
};
|
|
6372
6546
|
const { ids, index } = mapToSourceIds(resolved.rows, delegation.idMap);
|
|
6373
|
-
const visibleIdentity =
|
|
6547
|
+
const visibleIdentity = resolved.visibleOf ?? [];
|
|
6374
6548
|
for (let i = 0; i < index.length; i++) {
|
|
6375
6549
|
if (index[i] >= 0) continue;
|
|
6376
6550
|
const row = resolved.rows[i];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/moost-db",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.149",
|
|
4
4
|
"description": "Generic database controller for Moost with Atscript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"annotations",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@uniqu/url": "^0.1.13",
|
|
47
|
-
"@atscript/db-memory": "^0.1.
|
|
47
|
+
"@atscript/db-memory": "^0.1.149"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@atscript/core": "^0.1.100",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"better-sqlite3": "^12.6.2",
|
|
58
58
|
"moost": "^0.6.45",
|
|
59
59
|
"unplugin-atscript": "^0.1.100",
|
|
60
|
-
"@atscript/db-sqlite": "^0.1.
|
|
60
|
+
"@atscript/db-sqlite": "^0.1.149"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@atscript/typescript": "^0.1.100",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"@wooksjs/event-core": "^0.7.26",
|
|
67
67
|
"@wooksjs/http-body": "^0.7.26",
|
|
68
68
|
"moost": "^0.6.45",
|
|
69
|
-
"@atscript/db": "^0.1.
|
|
69
|
+
"@atscript/db": "^0.1.149"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|
|
72
72
|
"postinstall": "asc -f dts",
|