@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.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} so insights validation can reject unknown keys.
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
- /** Subclass contract: return `true` if `path` addresses a valid field on the bound source. */
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; {@link applyMetaOverlay} runs per request so
130
- * subclasses can prune the response by principal.
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, built once per controller.
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. Order: navigation paths first (a nav path "exists" on
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
- * Existence deliberately runs BEFORE the JSON / encrypted classification:
249
- * an untyped descendant of a JSON column (`address.nope`) is reported as
250
- * `Unknown field`, not as "inside JSON-stored column" — clients pin that
251
- * wording, so do not "align" it with the core backstop's text.
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
- protected readonly capabilities: FieldCapabilityIndex;
281
- /** Bound once: the field-existence check the gate hands to `capabilities.check`. */
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
- * After the per-path checks the core `$having` rule runs (`checkHavingKeys`:
313
- * aliases or `$groupBy` fields only), so a readable mock and a real table
314
- * answer alike.
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
- /** Validates $with relations against the readable. */
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} so insights validation can reject unknown keys.
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
- /** Subclass contract: return `true` if `path` addresses a valid field on the bound source. */
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; {@link applyMetaOverlay} runs per request so
130
- * subclasses can prune the response by principal.
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, built once per controller.
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. Order: navigation paths first (a nav path "exists" on
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
- * Existence deliberately runs BEFORE the JSON / encrypted classification:
249
- * an untyped descendant of a JSON column (`address.nope`) is reported as
250
- * `Unknown field`, not as "inside JSON-stored column" — clients pin that
251
- * wording, so do not "align" it with the core backstop's text.
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
- protected readonly capabilities: FieldCapabilityIndex;
281
- /** Bound once: the field-existence check the gate hands to `capabilities.check`. */
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
- * After the per-path checks the core `$having` rule runs (`checkHavingKeys`:
313
- * aliases or `$groupBy` fields only), so a readable mock and a real table
314
- * answer alike.
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
- /** Validates $with relations against the readable. */
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.