@atscript/moost-db 0.1.131 → 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 +230 -88
- package/dist/index.d.cts +126 -25
- package/dist/index.d.mts +126 -25
- package/dist/index.mjs +231 -89
- package/package.json +6 -6
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as resolveDbSpace, n as clearDbSpaces, r as provideDbSpace, t as DEFAULT_DB_SPACE } from "./db-space-registry-CWpYwZ4R.cjs";
|
|
2
2
|
import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, TSerializedAnnotatedType, TValidatorOptions, Validator } from "@atscript/typescript/utils";
|
|
3
|
-
import { AtscriptDbReadable, AtscriptDbTable, FilterExpr, FlatOf, TCrudOp, TCrudPermissions, TCrudPermissions as TCrudPermissions$1, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionIntent as TDbActionIntent$1, TDbActionLevel, TDbActionLevel as TDbActionLevel$1, TDbActionProcessor, TDbFieldMeta, TDbRemoveGuardContext, TDbRemoveGuardContext as TDbRemoveGuardContext$1, TDbWriteAction, TDbWriteAction as TDbWriteAction$1, TDbWriteGuardContext, TDbWriteGuardContext as TDbWriteGuardContext$1, TIdentification, TMetaResponse, TQueryPathOp, TQueryPathOp as TQueryPathOp$1, TQueryPathRefs, TQueryPathSource, Uniquery, UniqueryControls, collectQueryPaths } from "@atscript/db";
|
|
3
|
+
import { AtscriptDbReadable, AtscriptDbTable, BucketUnit, FilterExpr, FlatOf, TCrudOp, TCrudPermissions, TCrudPermissions as TCrudPermissions$1, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionIntent as TDbActionIntent$1, TDbActionLevel, TDbActionLevel as TDbActionLevel$1, TDbActionProcessor, TDbFieldMeta, TDbRemoveGuardContext, TDbRemoveGuardContext as TDbRemoveGuardContext$1, TDbWriteAction, TDbWriteAction as TDbWriteAction$1, TDbWriteGuardContext, TDbWriteGuardContext as TDbWriteGuardContext$1, TFilterPredicate, TIdentification, TMetaResponse, TQueryPathOp, TQueryPathOp as TQueryPathOp$1, TQueryPathRefs, TQueryPathSource, Uniquery, UniqueryControls, collectQueryPaths } from "@atscript/db";
|
|
4
4
|
import { HttpError } from "@moostjs/event-http";
|
|
5
5
|
import { Mate, Moost, TConsoleBase, TMateParamMeta, TMoostMetadata } from "moost";
|
|
6
6
|
import { parseUrl } from "@uniqu/url";
|
|
@@ -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).
|
|
@@ -40,10 +42,16 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
40
42
|
private _serializedType?;
|
|
41
43
|
/** Cached full meta response (computed lazily on first meta() call). */
|
|
42
44
|
private _metaResponse?;
|
|
45
|
+
/** {@link metaCacheKey} the cached response was built for. */
|
|
46
|
+
private _metaResponseKey?;
|
|
43
47
|
/** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
|
|
44
48
|
private _formSchemas;
|
|
45
49
|
constructor(boundType: T, controllerName: string, app: Moost, kindTag?: string);
|
|
46
|
-
/**
|
|
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
|
+
*/
|
|
47
55
|
protected abstract hasField(path: string): boolean;
|
|
48
56
|
/** Sets @db.http.path on the type metadata from the controller's computed prefix. */
|
|
49
57
|
private _resolveHttpPath;
|
|
@@ -126,10 +134,18 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
126
134
|
protected returnOne(result: Promise<DataType | null>): Promise<DataType | HttpError>;
|
|
127
135
|
/**
|
|
128
136
|
* **GET /meta** — returns the bound interface's metadata envelope. The
|
|
129
|
-
* static envelope is cached
|
|
130
|
-
* subclasses can prune the
|
|
137
|
+
* static envelope is cached (rebuilt when {@link metaCacheKey} changes);
|
|
138
|
+
* {@link applyMetaOverlay} runs per request so subclasses can prune the
|
|
139
|
+
* response by principal.
|
|
131
140
|
*/
|
|
132
141
|
meta(): Promise<TMetaResponse>;
|
|
142
|
+
/**
|
|
143
|
+
* Identity of the inputs the cached `/meta` envelope is built from — a new
|
|
144
|
+
* value rebuilds it. Default: constant (built once). The DB readable
|
|
145
|
+
* controller returns its capability index, which is rebuilt when the
|
|
146
|
+
* adapter's capabilities change (since 0.1.132).
|
|
147
|
+
*/
|
|
148
|
+
protected metaCacheKey(): unknown;
|
|
133
149
|
/**
|
|
134
150
|
* **GET /meta/form/:name** — returns the serialized schema of a form
|
|
135
151
|
* referenced by an action's `inputForm` field. The form name is the
|
|
@@ -180,10 +196,20 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
180
196
|
* `@db.table.filterable / sortable 'manual'` + `@db.column.*`). Policy applies
|
|
181
197
|
* to filters and `$sort` only; `$groupBy`, `$having` keys and aggregate
|
|
182
198
|
* `$field`s use the physical capability alone.
|
|
199
|
+
*
|
|
200
|
+
* A filter entry is judged by its predicate class (the core's `canFilterLeaf`):
|
|
201
|
+
* `filterable` is the value-comparison verdict, `filterOps` the narrower
|
|
202
|
+
* predicates that still pass where it is `false`.
|
|
203
|
+
*
|
|
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.
|
|
183
207
|
*/
|
|
184
208
|
interface TFieldCapability {
|
|
185
|
-
/** A filter on this path passes the gate (adapter ∧ ¬writeOnly ∧ ¬encrypted ∧ policy). */
|
|
209
|
+
/** A value-comparison filter on this path passes the gate (adapter ∧ ¬writeOnly ∧ ¬encrypted ∧ policy). */
|
|
186
210
|
filterable: boolean;
|
|
211
|
+
/** Present when `filterable` is `false` yet narrower predicates (`$exists`, `$geoWithin`) pass the gate. */
|
|
212
|
+
filterOps?: string[];
|
|
187
213
|
/** A `$sort` on this path passes the gate (adapter ∧ ¬writeOnly ∧ ¬encrypted ∧ policy). */
|
|
188
214
|
sortable: boolean;
|
|
189
215
|
/** The path may appear in `$select`. `@db.writeOnly` fields are selectable — the seal strips them after the gate. */
|
|
@@ -194,6 +220,13 @@ interface TFieldCapability {
|
|
|
194
220
|
filterReason?: string;
|
|
195
221
|
/** Present when `sortable` is `false` — the reason clause appended to the HTTP 400 message. */
|
|
196
222
|
sortReason?: string;
|
|
223
|
+
/**
|
|
224
|
+
* A calendar bucket over this path passes the gate (¬writeOnly ∧ the core's
|
|
225
|
+
* `bucketSourceVerdict`). Since 0.1.132.
|
|
226
|
+
*/
|
|
227
|
+
bucketable: boolean;
|
|
228
|
+
/** Present when `bucketable` is `false` — the reason clause appended to the HTTP 400 message. */
|
|
229
|
+
bucketReason?: string;
|
|
197
230
|
}
|
|
198
231
|
/** One rejected path: `path` is the offending logical path, `message` the full sentence. */
|
|
199
232
|
interface TCapabilityVerdict {
|
|
@@ -201,13 +234,18 @@ interface TCapabilityVerdict {
|
|
|
201
234
|
message: string;
|
|
202
235
|
}
|
|
203
236
|
/** The readable members the index reads. */
|
|
204
|
-
type TCapabilityReadable = Pick<AtscriptDbReadable, "type" | "fieldDescriptors" | "flatMap" | "navFields" | "relations" | "ignoredFields" | "canFilterField" | "canSortField">;
|
|
237
|
+
type TCapabilityReadable = Pick<AtscriptDbReadable, "type" | "fieldDescriptors" | "flatMap" | "navFields" | "relations" | "ignoredFields" | "canFilterField" | "canSortField" | "isGeoSearchable" | "calendarBucketUnits" | "dimensions" | "measures">;
|
|
205
238
|
/**
|
|
206
|
-
* Capability index of one readable
|
|
239
|
+
* Capability index of one readable.
|
|
207
240
|
*
|
|
208
241
|
* - {@link entries} feeds `/meta.fields` (listed leaves in descriptor order);
|
|
209
242
|
* - {@link check} is the request gate: same inputs, same answer.
|
|
210
243
|
*
|
|
244
|
+
* Built from the adapter's capabilities as they are NOW. Some are only known
|
|
245
|
+
* after schema sync (PostgreSQL learns PostGIS there), so owners rebuild the
|
|
246
|
+
* index when {@link adapterSignature} changes (see `AsDbReadableController`'s
|
|
247
|
+
* `capabilities` getter) instead of keeping a constructor-time snapshot.
|
|
248
|
+
*
|
|
211
249
|
* Paths outside the index are classified by the core's `classifyQueryPath`
|
|
212
250
|
* (the same rules the core backstop applies) — navigation path, nested-object
|
|
213
251
|
* parent, JSON descendant (relational adapters), encrypted descendant,
|
|
@@ -224,9 +262,24 @@ declare class FieldCapabilityIndex implements TQueryPathSource {
|
|
|
224
262
|
readonly jsonParents: ReadonlySet<string>;
|
|
225
263
|
/** Descriptors carrying `@db.encrypted` (the ciphertext column on relational adapters). */
|
|
226
264
|
readonly encryptedFields: ReadonlySet<string>;
|
|
265
|
+
/** Every field descriptor's `physicalName` — names a calendar-bucket alias may not take. */
|
|
266
|
+
readonly physicalNames: ReadonlySet<string>;
|
|
267
|
+
/** Calendar-bucket units the adapter groups by, in `BUCKET_UNITS` order (`/meta.bucketUnits`). */
|
|
268
|
+
readonly bucketUnits: readonly BucketUnit[];
|
|
269
|
+
/** The adapter-level capabilities this index was built against — see {@link adapterSignature}. */
|
|
270
|
+
readonly signature: string;
|
|
271
|
+
/**
|
|
272
|
+
* The adapter-level capabilities that can change after construction (geo
|
|
273
|
+
* support, calendar-bucket units): an index whose {@link signature}
|
|
274
|
+
* differs from this is stale. Any new adapter-level input the index reads
|
|
275
|
+
* must be added here.
|
|
276
|
+
*/
|
|
277
|
+
static adapterSignature(source: Pick<TCapabilityReadable, "isGeoSearchable" | "calendarBucketUnits">): string;
|
|
227
278
|
private readonly _entries;
|
|
228
279
|
/** Nested-object parents (never listed, always selectable) → their listed leaves. */
|
|
229
280
|
private readonly _objectParents;
|
|
281
|
+
/** What `bucketSourceVerdict` reads of the table (JSON-value parents, dimensions, measures). */
|
|
282
|
+
private readonly _bucketTable;
|
|
230
283
|
/** Listed leaves — the {@link TQueryPathSource} view for `classifyQueryPath`. */
|
|
231
284
|
get leaves(): ReadonlyMap<string, unknown>;
|
|
232
285
|
/** Nested-object parents — the {@link TQueryPathSource} view for `classifyQueryPath`. */
|
|
@@ -239,18 +292,28 @@ declare class FieldCapabilityIndex implements TQueryPathSource {
|
|
|
239
292
|
isPhysicallyFilterable(path: string): boolean;
|
|
240
293
|
/**
|
|
241
294
|
* Gate check for one path in one position. Returns `undefined` when the
|
|
242
|
-
* path is accepted.
|
|
243
|
-
* the target table but is never a column here), then a listed leaf's
|
|
244
|
-
* capability (no existence lookup needed — every listed leaf is a real
|
|
245
|
-
* field), then `exists` (the readable's `isValidFieldPath`) and, for paths
|
|
246
|
-
* that exist but are not leaves, the storage classification.
|
|
295
|
+
* path is accepted.
|
|
247
296
|
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
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.
|
|
312
|
+
*
|
|
313
|
+
* `predicate` is a filter entry's class (`collectQueryPaths` records it per
|
|
314
|
+
* occurrence); it only matters for `op === "filter"` on a listed leaf.
|
|
252
315
|
*/
|
|
253
|
-
check(path: string, op: TQueryPathOp$1, exists: (path: string) => boolean): TCapabilityVerdict | undefined;
|
|
316
|
+
check(path: string, op: TQueryPathOp$1, exists: (path: string) => boolean, predicate?: TFilterPredicate): TCapabilityVerdict | undefined;
|
|
254
317
|
}
|
|
255
318
|
//#endregion
|
|
256
319
|
//#region src/as-db-readable.controller.d.ts
|
|
@@ -276,9 +339,18 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
276
339
|
* Per-path capability index (since 0.1.128): the ONE input both `/meta.fields`
|
|
277
340
|
* and the request gate ({@link checkCapabilities}) are computed from, so
|
|
278
341
|
* metadata and runtime can never diverge.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
342
|
+
*
|
|
343
|
+
* Built on first use and rebuilt whenever the adapter-level capabilities
|
|
344
|
+
* change (`FieldCapabilityIndex.adapterSignature`: geo support, calendar
|
|
345
|
+
* buckets) — PostgreSQL learns PostGIS only during schema sync, which may
|
|
346
|
+
* run after this controller is constructed, so a constructor-time snapshot
|
|
347
|
+
* would keep advertising (and gating) the pre-sync answer (since 0.1.132).
|
|
348
|
+
*/
|
|
349
|
+
protected get capabilities(): FieldCapabilityIndex;
|
|
350
|
+
private _capabilities?;
|
|
351
|
+
/** `/meta` is a projection of {@link capabilities}: a rebuilt index rebuilds the cached envelope. */
|
|
352
|
+
protected metaCacheKey(): unknown;
|
|
353
|
+
/** Bound once: the visibility check ({@link hasField}) the gate hands to `capabilities.check`. */
|
|
282
354
|
private readonly _exists;
|
|
283
355
|
private readonly _preferredIdSet;
|
|
284
356
|
private readonly _overlayIsNoOp;
|
|
@@ -298,6 +370,20 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
298
370
|
private _collectInvertibleFields;
|
|
299
371
|
private _collectQuantityRefs;
|
|
300
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
|
+
*/
|
|
301
387
|
protected hasField(path: string): boolean;
|
|
302
388
|
/**
|
|
303
389
|
* Structural capability gate (since 0.1.128): walks the PARSED query —
|
|
@@ -309,14 +395,29 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
309
395
|
*
|
|
310
396
|
* Rejections use the structured envelope `{ message, statusCode: 400,
|
|
311
397
|
* errors: [{ path, message }] }` — `path` is the offending logical path.
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
398
|
+
*
|
|
399
|
+
* Expects normalized `$select` computed entries ({@link checkComputedSelect}
|
|
400
|
+
* ran first); a bucket's source is checked like any other path (op
|
|
401
|
+
* `bucket`). After the per-path checks the core `$having` rule runs
|
|
402
|
+
* (`checkHavingKeys`: aliases or `$groupBy` fields only), so a readable mock
|
|
403
|
+
* and a real table answer alike.
|
|
315
404
|
*/
|
|
316
405
|
protected checkCapabilities(parsed: {
|
|
317
406
|
filter?: FilterExpr;
|
|
318
407
|
controls?: object;
|
|
319
408
|
}): HttpError | undefined;
|
|
409
|
+
/**
|
|
410
|
+
* The core's shared normalizer of `$select` computed entries
|
|
411
|
+
* (`resolveCalendarBuckets`) as a 400 with the core's wording and `path`
|
|
412
|
+
* (`$select` / `$groupBy`): entry shapes, calendar-bucket unit / zone /
|
|
413
|
+
* week start / alias, "grouped queries only", "must also appear in
|
|
414
|
+
* $groupBy", alias collisions with this table's fields. Runs once per
|
|
415
|
+
* request, before {@link checkCapabilities}: at the head of
|
|
416
|
+
* {@link validateParsed} — ahead of the controls DTO, which would otherwise
|
|
417
|
+
* answer a bucket in a non-grouped query with a generic type mismatch — or
|
|
418
|
+
* explicitly on the endpoint that skips it (`geo`).
|
|
419
|
+
*/
|
|
420
|
+
protected checkComputedSelect(controls: object | undefined): HttpError | undefined;
|
|
320
421
|
/**
|
|
321
422
|
* Root-path existence moved into {@link checkCapabilities}; the insights map
|
|
322
423
|
* only serves `$with` sub-controls here — the URL parser flattens
|
|
@@ -324,7 +425,7 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
324
425
|
* resolved against the target table through `isValidFieldPath`.
|
|
325
426
|
*/
|
|
326
427
|
protected validateInsights(insights: Map<string, unknown>): string | undefined;
|
|
327
|
-
/**
|
|
428
|
+
/** {@link checkComputedSelect} (before the controls DTO), then $with relations against the readable. */
|
|
328
429
|
protected validateParsed(parsed: Uniquery, type: "query" | "pages" | "getOne"): HttpError | undefined;
|
|
329
430
|
/**
|
|
330
431
|
* Compute an embedding vector from a search term.
|
package/dist/index.d.mts
CHANGED
|
@@ -3,7 +3,7 @@ import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, TSerializ
|
|
|
3
3
|
import { HttpError } from "@moostjs/event-http";
|
|
4
4
|
import { Mate, Moost, TConsoleBase, TMateParamMeta, TMoostMetadata } from "moost";
|
|
5
5
|
import { parseUrl } from "@uniqu/url";
|
|
6
|
-
import { AtscriptDbReadable, AtscriptDbTable, FilterExpr, FlatOf, TCrudOp, TCrudPermissions, TCrudPermissions as TCrudPermissions$1, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionIntent as TDbActionIntent$1, TDbActionLevel, TDbActionLevel as TDbActionLevel$1, TDbActionProcessor, TDbFieldMeta, TDbRemoveGuardContext, TDbRemoveGuardContext as TDbRemoveGuardContext$1, TDbWriteAction, TDbWriteAction as TDbWriteAction$1, TDbWriteGuardContext, TDbWriteGuardContext as TDbWriteGuardContext$1, TIdentification, TMetaResponse, TQueryPathOp, TQueryPathOp as TQueryPathOp$1, TQueryPathRefs, TQueryPathSource, Uniquery, UniqueryControls, collectQueryPaths } from "@atscript/db";
|
|
6
|
+
import { AtscriptDbReadable, AtscriptDbTable, BucketUnit, FilterExpr, FlatOf, TCrudOp, TCrudPermissions, TCrudPermissions as TCrudPermissions$1, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionIntent as TDbActionIntent$1, TDbActionLevel, TDbActionLevel as TDbActionLevel$1, TDbActionProcessor, TDbFieldMeta, TDbRemoveGuardContext, TDbRemoveGuardContext as TDbRemoveGuardContext$1, TDbWriteAction, TDbWriteAction as TDbWriteAction$1, TDbWriteGuardContext, TDbWriteGuardContext as TDbWriteGuardContext$1, TFilterPredicate, TIdentification, TMetaResponse, TQueryPathOp, TQueryPathOp as TQueryPathOp$1, TQueryPathRefs, TQueryPathSource, Uniquery, UniqueryControls, collectQueryPaths } from "@atscript/db";
|
|
7
7
|
//#region src/as-readable.controller.d.ts
|
|
8
8
|
/**
|
|
9
9
|
* Abstract base class for read-only HTTP controllers over an Atscript interface.
|
|
@@ -22,7 +22,9 @@ import { AtscriptDbReadable, AtscriptDbTable, FilterExpr, FlatOf, TCrudOp, TCrud
|
|
|
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).
|
|
@@ -40,10 +42,16 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
40
42
|
private _serializedType?;
|
|
41
43
|
/** Cached full meta response (computed lazily on first meta() call). */
|
|
42
44
|
private _metaResponse?;
|
|
45
|
+
/** {@link metaCacheKey} the cached response was built for. */
|
|
46
|
+
private _metaResponseKey?;
|
|
43
47
|
/** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
|
|
44
48
|
private _formSchemas;
|
|
45
49
|
constructor(boundType: T, controllerName: string, app: Moost, kindTag?: string);
|
|
46
|
-
/**
|
|
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
|
+
*/
|
|
47
55
|
protected abstract hasField(path: string): boolean;
|
|
48
56
|
/** Sets @db.http.path on the type metadata from the controller's computed prefix. */
|
|
49
57
|
private _resolveHttpPath;
|
|
@@ -126,10 +134,18 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
126
134
|
protected returnOne(result: Promise<DataType | null>): Promise<DataType | HttpError>;
|
|
127
135
|
/**
|
|
128
136
|
* **GET /meta** — returns the bound interface's metadata envelope. The
|
|
129
|
-
* static envelope is cached
|
|
130
|
-
* subclasses can prune the
|
|
137
|
+
* static envelope is cached (rebuilt when {@link metaCacheKey} changes);
|
|
138
|
+
* {@link applyMetaOverlay} runs per request so subclasses can prune the
|
|
139
|
+
* response by principal.
|
|
131
140
|
*/
|
|
132
141
|
meta(): Promise<TMetaResponse>;
|
|
142
|
+
/**
|
|
143
|
+
* Identity of the inputs the cached `/meta` envelope is built from — a new
|
|
144
|
+
* value rebuilds it. Default: constant (built once). The DB readable
|
|
145
|
+
* controller returns its capability index, which is rebuilt when the
|
|
146
|
+
* adapter's capabilities change (since 0.1.132).
|
|
147
|
+
*/
|
|
148
|
+
protected metaCacheKey(): unknown;
|
|
133
149
|
/**
|
|
134
150
|
* **GET /meta/form/:name** — returns the serialized schema of a form
|
|
135
151
|
* referenced by an action's `inputForm` field. The form name is the
|
|
@@ -180,10 +196,20 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
180
196
|
* `@db.table.filterable / sortable 'manual'` + `@db.column.*`). Policy applies
|
|
181
197
|
* to filters and `$sort` only; `$groupBy`, `$having` keys and aggregate
|
|
182
198
|
* `$field`s use the physical capability alone.
|
|
199
|
+
*
|
|
200
|
+
* A filter entry is judged by its predicate class (the core's `canFilterLeaf`):
|
|
201
|
+
* `filterable` is the value-comparison verdict, `filterOps` the narrower
|
|
202
|
+
* predicates that still pass where it is `false`.
|
|
203
|
+
*
|
|
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.
|
|
183
207
|
*/
|
|
184
208
|
interface TFieldCapability {
|
|
185
|
-
/** A filter on this path passes the gate (adapter ∧ ¬writeOnly ∧ ¬encrypted ∧ policy). */
|
|
209
|
+
/** A value-comparison filter on this path passes the gate (adapter ∧ ¬writeOnly ∧ ¬encrypted ∧ policy). */
|
|
186
210
|
filterable: boolean;
|
|
211
|
+
/** Present when `filterable` is `false` yet narrower predicates (`$exists`, `$geoWithin`) pass the gate. */
|
|
212
|
+
filterOps?: string[];
|
|
187
213
|
/** A `$sort` on this path passes the gate (adapter ∧ ¬writeOnly ∧ ¬encrypted ∧ policy). */
|
|
188
214
|
sortable: boolean;
|
|
189
215
|
/** The path may appear in `$select`. `@db.writeOnly` fields are selectable — the seal strips them after the gate. */
|
|
@@ -194,6 +220,13 @@ interface TFieldCapability {
|
|
|
194
220
|
filterReason?: string;
|
|
195
221
|
/** Present when `sortable` is `false` — the reason clause appended to the HTTP 400 message. */
|
|
196
222
|
sortReason?: string;
|
|
223
|
+
/**
|
|
224
|
+
* A calendar bucket over this path passes the gate (¬writeOnly ∧ the core's
|
|
225
|
+
* `bucketSourceVerdict`). Since 0.1.132.
|
|
226
|
+
*/
|
|
227
|
+
bucketable: boolean;
|
|
228
|
+
/** Present when `bucketable` is `false` — the reason clause appended to the HTTP 400 message. */
|
|
229
|
+
bucketReason?: string;
|
|
197
230
|
}
|
|
198
231
|
/** One rejected path: `path` is the offending logical path, `message` the full sentence. */
|
|
199
232
|
interface TCapabilityVerdict {
|
|
@@ -201,13 +234,18 @@ interface TCapabilityVerdict {
|
|
|
201
234
|
message: string;
|
|
202
235
|
}
|
|
203
236
|
/** The readable members the index reads. */
|
|
204
|
-
type TCapabilityReadable = Pick<AtscriptDbReadable, "type" | "fieldDescriptors" | "flatMap" | "navFields" | "relations" | "ignoredFields" | "canFilterField" | "canSortField">;
|
|
237
|
+
type TCapabilityReadable = Pick<AtscriptDbReadable, "type" | "fieldDescriptors" | "flatMap" | "navFields" | "relations" | "ignoredFields" | "canFilterField" | "canSortField" | "isGeoSearchable" | "calendarBucketUnits" | "dimensions" | "measures">;
|
|
205
238
|
/**
|
|
206
|
-
* Capability index of one readable
|
|
239
|
+
* Capability index of one readable.
|
|
207
240
|
*
|
|
208
241
|
* - {@link entries} feeds `/meta.fields` (listed leaves in descriptor order);
|
|
209
242
|
* - {@link check} is the request gate: same inputs, same answer.
|
|
210
243
|
*
|
|
244
|
+
* Built from the adapter's capabilities as they are NOW. Some are only known
|
|
245
|
+
* after schema sync (PostgreSQL learns PostGIS there), so owners rebuild the
|
|
246
|
+
* index when {@link adapterSignature} changes (see `AsDbReadableController`'s
|
|
247
|
+
* `capabilities` getter) instead of keeping a constructor-time snapshot.
|
|
248
|
+
*
|
|
211
249
|
* Paths outside the index are classified by the core's `classifyQueryPath`
|
|
212
250
|
* (the same rules the core backstop applies) — navigation path, nested-object
|
|
213
251
|
* parent, JSON descendant (relational adapters), encrypted descendant,
|
|
@@ -224,9 +262,24 @@ declare class FieldCapabilityIndex implements TQueryPathSource {
|
|
|
224
262
|
readonly jsonParents: ReadonlySet<string>;
|
|
225
263
|
/** Descriptors carrying `@db.encrypted` (the ciphertext column on relational adapters). */
|
|
226
264
|
readonly encryptedFields: ReadonlySet<string>;
|
|
265
|
+
/** Every field descriptor's `physicalName` — names a calendar-bucket alias may not take. */
|
|
266
|
+
readonly physicalNames: ReadonlySet<string>;
|
|
267
|
+
/** Calendar-bucket units the adapter groups by, in `BUCKET_UNITS` order (`/meta.bucketUnits`). */
|
|
268
|
+
readonly bucketUnits: readonly BucketUnit[];
|
|
269
|
+
/** The adapter-level capabilities this index was built against — see {@link adapterSignature}. */
|
|
270
|
+
readonly signature: string;
|
|
271
|
+
/**
|
|
272
|
+
* The adapter-level capabilities that can change after construction (geo
|
|
273
|
+
* support, calendar-bucket units): an index whose {@link signature}
|
|
274
|
+
* differs from this is stale. Any new adapter-level input the index reads
|
|
275
|
+
* must be added here.
|
|
276
|
+
*/
|
|
277
|
+
static adapterSignature(source: Pick<TCapabilityReadable, "isGeoSearchable" | "calendarBucketUnits">): string;
|
|
227
278
|
private readonly _entries;
|
|
228
279
|
/** Nested-object parents (never listed, always selectable) → their listed leaves. */
|
|
229
280
|
private readonly _objectParents;
|
|
281
|
+
/** What `bucketSourceVerdict` reads of the table (JSON-value parents, dimensions, measures). */
|
|
282
|
+
private readonly _bucketTable;
|
|
230
283
|
/** Listed leaves — the {@link TQueryPathSource} view for `classifyQueryPath`. */
|
|
231
284
|
get leaves(): ReadonlyMap<string, unknown>;
|
|
232
285
|
/** Nested-object parents — the {@link TQueryPathSource} view for `classifyQueryPath`. */
|
|
@@ -239,18 +292,28 @@ declare class FieldCapabilityIndex implements TQueryPathSource {
|
|
|
239
292
|
isPhysicallyFilterable(path: string): boolean;
|
|
240
293
|
/**
|
|
241
294
|
* Gate check for one path in one position. Returns `undefined` when the
|
|
242
|
-
* path is accepted.
|
|
243
|
-
* the target table but is never a column here), then a listed leaf's
|
|
244
|
-
* capability (no existence lookup needed — every listed leaf is a real
|
|
245
|
-
* field), then `exists` (the readable's `isValidFieldPath`) and, for paths
|
|
246
|
-
* that exist but are not leaves, the storage classification.
|
|
295
|
+
* path is accepted.
|
|
247
296
|
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
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.
|
|
312
|
+
*
|
|
313
|
+
* `predicate` is a filter entry's class (`collectQueryPaths` records it per
|
|
314
|
+
* occurrence); it only matters for `op === "filter"` on a listed leaf.
|
|
252
315
|
*/
|
|
253
|
-
check(path: string, op: TQueryPathOp$1, exists: (path: string) => boolean): TCapabilityVerdict | undefined;
|
|
316
|
+
check(path: string, op: TQueryPathOp$1, exists: (path: string) => boolean, predicate?: TFilterPredicate): TCapabilityVerdict | undefined;
|
|
254
317
|
}
|
|
255
318
|
//#endregion
|
|
256
319
|
//#region src/as-db-readable.controller.d.ts
|
|
@@ -276,9 +339,18 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
276
339
|
* Per-path capability index (since 0.1.128): the ONE input both `/meta.fields`
|
|
277
340
|
* and the request gate ({@link checkCapabilities}) are computed from, so
|
|
278
341
|
* metadata and runtime can never diverge.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
342
|
+
*
|
|
343
|
+
* Built on first use and rebuilt whenever the adapter-level capabilities
|
|
344
|
+
* change (`FieldCapabilityIndex.adapterSignature`: geo support, calendar
|
|
345
|
+
* buckets) — PostgreSQL learns PostGIS only during schema sync, which may
|
|
346
|
+
* run after this controller is constructed, so a constructor-time snapshot
|
|
347
|
+
* would keep advertising (and gating) the pre-sync answer (since 0.1.132).
|
|
348
|
+
*/
|
|
349
|
+
protected get capabilities(): FieldCapabilityIndex;
|
|
350
|
+
private _capabilities?;
|
|
351
|
+
/** `/meta` is a projection of {@link capabilities}: a rebuilt index rebuilds the cached envelope. */
|
|
352
|
+
protected metaCacheKey(): unknown;
|
|
353
|
+
/** Bound once: the visibility check ({@link hasField}) the gate hands to `capabilities.check`. */
|
|
282
354
|
private readonly _exists;
|
|
283
355
|
private readonly _preferredIdSet;
|
|
284
356
|
private readonly _overlayIsNoOp;
|
|
@@ -298,6 +370,20 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
298
370
|
private _collectInvertibleFields;
|
|
299
371
|
private _collectQuantityRefs;
|
|
300
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
|
+
*/
|
|
301
387
|
protected hasField(path: string): boolean;
|
|
302
388
|
/**
|
|
303
389
|
* Structural capability gate (since 0.1.128): walks the PARSED query —
|
|
@@ -309,14 +395,29 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
309
395
|
*
|
|
310
396
|
* Rejections use the structured envelope `{ message, statusCode: 400,
|
|
311
397
|
* errors: [{ path, message }] }` — `path` is the offending logical path.
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
398
|
+
*
|
|
399
|
+
* Expects normalized `$select` computed entries ({@link checkComputedSelect}
|
|
400
|
+
* ran first); a bucket's source is checked like any other path (op
|
|
401
|
+
* `bucket`). After the per-path checks the core `$having` rule runs
|
|
402
|
+
* (`checkHavingKeys`: aliases or `$groupBy` fields only), so a readable mock
|
|
403
|
+
* and a real table answer alike.
|
|
315
404
|
*/
|
|
316
405
|
protected checkCapabilities(parsed: {
|
|
317
406
|
filter?: FilterExpr;
|
|
318
407
|
controls?: object;
|
|
319
408
|
}): HttpError | undefined;
|
|
409
|
+
/**
|
|
410
|
+
* The core's shared normalizer of `$select` computed entries
|
|
411
|
+
* (`resolveCalendarBuckets`) as a 400 with the core's wording and `path`
|
|
412
|
+
* (`$select` / `$groupBy`): entry shapes, calendar-bucket unit / zone /
|
|
413
|
+
* week start / alias, "grouped queries only", "must also appear in
|
|
414
|
+
* $groupBy", alias collisions with this table's fields. Runs once per
|
|
415
|
+
* request, before {@link checkCapabilities}: at the head of
|
|
416
|
+
* {@link validateParsed} — ahead of the controls DTO, which would otherwise
|
|
417
|
+
* answer a bucket in a non-grouped query with a generic type mismatch — or
|
|
418
|
+
* explicitly on the endpoint that skips it (`geo`).
|
|
419
|
+
*/
|
|
420
|
+
protected checkComputedSelect(controls: object | undefined): HttpError | undefined;
|
|
320
421
|
/**
|
|
321
422
|
* Root-path existence moved into {@link checkCapabilities}; the insights map
|
|
322
423
|
* only serves `$with` sub-controls here — the URL parser flattens
|
|
@@ -324,7 +425,7 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
324
425
|
* resolved against the target table through `isValidFieldPath`.
|
|
325
426
|
*/
|
|
326
427
|
protected validateInsights(insights: Map<string, unknown>): string | undefined;
|
|
327
|
-
/**
|
|
428
|
+
/** {@link checkComputedSelect} (before the controls DTO), then $with relations against the readable. */
|
|
328
429
|
protected validateParsed(parsed: Uniquery, type: "query" | "pages" | "getOne"): HttpError | undefined;
|
|
329
430
|
/**
|
|
330
431
|
* Compute an embedding vector from a search term.
|