@atscript/moost-db 0.1.132 → 0.1.133
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 +61 -42
- package/dist/index.d.cts +46 -25
- package/dist/index.d.mts +46 -25
- package/dist/index.mjs +62 -43
- package/package.json +6 -6
package/dist/index.cjs
CHANGED
|
@@ -1155,16 +1155,12 @@ function resolveBoundReadable(ctor) {
|
|
|
1155
1155
|
}
|
|
1156
1156
|
//#endregion
|
|
1157
1157
|
//#region src/meta/field-capabilities.ts
|
|
1158
|
-
const
|
|
1159
|
-
const REASON_ADAPTER_FILTER = `${ADAPTER_FILTER}.`;
|
|
1158
|
+
const REASON_ADAPTER_FILTER = `${_atscript_db.ADAPTER_FILTER_REASON}.`;
|
|
1160
1159
|
const REASON_ADAPTER_SORT = "adapter cannot sort on this storage type.";
|
|
1161
1160
|
const REASON_WRITE_ONLY = "field is @db.writeOnly.";
|
|
1162
|
-
const REASON_ENCRYPTED =
|
|
1161
|
+
const REASON_ENCRYPTED = `${_atscript_db.ENCRYPTED_REASON}.`;
|
|
1163
1162
|
const REASON_ANNOTATION_FILTER = "add @db.column.filterable to enable.";
|
|
1164
1163
|
const REASON_ANNOTATION_SORT = "add @db.column.sortable to enable.";
|
|
1165
|
-
const REASON_NOT_TIMESTAMP = "not a timestamp field (declare it number.timestamp).";
|
|
1166
|
-
const REASON_NOT_DIMENSION = "not a dimension.";
|
|
1167
|
-
const REASON_NO_BUCKETS = "adapter has no calendar buckets.";
|
|
1168
1164
|
/** Sentence subject per op ("Filtering on field …"). */
|
|
1169
1165
|
const OP_SUBJECT = {
|
|
1170
1166
|
filter: "Filtering on",
|
|
@@ -1235,14 +1231,8 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1235
1231
|
_entries = /* @__PURE__ */ new Map();
|
|
1236
1232
|
/** Nested-object parents (never listed, always selectable) → their listed leaves. */
|
|
1237
1233
|
_objectParents = /* @__PURE__ */ new Map();
|
|
1238
|
-
/**
|
|
1239
|
-
|
|
1240
|
-
* measures), else `undefined` — the core rule: a grouping source, a
|
|
1241
|
-
* bucketed field included, must then be a dimension.
|
|
1242
|
-
*/
|
|
1243
|
-
_dimensions;
|
|
1244
|
-
/** Paths of every JSON-value descriptor (`isJsonValueField`) — see `jsonValueAncestor`. */
|
|
1245
|
-
_jsonValueParents;
|
|
1234
|
+
/** What `bucketSourceVerdict` reads of the table (JSON-value parents, dimensions, measures). */
|
|
1235
|
+
_bucketTable;
|
|
1246
1236
|
/** Listed leaves — the {@link TQueryPathSource} view for `classifyQueryPath`. */
|
|
1247
1237
|
get leaves() {
|
|
1248
1238
|
return this._entries;
|
|
@@ -1259,7 +1249,6 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1259
1249
|
this.signature = FieldCapabilityIndex.adapterSignature(source);
|
|
1260
1250
|
const units = source.calendarBucketUnits();
|
|
1261
1251
|
this.bucketUnits = _uniqu_core.BUCKET_UNITS.filter((unit) => units.has(unit));
|
|
1262
|
-
this._dimensions = source.dimensions.length > 0 || source.measures.length > 0 ? new Set(source.dimensions) : void 0;
|
|
1263
1252
|
const physicalNames = /* @__PURE__ */ new Set();
|
|
1264
1253
|
const jsonValueParents = /* @__PURE__ */ new Set();
|
|
1265
1254
|
for (const fd of source.fieldDescriptors) {
|
|
@@ -1267,7 +1256,11 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1267
1256
|
if ((0, _atscript_db.isJsonValueField)(fd)) jsonValueParents.add(fd.path);
|
|
1268
1257
|
}
|
|
1269
1258
|
this.physicalNames = physicalNames;
|
|
1270
|
-
this.
|
|
1259
|
+
this._bucketTable = {
|
|
1260
|
+
jsonValueParents,
|
|
1261
|
+
dimensions: source.dimensions,
|
|
1262
|
+
measures: source.measures
|
|
1263
|
+
};
|
|
1271
1264
|
const nav = new Set(source.navFields);
|
|
1272
1265
|
if (nav.size === 0) for (const name of source.relations.keys()) nav.add(name);
|
|
1273
1266
|
this.navFields = nav;
|
|
@@ -1324,18 +1317,14 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1324
1317
|
geo: verdict("geo", true)
|
|
1325
1318
|
};
|
|
1326
1319
|
const filterOps = filterBy.compare === REASON_ADAPTER_FILTER && !filterPolicyBlocked ? (0, _atscript_db.narrowerFilterOps)(fd, source) : [];
|
|
1327
|
-
if (filterOps.length > 0) filterBy.compare = `${
|
|
1320
|
+
if (filterOps.length > 0) filterBy.compare = `${_atscript_db.ADAPTER_FILTER_REASON}${(0, _atscript_db.acceptedOperatorsHint)(filterOps)}.`;
|
|
1328
1321
|
const physicalReason = verdict("compare", false);
|
|
1329
1322
|
let sortReason = source.canSortField(fd) ? void 0 : REASON_ADAPTER_SORT;
|
|
1330
1323
|
if (fd.encrypted) sortReason = REASON_ENCRYPTED;
|
|
1331
1324
|
if (isWriteOnly) sortReason = REASON_WRITE_ONLY;
|
|
1332
1325
|
if (!sortReason && this.sortableManual && !annotated(fd, "db.column.sortable")) sortReason = REASON_ANNOTATION_SORT;
|
|
1333
|
-
|
|
1334
|
-
const
|
|
1335
|
-
if (!bucketReason && jsonAncestor !== void 0) bucketReason = `inside JSON-stored column "${jsonAncestor}".`;
|
|
1336
|
-
if (!bucketReason && !(0, _atscript_db.isBucketableField)(fd)) bucketReason = REASON_NOT_TIMESTAMP;
|
|
1337
|
-
if (!bucketReason && this._dimensions && !this._dimensions.has(fd.path)) bucketReason = REASON_NOT_DIMENSION;
|
|
1338
|
-
if (!bucketReason && this.bucketUnits.length === 0) bucketReason = REASON_NO_BUCKETS;
|
|
1326
|
+
const bucket = isWriteOnly ? void 0 : (0, _atscript_db.bucketSourceVerdict)(fd, this._bucketTable, source);
|
|
1327
|
+
const bucketReason = !bucket ? REASON_WRITE_ONLY : bucket.ok ? void 0 : `${bucket.reason}.`;
|
|
1339
1328
|
const cap = {
|
|
1340
1329
|
filterable: filterBy.compare === void 0,
|
|
1341
1330
|
sortable: sortReason === void 0,
|
|
@@ -1369,21 +1358,32 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1369
1358
|
}
|
|
1370
1359
|
/**
|
|
1371
1360
|
* Gate check for one path in one position. Returns `undefined` when the
|
|
1372
|
-
* path is accepted.
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1361
|
+
* path is accepted.
|
|
1362
|
+
*
|
|
1363
|
+
* `exists` runs FIRST, for every path (since 0.1.133): it is the
|
|
1364
|
+
* controller's `hasField`, the visibility hook subclasses narrow per
|
|
1365
|
+
* request (e.g. a projection-scoped viewer). A path it rejects answers
|
|
1366
|
+
* `Unknown field "x"` exactly like a nonexistent one — never a capability
|
|
1367
|
+
* or navigation hint, which would reveal the field and let a filter or
|
|
1368
|
+
* sort on it act as a value oracle. Before 0.1.133 listed leaves and
|
|
1369
|
+
* navigation paths skipped it.
|
|
1377
1370
|
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
1371
|
+
* Then: navigation paths (a nav path exists on the target table but is
|
|
1372
|
+
* never a column here), a listed leaf's capability, and for other paths
|
|
1373
|
+
* the storage classification. Existence also runs BEFORE the JSON /
|
|
1374
|
+
* encrypted classification: an untyped descendant of a JSON column
|
|
1375
|
+
* (`address.nope`) is reported as `Unknown field`, not as "inside
|
|
1376
|
+
* JSON-stored column" — clients pin that wording, so do not "align" it
|
|
1377
|
+
* with the core backstop's text.
|
|
1382
1378
|
*
|
|
1383
1379
|
* `predicate` is a filter entry's class (`collectQueryPaths` records it per
|
|
1384
1380
|
* occurrence); it only matters for `op === "filter"` on a listed leaf.
|
|
1385
1381
|
*/
|
|
1386
1382
|
check(path, op, exists, predicate = "compare") {
|
|
1383
|
+
if (!exists(path)) return {
|
|
1384
|
+
path,
|
|
1385
|
+
message: `Unknown field "${path}"`
|
|
1386
|
+
};
|
|
1387
1387
|
const { kind, parent } = (0, _atscript_db.classifyQueryPath)(this, path);
|
|
1388
1388
|
if (kind === "nav") {
|
|
1389
1389
|
if (parent === void 0) return {
|
|
@@ -1423,10 +1423,6 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1423
1423
|
};
|
|
1424
1424
|
}
|
|
1425
1425
|
}
|
|
1426
|
-
if (!exists(path)) return {
|
|
1427
|
-
path,
|
|
1428
|
-
message: `Unknown field "${path}"`
|
|
1429
|
-
};
|
|
1430
1426
|
switch (kind) {
|
|
1431
1427
|
case "objectParent":
|
|
1432
1428
|
if (op === "select") return void 0;
|
|
@@ -1537,7 +1533,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1537
1533
|
metaCacheKey() {
|
|
1538
1534
|
return this.capabilities;
|
|
1539
1535
|
}
|
|
1540
|
-
/** Bound once: the
|
|
1536
|
+
/** Bound once: the visibility check ({@link hasField}) the gate hands to `capabilities.check`. */
|
|
1541
1537
|
_exists = (path) => this.hasField(path);
|
|
1542
1538
|
_preferredIdSet;
|
|
1543
1539
|
_overlayIsNoOp;
|
|
@@ -1590,6 +1586,20 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1590
1586
|
for (const [path, entry] of this.readable.flatMap) if (entry?.metadata?.has?.(annotation)) out.add(path);
|
|
1591
1587
|
return out;
|
|
1592
1588
|
}
|
|
1589
|
+
/**
|
|
1590
|
+
* THE field-visibility hook: every gated path consults it before any
|
|
1591
|
+
* capability check (since 0.1.133) — filter keys (inside `$and` / `$or` /
|
|
1592
|
+
* `$not`, existence predicates included), `$sort`, `$select`,
|
|
1593
|
+
* `$groupBy`, `$having` keys, aggregate and calendar-bucket `$field`s,
|
|
1594
|
+
* `$with` relation names and sub-query paths, and the `$search` fallback
|
|
1595
|
+
* fields. A path it rejects is answered exactly like a nonexistent one
|
|
1596
|
+
* (`Unknown field "x"` / `Unknown relation "x"`), so override it to hide
|
|
1597
|
+
* fields per request (read scopes). The default accepts every real path
|
|
1598
|
+
* (`isValidFieldPath`). `/meta` does NOT consult it — prune hidden fields
|
|
1599
|
+
* there with `applyMetaOverlay`. Native text search and vector search
|
|
1600
|
+
* (`$vector` names an index) run inside the engine over its indexes, out of
|
|
1601
|
+
* this hook's reach — keep hidden fields out of those indexes.
|
|
1602
|
+
*/
|
|
1593
1603
|
hasField(path) {
|
|
1594
1604
|
if (typeof this.readable.isValidFieldPath === "function") return this.readable.isValidFieldPath(path);
|
|
1595
1605
|
return this.readable.flatMap.has(path);
|
|
@@ -1677,7 +1687,13 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1677
1687
|
const withRelations = parsed.controls.$with;
|
|
1678
1688
|
if (withRelations?.length) {
|
|
1679
1689
|
const relations = this.readable.relations;
|
|
1680
|
-
for (const rel of withRelations)
|
|
1690
|
+
for (const rel of withRelations) {
|
|
1691
|
+
const dot = rel.name.indexOf(".");
|
|
1692
|
+
if (!(dot === -1 ? relations.has(rel.name) && this.hasField(rel.name) : this.hasField(rel.name.slice(0, dot)))) {
|
|
1693
|
+
const visible = [...relations.keys()].filter((name) => this.hasField(name));
|
|
1694
|
+
return badRequest("$with", `Unknown relation "${rel.name}"`, `Unknown relation "${rel.name}" in $with. Available relations: ${visible.join(", ") || "(none)"}`);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1681
1697
|
}
|
|
1682
1698
|
}
|
|
1683
1699
|
/**
|
|
@@ -1914,10 +1930,11 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1914
1930
|
/** First `@db.writeOnly` field referenced by `$groupBy` / aggregate `$select`, or undefined. */
|
|
1915
1931
|
_findWriteOnlyInAggregate(groupBy, select) {
|
|
1916
1932
|
if (this._writeOnlySet.size === 0) return void 0;
|
|
1917
|
-
|
|
1933
|
+
const sealed = (f) => this._writeOnlySet.has(f) && this.hasField(f);
|
|
1934
|
+
for (const f of groupBy) if (sealed(f)) return f;
|
|
1918
1935
|
if (Array.isArray(select)) for (const item of select) {
|
|
1919
1936
|
const field = typeof item === "string" ? item : item.$field;
|
|
1920
|
-
if (field &&
|
|
1937
|
+
if (field && sealed(field)) return field;
|
|
1921
1938
|
}
|
|
1922
1939
|
}
|
|
1923
1940
|
/**
|
|
@@ -1929,9 +1946,11 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1929
1946
|
applySearchFallback(filter, controls) {
|
|
1930
1947
|
const term = controls.$search;
|
|
1931
1948
|
if (!term || controls.$vector !== void 0) return filter;
|
|
1932
|
-
if (this.readable.isSearchable()
|
|
1949
|
+
if (this.readable.isSearchable()) return filter;
|
|
1950
|
+
const fields = this._searchFallbackFields.filter((f) => this.hasField(f));
|
|
1951
|
+
if (fields.length === 0) return filter;
|
|
1933
1952
|
const rx = `/${term.replace(/[.*+?^${}()|[\]\\/]/g, String.raw`\$&`)}/i`;
|
|
1934
|
-
const fragment = { $or:
|
|
1953
|
+
const fragment = { $or: fields.map((f) => ({ [f]: { $regex: rx } })) };
|
|
1935
1954
|
return filter && Object.keys(filter).length > 0 ? { $and: [filter, fragment] } : fragment;
|
|
1936
1955
|
}
|
|
1937
1956
|
/**
|
package/dist/index.d.cts
CHANGED
|
@@ -22,7 +22,9 @@ import { parseUrl } from "@uniqu/url";
|
|
|
22
22
|
*
|
|
23
23
|
* Subclass responsibilities:
|
|
24
24
|
* - Pass the bound interface + logical name + (optional) kind tag through super().
|
|
25
|
-
* - Implement {@link hasField}
|
|
25
|
+
* - Implement {@link hasField} — the field-visibility hook every validated
|
|
26
|
+
* path consults; a path it rejects gets the same `Unknown field` 400 as a
|
|
27
|
+
* nonexistent one, so overriding it hides fields per request.
|
|
26
28
|
* - Register the `/query`, `/pages`, `/one(/:id)` routes with the concrete
|
|
27
29
|
* handlers that match the data source's contract (DB readables route into
|
|
28
30
|
* aggregate/vector/search; value-help controllers just filter/sort/paginate).
|
|
@@ -45,7 +47,11 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
45
47
|
/** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
|
|
46
48
|
private _formSchemas;
|
|
47
49
|
constructor(boundType: T, controllerName: string, app: Moost, kindTag?: string);
|
|
48
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Subclass contract: return `true` if `path` addresses a field that exists
|
|
52
|
+
* AND is visible to the current request — see the DB controller's override
|
|
53
|
+
* for the full list of positions that consult it.
|
|
54
|
+
*/
|
|
49
55
|
protected abstract hasField(path: string): boolean;
|
|
50
56
|
/** Sets @db.http.path on the type metadata from the controller's computed prefix. */
|
|
51
57
|
private _resolveHttpPath;
|
|
@@ -195,9 +201,9 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
195
201
|
* `filterable` is the value-comparison verdict, `filterOps` the narrower
|
|
196
202
|
* predicates that still pass where it is `false`.
|
|
197
203
|
*
|
|
198
|
-
* A calendar-bucket source (`bucketable`)
|
|
199
|
-
*
|
|
200
|
-
*
|
|
204
|
+
* A calendar-bucket source (`bucketable`) is the core's `bucketSourceVerdict`
|
|
205
|
+
* (the same function the core path guard runs) under the HTTP-only
|
|
206
|
+
* `@db.writeOnly` veto.
|
|
201
207
|
*/
|
|
202
208
|
interface TFieldCapability {
|
|
203
209
|
/** A value-comparison filter on this path passes the gate (adapter ∧ ¬writeOnly ∧ ¬encrypted ∧ policy). */
|
|
@@ -215,8 +221,8 @@ interface TFieldCapability {
|
|
|
215
221
|
/** Present when `sortable` is `false` — the reason clause appended to the HTTP 400 message. */
|
|
216
222
|
sortReason?: string;
|
|
217
223
|
/**
|
|
218
|
-
* A calendar bucket over this path passes the gate (
|
|
219
|
-
*
|
|
224
|
+
* A calendar bucket over this path passes the gate (¬writeOnly ∧ the core's
|
|
225
|
+
* `bucketSourceVerdict`). Since 0.1.132.
|
|
220
226
|
*/
|
|
221
227
|
bucketable: boolean;
|
|
222
228
|
/** Present when `bucketable` is `false` — the reason clause appended to the HTTP 400 message. */
|
|
@@ -272,14 +278,8 @@ declare class FieldCapabilityIndex implements TQueryPathSource {
|
|
|
272
278
|
private readonly _entries;
|
|
273
279
|
/** Nested-object parents (never listed, always selectable) → their listed leaves. */
|
|
274
280
|
private readonly _objectParents;
|
|
275
|
-
/**
|
|
276
|
-
|
|
277
|
-
* measures), else `undefined` — the core rule: a grouping source, a
|
|
278
|
-
* bucketed field included, must then be a dimension.
|
|
279
|
-
*/
|
|
280
|
-
private readonly _dimensions;
|
|
281
|
-
/** Paths of every JSON-value descriptor (`isJsonValueField`) — see `jsonValueAncestor`. */
|
|
282
|
-
private readonly _jsonValueParents;
|
|
281
|
+
/** What `bucketSourceVerdict` reads of the table (JSON-value parents, dimensions, measures). */
|
|
282
|
+
private readonly _bucketTable;
|
|
283
283
|
/** Listed leaves — the {@link TQueryPathSource} view for `classifyQueryPath`. */
|
|
284
284
|
get leaves(): ReadonlyMap<string, unknown>;
|
|
285
285
|
/** Nested-object parents — the {@link TQueryPathSource} view for `classifyQueryPath`. */
|
|
@@ -292,16 +292,23 @@ declare class FieldCapabilityIndex implements TQueryPathSource {
|
|
|
292
292
|
isPhysicallyFilterable(path: string): boolean;
|
|
293
293
|
/**
|
|
294
294
|
* Gate check for one path in one position. Returns `undefined` when the
|
|
295
|
-
* path is accepted.
|
|
296
|
-
* the target table but is never a column here), then a listed leaf's
|
|
297
|
-
* capability (no existence lookup needed — every listed leaf is a real
|
|
298
|
-
* field), then `exists` (the readable's `isValidFieldPath`) and, for paths
|
|
299
|
-
* that exist but are not leaves, the storage classification.
|
|
295
|
+
* path is accepted.
|
|
300
296
|
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
297
|
+
* `exists` runs FIRST, for every path (since 0.1.133): it is the
|
|
298
|
+
* controller's `hasField`, the visibility hook subclasses narrow per
|
|
299
|
+
* request (e.g. a projection-scoped viewer). A path it rejects answers
|
|
300
|
+
* `Unknown field "x"` exactly like a nonexistent one — never a capability
|
|
301
|
+
* or navigation hint, which would reveal the field and let a filter or
|
|
302
|
+
* sort on it act as a value oracle. Before 0.1.133 listed leaves and
|
|
303
|
+
* navigation paths skipped it.
|
|
304
|
+
*
|
|
305
|
+
* Then: navigation paths (a nav path exists on the target table but is
|
|
306
|
+
* never a column here), a listed leaf's capability, and for other paths
|
|
307
|
+
* the storage classification. Existence also runs BEFORE the JSON /
|
|
308
|
+
* encrypted classification: an untyped descendant of a JSON column
|
|
309
|
+
* (`address.nope`) is reported as `Unknown field`, not as "inside
|
|
310
|
+
* JSON-stored column" — clients pin that wording, so do not "align" it
|
|
311
|
+
* with the core backstop's text.
|
|
305
312
|
*
|
|
306
313
|
* `predicate` is a filter entry's class (`collectQueryPaths` records it per
|
|
307
314
|
* occurrence); it only matters for `op === "filter"` on a listed leaf.
|
|
@@ -343,7 +350,7 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
343
350
|
private _capabilities?;
|
|
344
351
|
/** `/meta` is a projection of {@link capabilities}: a rebuilt index rebuilds the cached envelope. */
|
|
345
352
|
protected metaCacheKey(): unknown;
|
|
346
|
-
/** Bound once: the
|
|
353
|
+
/** Bound once: the visibility check ({@link hasField}) the gate hands to `capabilities.check`. */
|
|
347
354
|
private readonly _exists;
|
|
348
355
|
private readonly _preferredIdSet;
|
|
349
356
|
private readonly _overlayIsNoOp;
|
|
@@ -363,6 +370,20 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
363
370
|
private _collectInvertibleFields;
|
|
364
371
|
private _collectQuantityRefs;
|
|
365
372
|
private _collectAnnotated;
|
|
373
|
+
/**
|
|
374
|
+
* THE field-visibility hook: every gated path consults it before any
|
|
375
|
+
* capability check (since 0.1.133) — filter keys (inside `$and` / `$or` /
|
|
376
|
+
* `$not`, existence predicates included), `$sort`, `$select`,
|
|
377
|
+
* `$groupBy`, `$having` keys, aggregate and calendar-bucket `$field`s,
|
|
378
|
+
* `$with` relation names and sub-query paths, and the `$search` fallback
|
|
379
|
+
* fields. A path it rejects is answered exactly like a nonexistent one
|
|
380
|
+
* (`Unknown field "x"` / `Unknown relation "x"`), so override it to hide
|
|
381
|
+
* fields per request (read scopes). The default accepts every real path
|
|
382
|
+
* (`isValidFieldPath`). `/meta` does NOT consult it — prune hidden fields
|
|
383
|
+
* there with `applyMetaOverlay`. Native text search and vector search
|
|
384
|
+
* (`$vector` names an index) run inside the engine over its indexes, out of
|
|
385
|
+
* this hook's reach — keep hidden fields out of those indexes.
|
|
386
|
+
*/
|
|
366
387
|
protected hasField(path: string): boolean;
|
|
367
388
|
/**
|
|
368
389
|
* Structural capability gate (since 0.1.128): walks the PARSED query —
|
package/dist/index.d.mts
CHANGED
|
@@ -22,7 +22,9 @@ import { AtscriptDbReadable, AtscriptDbTable, BucketUnit, FilterExpr, FlatOf, TC
|
|
|
22
22
|
*
|
|
23
23
|
* Subclass responsibilities:
|
|
24
24
|
* - Pass the bound interface + logical name + (optional) kind tag through super().
|
|
25
|
-
* - Implement {@link hasField}
|
|
25
|
+
* - Implement {@link hasField} — the field-visibility hook every validated
|
|
26
|
+
* path consults; a path it rejects gets the same `Unknown field` 400 as a
|
|
27
|
+
* nonexistent one, so overriding it hides fields per request.
|
|
26
28
|
* - Register the `/query`, `/pages`, `/one(/:id)` routes with the concrete
|
|
27
29
|
* handlers that match the data source's contract (DB readables route into
|
|
28
30
|
* aggregate/vector/search; value-help controllers just filter/sort/paginate).
|
|
@@ -45,7 +47,11 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
45
47
|
/** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
|
|
46
48
|
private _formSchemas;
|
|
47
49
|
constructor(boundType: T, controllerName: string, app: Moost, kindTag?: string);
|
|
48
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Subclass contract: return `true` if `path` addresses a field that exists
|
|
52
|
+
* AND is visible to the current request — see the DB controller's override
|
|
53
|
+
* for the full list of positions that consult it.
|
|
54
|
+
*/
|
|
49
55
|
protected abstract hasField(path: string): boolean;
|
|
50
56
|
/** Sets @db.http.path on the type metadata from the controller's computed prefix. */
|
|
51
57
|
private _resolveHttpPath;
|
|
@@ -195,9 +201,9 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
195
201
|
* `filterable` is the value-comparison verdict, `filterOps` the narrower
|
|
196
202
|
* predicates that still pass where it is `false`.
|
|
197
203
|
*
|
|
198
|
-
* A calendar-bucket source (`bucketable`)
|
|
199
|
-
*
|
|
200
|
-
*
|
|
204
|
+
* A calendar-bucket source (`bucketable`) is the core's `bucketSourceVerdict`
|
|
205
|
+
* (the same function the core path guard runs) under the HTTP-only
|
|
206
|
+
* `@db.writeOnly` veto.
|
|
201
207
|
*/
|
|
202
208
|
interface TFieldCapability {
|
|
203
209
|
/** A value-comparison filter on this path passes the gate (adapter ∧ ¬writeOnly ∧ ¬encrypted ∧ policy). */
|
|
@@ -215,8 +221,8 @@ interface TFieldCapability {
|
|
|
215
221
|
/** Present when `sortable` is `false` — the reason clause appended to the HTTP 400 message. */
|
|
216
222
|
sortReason?: string;
|
|
217
223
|
/**
|
|
218
|
-
* A calendar bucket over this path passes the gate (
|
|
219
|
-
*
|
|
224
|
+
* A calendar bucket over this path passes the gate (¬writeOnly ∧ the core's
|
|
225
|
+
* `bucketSourceVerdict`). Since 0.1.132.
|
|
220
226
|
*/
|
|
221
227
|
bucketable: boolean;
|
|
222
228
|
/** Present when `bucketable` is `false` — the reason clause appended to the HTTP 400 message. */
|
|
@@ -272,14 +278,8 @@ declare class FieldCapabilityIndex implements TQueryPathSource {
|
|
|
272
278
|
private readonly _entries;
|
|
273
279
|
/** Nested-object parents (never listed, always selectable) → their listed leaves. */
|
|
274
280
|
private readonly _objectParents;
|
|
275
|
-
/**
|
|
276
|
-
|
|
277
|
-
* measures), else `undefined` — the core rule: a grouping source, a
|
|
278
|
-
* bucketed field included, must then be a dimension.
|
|
279
|
-
*/
|
|
280
|
-
private readonly _dimensions;
|
|
281
|
-
/** Paths of every JSON-value descriptor (`isJsonValueField`) — see `jsonValueAncestor`. */
|
|
282
|
-
private readonly _jsonValueParents;
|
|
281
|
+
/** What `bucketSourceVerdict` reads of the table (JSON-value parents, dimensions, measures). */
|
|
282
|
+
private readonly _bucketTable;
|
|
283
283
|
/** Listed leaves — the {@link TQueryPathSource} view for `classifyQueryPath`. */
|
|
284
284
|
get leaves(): ReadonlyMap<string, unknown>;
|
|
285
285
|
/** Nested-object parents — the {@link TQueryPathSource} view for `classifyQueryPath`. */
|
|
@@ -292,16 +292,23 @@ declare class FieldCapabilityIndex implements TQueryPathSource {
|
|
|
292
292
|
isPhysicallyFilterable(path: string): boolean;
|
|
293
293
|
/**
|
|
294
294
|
* Gate check for one path in one position. Returns `undefined` when the
|
|
295
|
-
* path is accepted.
|
|
296
|
-
* the target table but is never a column here), then a listed leaf's
|
|
297
|
-
* capability (no existence lookup needed — every listed leaf is a real
|
|
298
|
-
* field), then `exists` (the readable's `isValidFieldPath`) and, for paths
|
|
299
|
-
* that exist but are not leaves, the storage classification.
|
|
295
|
+
* path is accepted.
|
|
300
296
|
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
297
|
+
* `exists` runs FIRST, for every path (since 0.1.133): it is the
|
|
298
|
+
* controller's `hasField`, the visibility hook subclasses narrow per
|
|
299
|
+
* request (e.g. a projection-scoped viewer). A path it rejects answers
|
|
300
|
+
* `Unknown field "x"` exactly like a nonexistent one — never a capability
|
|
301
|
+
* or navigation hint, which would reveal the field and let a filter or
|
|
302
|
+
* sort on it act as a value oracle. Before 0.1.133 listed leaves and
|
|
303
|
+
* navigation paths skipped it.
|
|
304
|
+
*
|
|
305
|
+
* Then: navigation paths (a nav path exists on the target table but is
|
|
306
|
+
* never a column here), a listed leaf's capability, and for other paths
|
|
307
|
+
* the storage classification. Existence also runs BEFORE the JSON /
|
|
308
|
+
* encrypted classification: an untyped descendant of a JSON column
|
|
309
|
+
* (`address.nope`) is reported as `Unknown field`, not as "inside
|
|
310
|
+
* JSON-stored column" — clients pin that wording, so do not "align" it
|
|
311
|
+
* with the core backstop's text.
|
|
305
312
|
*
|
|
306
313
|
* `predicate` is a filter entry's class (`collectQueryPaths` records it per
|
|
307
314
|
* occurrence); it only matters for `op === "filter"` on a listed leaf.
|
|
@@ -343,7 +350,7 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
343
350
|
private _capabilities?;
|
|
344
351
|
/** `/meta` is a projection of {@link capabilities}: a rebuilt index rebuilds the cached envelope. */
|
|
345
352
|
protected metaCacheKey(): unknown;
|
|
346
|
-
/** Bound once: the
|
|
353
|
+
/** Bound once: the visibility check ({@link hasField}) the gate hands to `capabilities.check`. */
|
|
347
354
|
private readonly _exists;
|
|
348
355
|
private readonly _preferredIdSet;
|
|
349
356
|
private readonly _overlayIsNoOp;
|
|
@@ -363,6 +370,20 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
363
370
|
private _collectInvertibleFields;
|
|
364
371
|
private _collectQuantityRefs;
|
|
365
372
|
private _collectAnnotated;
|
|
373
|
+
/**
|
|
374
|
+
* THE field-visibility hook: every gated path consults it before any
|
|
375
|
+
* capability check (since 0.1.133) — filter keys (inside `$and` / `$or` /
|
|
376
|
+
* `$not`, existence predicates included), `$sort`, `$select`,
|
|
377
|
+
* `$groupBy`, `$having` keys, aggregate and calendar-bucket `$field`s,
|
|
378
|
+
* `$with` relation names and sub-query paths, and the `$search` fallback
|
|
379
|
+
* fields. A path it rejects is answered exactly like a nonexistent one
|
|
380
|
+
* (`Unknown field "x"` / `Unknown relation "x"`), so override it to hide
|
|
381
|
+
* fields per request (read scopes). The default accepts every real path
|
|
382
|
+
* (`isValidFieldPath`). `/meta` does NOT consult it — prune hidden fields
|
|
383
|
+
* there with `applyMetaOverlay`. Native text search and vector search
|
|
384
|
+
* (`$vector` names an index) run inside the engine over its indexes, out of
|
|
385
|
+
* this hook's reach — keep hidden fields out of those indexes.
|
|
386
|
+
*/
|
|
366
387
|
protected hasField(path: string): boolean;
|
|
367
388
|
/**
|
|
368
389
|
* Structural capability gate (since 0.1.128): walks the PARSED query —
|
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { ValidatorError, defineAnnotatedType, isAnnotatedType, serializeAnnotate
|
|
|
3
3
|
import { Body, Delete, Get, HttpError, Patch, Post, Put, Query, Url } from "@moostjs/event-http";
|
|
4
4
|
import { ApplyDecorators, Controller, Inherit, Inject, Intercept, Moost, Optional, Param, Provide, Resolve, TInterceptorPriority, defineBeforeInterceptor, defineInterceptor, getMoostMate, useControllerContext } from "moost";
|
|
5
5
|
import { parseUrl } from "@uniqu/url";
|
|
6
|
-
import { DbError, acceptedOperatorsHint, canFilterLeaf, checkHavingKeys, classifyQueryPath, collectQueryPaths, collectQueryPaths as collectQueryPaths$1, findAncestorInSet,
|
|
6
|
+
import { ADAPTER_FILTER_REASON, DbError, ENCRYPTED_REASON, acceptedOperatorsHint, bucketSourceVerdict, canFilterLeaf, checkHavingKeys, classifyQueryPath, collectQueryPaths, collectQueryPaths as collectQueryPaths$1, findAncestorInSet, isJsonValueField, isPlainObject, narrowerFilterOps, reconcileCas, resolveCalendarBuckets, unsupportedOperatorMessage } from "@atscript/db";
|
|
7
7
|
import { BUCKET_UNITS } from "@uniqu/core";
|
|
8
8
|
import { buildMemoryPredicate, projectRow, sortRows } from "@atscript/db-memory";
|
|
9
9
|
import { cached, current, defineWook, key } from "@wooksjs/event-core";
|
|
@@ -1154,16 +1154,12 @@ function resolveBoundReadable(ctor) {
|
|
|
1154
1154
|
}
|
|
1155
1155
|
//#endregion
|
|
1156
1156
|
//#region src/meta/field-capabilities.ts
|
|
1157
|
-
const
|
|
1158
|
-
const REASON_ADAPTER_FILTER = `${ADAPTER_FILTER}.`;
|
|
1157
|
+
const REASON_ADAPTER_FILTER = `${ADAPTER_FILTER_REASON}.`;
|
|
1159
1158
|
const REASON_ADAPTER_SORT = "adapter cannot sort on this storage type.";
|
|
1160
1159
|
const REASON_WRITE_ONLY = "field is @db.writeOnly.";
|
|
1161
|
-
const REASON_ENCRYPTED =
|
|
1160
|
+
const REASON_ENCRYPTED = `${ENCRYPTED_REASON}.`;
|
|
1162
1161
|
const REASON_ANNOTATION_FILTER = "add @db.column.filterable to enable.";
|
|
1163
1162
|
const REASON_ANNOTATION_SORT = "add @db.column.sortable to enable.";
|
|
1164
|
-
const REASON_NOT_TIMESTAMP = "not a timestamp field (declare it number.timestamp).";
|
|
1165
|
-
const REASON_NOT_DIMENSION = "not a dimension.";
|
|
1166
|
-
const REASON_NO_BUCKETS = "adapter has no calendar buckets.";
|
|
1167
1163
|
/** Sentence subject per op ("Filtering on field …"). */
|
|
1168
1164
|
const OP_SUBJECT = {
|
|
1169
1165
|
filter: "Filtering on",
|
|
@@ -1234,14 +1230,8 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1234
1230
|
_entries = /* @__PURE__ */ new Map();
|
|
1235
1231
|
/** Nested-object parents (never listed, always selectable) → their listed leaves. */
|
|
1236
1232
|
_objectParents = /* @__PURE__ */ new Map();
|
|
1237
|
-
/**
|
|
1238
|
-
|
|
1239
|
-
* measures), else `undefined` — the core rule: a grouping source, a
|
|
1240
|
-
* bucketed field included, must then be a dimension.
|
|
1241
|
-
*/
|
|
1242
|
-
_dimensions;
|
|
1243
|
-
/** Paths of every JSON-value descriptor (`isJsonValueField`) — see `jsonValueAncestor`. */
|
|
1244
|
-
_jsonValueParents;
|
|
1233
|
+
/** What `bucketSourceVerdict` reads of the table (JSON-value parents, dimensions, measures). */
|
|
1234
|
+
_bucketTable;
|
|
1245
1235
|
/** Listed leaves — the {@link TQueryPathSource} view for `classifyQueryPath`. */
|
|
1246
1236
|
get leaves() {
|
|
1247
1237
|
return this._entries;
|
|
@@ -1258,7 +1248,6 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1258
1248
|
this.signature = FieldCapabilityIndex.adapterSignature(source);
|
|
1259
1249
|
const units = source.calendarBucketUnits();
|
|
1260
1250
|
this.bucketUnits = BUCKET_UNITS.filter((unit) => units.has(unit));
|
|
1261
|
-
this._dimensions = source.dimensions.length > 0 || source.measures.length > 0 ? new Set(source.dimensions) : void 0;
|
|
1262
1251
|
const physicalNames = /* @__PURE__ */ new Set();
|
|
1263
1252
|
const jsonValueParents = /* @__PURE__ */ new Set();
|
|
1264
1253
|
for (const fd of source.fieldDescriptors) {
|
|
@@ -1266,7 +1255,11 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1266
1255
|
if (isJsonValueField(fd)) jsonValueParents.add(fd.path);
|
|
1267
1256
|
}
|
|
1268
1257
|
this.physicalNames = physicalNames;
|
|
1269
|
-
this.
|
|
1258
|
+
this._bucketTable = {
|
|
1259
|
+
jsonValueParents,
|
|
1260
|
+
dimensions: source.dimensions,
|
|
1261
|
+
measures: source.measures
|
|
1262
|
+
};
|
|
1270
1263
|
const nav = new Set(source.navFields);
|
|
1271
1264
|
if (nav.size === 0) for (const name of source.relations.keys()) nav.add(name);
|
|
1272
1265
|
this.navFields = nav;
|
|
@@ -1323,18 +1316,14 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1323
1316
|
geo: verdict("geo", true)
|
|
1324
1317
|
};
|
|
1325
1318
|
const filterOps = filterBy.compare === REASON_ADAPTER_FILTER && !filterPolicyBlocked ? narrowerFilterOps(fd, source) : [];
|
|
1326
|
-
if (filterOps.length > 0) filterBy.compare = `${
|
|
1319
|
+
if (filterOps.length > 0) filterBy.compare = `${ADAPTER_FILTER_REASON}${acceptedOperatorsHint(filterOps)}.`;
|
|
1327
1320
|
const physicalReason = verdict("compare", false);
|
|
1328
1321
|
let sortReason = source.canSortField(fd) ? void 0 : REASON_ADAPTER_SORT;
|
|
1329
1322
|
if (fd.encrypted) sortReason = REASON_ENCRYPTED;
|
|
1330
1323
|
if (isWriteOnly) sortReason = REASON_WRITE_ONLY;
|
|
1331
1324
|
if (!sortReason && this.sortableManual && !annotated(fd, "db.column.sortable")) sortReason = REASON_ANNOTATION_SORT;
|
|
1332
|
-
|
|
1333
|
-
const
|
|
1334
|
-
if (!bucketReason && jsonAncestor !== void 0) bucketReason = `inside JSON-stored column "${jsonAncestor}".`;
|
|
1335
|
-
if (!bucketReason && !isBucketableField(fd)) bucketReason = REASON_NOT_TIMESTAMP;
|
|
1336
|
-
if (!bucketReason && this._dimensions && !this._dimensions.has(fd.path)) bucketReason = REASON_NOT_DIMENSION;
|
|
1337
|
-
if (!bucketReason && this.bucketUnits.length === 0) bucketReason = REASON_NO_BUCKETS;
|
|
1325
|
+
const bucket = isWriteOnly ? void 0 : bucketSourceVerdict(fd, this._bucketTable, source);
|
|
1326
|
+
const bucketReason = !bucket ? REASON_WRITE_ONLY : bucket.ok ? void 0 : `${bucket.reason}.`;
|
|
1338
1327
|
const cap = {
|
|
1339
1328
|
filterable: filterBy.compare === void 0,
|
|
1340
1329
|
sortable: sortReason === void 0,
|
|
@@ -1368,21 +1357,32 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1368
1357
|
}
|
|
1369
1358
|
/**
|
|
1370
1359
|
* Gate check for one path in one position. Returns `undefined` when the
|
|
1371
|
-
* path is accepted.
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1360
|
+
* path is accepted.
|
|
1361
|
+
*
|
|
1362
|
+
* `exists` runs FIRST, for every path (since 0.1.133): it is the
|
|
1363
|
+
* controller's `hasField`, the visibility hook subclasses narrow per
|
|
1364
|
+
* request (e.g. a projection-scoped viewer). A path it rejects answers
|
|
1365
|
+
* `Unknown field "x"` exactly like a nonexistent one — never a capability
|
|
1366
|
+
* or navigation hint, which would reveal the field and let a filter or
|
|
1367
|
+
* sort on it act as a value oracle. Before 0.1.133 listed leaves and
|
|
1368
|
+
* navigation paths skipped it.
|
|
1376
1369
|
*
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
1370
|
+
* Then: navigation paths (a nav path exists on the target table but is
|
|
1371
|
+
* never a column here), a listed leaf's capability, and for other paths
|
|
1372
|
+
* the storage classification. Existence also runs BEFORE the JSON /
|
|
1373
|
+
* encrypted classification: an untyped descendant of a JSON column
|
|
1374
|
+
* (`address.nope`) is reported as `Unknown field`, not as "inside
|
|
1375
|
+
* JSON-stored column" — clients pin that wording, so do not "align" it
|
|
1376
|
+
* with the core backstop's text.
|
|
1381
1377
|
*
|
|
1382
1378
|
* `predicate` is a filter entry's class (`collectQueryPaths` records it per
|
|
1383
1379
|
* occurrence); it only matters for `op === "filter"` on a listed leaf.
|
|
1384
1380
|
*/
|
|
1385
1381
|
check(path, op, exists, predicate = "compare") {
|
|
1382
|
+
if (!exists(path)) return {
|
|
1383
|
+
path,
|
|
1384
|
+
message: `Unknown field "${path}"`
|
|
1385
|
+
};
|
|
1386
1386
|
const { kind, parent } = classifyQueryPath(this, path);
|
|
1387
1387
|
if (kind === "nav") {
|
|
1388
1388
|
if (parent === void 0) return {
|
|
@@ -1422,10 +1422,6 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
|
|
|
1422
1422
|
};
|
|
1423
1423
|
}
|
|
1424
1424
|
}
|
|
1425
|
-
if (!exists(path)) return {
|
|
1426
|
-
path,
|
|
1427
|
-
message: `Unknown field "${path}"`
|
|
1428
|
-
};
|
|
1429
1425
|
switch (kind) {
|
|
1430
1426
|
case "objectParent":
|
|
1431
1427
|
if (op === "select") return void 0;
|
|
@@ -1536,7 +1532,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1536
1532
|
metaCacheKey() {
|
|
1537
1533
|
return this.capabilities;
|
|
1538
1534
|
}
|
|
1539
|
-
/** Bound once: the
|
|
1535
|
+
/** Bound once: the visibility check ({@link hasField}) the gate hands to `capabilities.check`. */
|
|
1540
1536
|
_exists = (path) => this.hasField(path);
|
|
1541
1537
|
_preferredIdSet;
|
|
1542
1538
|
_overlayIsNoOp;
|
|
@@ -1589,6 +1585,20 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1589
1585
|
for (const [path, entry] of this.readable.flatMap) if (entry?.metadata?.has?.(annotation)) out.add(path);
|
|
1590
1586
|
return out;
|
|
1591
1587
|
}
|
|
1588
|
+
/**
|
|
1589
|
+
* THE field-visibility hook: every gated path consults it before any
|
|
1590
|
+
* capability check (since 0.1.133) — filter keys (inside `$and` / `$or` /
|
|
1591
|
+
* `$not`, existence predicates included), `$sort`, `$select`,
|
|
1592
|
+
* `$groupBy`, `$having` keys, aggregate and calendar-bucket `$field`s,
|
|
1593
|
+
* `$with` relation names and sub-query paths, and the `$search` fallback
|
|
1594
|
+
* fields. A path it rejects is answered exactly like a nonexistent one
|
|
1595
|
+
* (`Unknown field "x"` / `Unknown relation "x"`), so override it to hide
|
|
1596
|
+
* fields per request (read scopes). The default accepts every real path
|
|
1597
|
+
* (`isValidFieldPath`). `/meta` does NOT consult it — prune hidden fields
|
|
1598
|
+
* there with `applyMetaOverlay`. Native text search and vector search
|
|
1599
|
+
* (`$vector` names an index) run inside the engine over its indexes, out of
|
|
1600
|
+
* this hook's reach — keep hidden fields out of those indexes.
|
|
1601
|
+
*/
|
|
1592
1602
|
hasField(path) {
|
|
1593
1603
|
if (typeof this.readable.isValidFieldPath === "function") return this.readable.isValidFieldPath(path);
|
|
1594
1604
|
return this.readable.flatMap.has(path);
|
|
@@ -1676,7 +1686,13 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1676
1686
|
const withRelations = parsed.controls.$with;
|
|
1677
1687
|
if (withRelations?.length) {
|
|
1678
1688
|
const relations = this.readable.relations;
|
|
1679
|
-
for (const rel of withRelations)
|
|
1689
|
+
for (const rel of withRelations) {
|
|
1690
|
+
const dot = rel.name.indexOf(".");
|
|
1691
|
+
if (!(dot === -1 ? relations.has(rel.name) && this.hasField(rel.name) : this.hasField(rel.name.slice(0, dot)))) {
|
|
1692
|
+
const visible = [...relations.keys()].filter((name) => this.hasField(name));
|
|
1693
|
+
return badRequest("$with", `Unknown relation "${rel.name}"`, `Unknown relation "${rel.name}" in $with. Available relations: ${visible.join(", ") || "(none)"}`);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1680
1696
|
}
|
|
1681
1697
|
}
|
|
1682
1698
|
/**
|
|
@@ -1913,10 +1929,11 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1913
1929
|
/** First `@db.writeOnly` field referenced by `$groupBy` / aggregate `$select`, or undefined. */
|
|
1914
1930
|
_findWriteOnlyInAggregate(groupBy, select) {
|
|
1915
1931
|
if (this._writeOnlySet.size === 0) return void 0;
|
|
1916
|
-
|
|
1932
|
+
const sealed = (f) => this._writeOnlySet.has(f) && this.hasField(f);
|
|
1933
|
+
for (const f of groupBy) if (sealed(f)) return f;
|
|
1917
1934
|
if (Array.isArray(select)) for (const item of select) {
|
|
1918
1935
|
const field = typeof item === "string" ? item : item.$field;
|
|
1919
|
-
if (field &&
|
|
1936
|
+
if (field && sealed(field)) return field;
|
|
1920
1937
|
}
|
|
1921
1938
|
}
|
|
1922
1939
|
/**
|
|
@@ -1928,9 +1945,11 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1928
1945
|
applySearchFallback(filter, controls) {
|
|
1929
1946
|
const term = controls.$search;
|
|
1930
1947
|
if (!term || controls.$vector !== void 0) return filter;
|
|
1931
|
-
if (this.readable.isSearchable()
|
|
1948
|
+
if (this.readable.isSearchable()) return filter;
|
|
1949
|
+
const fields = this._searchFallbackFields.filter((f) => this.hasField(f));
|
|
1950
|
+
if (fields.length === 0) return filter;
|
|
1932
1951
|
const rx = `/${term.replace(/[.*+?^${}()|[\]\\/]/g, String.raw`\$&`)}/i`;
|
|
1933
|
-
const fragment = { $or:
|
|
1952
|
+
const fragment = { $or: fields.map((f) => ({ [f]: { $regex: rx } })) };
|
|
1934
1953
|
return filter && Object.keys(filter).length > 0 ? { $and: [filter, fragment] } : fragment;
|
|
1935
1954
|
}
|
|
1936
1955
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/moost-db",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.133",
|
|
4
4
|
"description": "Generic database controller for Moost with Atscript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"annotations",
|
|
@@ -43,14 +43,14 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@uniqu/url": "^0.1.
|
|
47
|
-
"@atscript/db-memory": "^0.1.
|
|
46
|
+
"@uniqu/url": "^0.1.10",
|
|
47
|
+
"@atscript/db-memory": "^0.1.133"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@atscript/core": "^0.1.92",
|
|
51
51
|
"@atscript/typescript": "^0.1.92",
|
|
52
52
|
"@moostjs/event-http": "^0.6.37",
|
|
53
|
-
"@uniqu/core": "^0.1.
|
|
53
|
+
"@uniqu/core": "^0.1.10",
|
|
54
54
|
"@wooksjs/event-core": "^0.7.23",
|
|
55
55
|
"@wooksjs/event-http": "^0.7.23",
|
|
56
56
|
"@wooksjs/http-body": "^0.7.23",
|
|
@@ -60,11 +60,11 @@
|
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@atscript/typescript": "^0.1.92",
|
|
62
62
|
"@moostjs/event-http": "^0.6.37",
|
|
63
|
-
"@uniqu/core": "^0.1.
|
|
63
|
+
"@uniqu/core": "^0.1.10",
|
|
64
64
|
"@wooksjs/event-core": "^0.7.23",
|
|
65
65
|
"@wooksjs/http-body": "^0.7.23",
|
|
66
66
|
"moost": "^0.6.37",
|
|
67
|
-
"@atscript/db": "^0.1.
|
|
67
|
+
"@atscript/db": "^0.1.133"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
70
70
|
"postinstall": "asc -f dts",
|