@adonis-agora/filter 0.5.0 → 0.7.0

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +1 -1
  3. package/dist/configure.d.ts +15 -0
  4. package/dist/configure.d.ts.map +1 -0
  5. package/dist/configure.js +20 -0
  6. package/dist/configure.js.map +1 -0
  7. package/dist/src/aggregate.d.ts +61 -0
  8. package/dist/src/aggregate.d.ts.map +1 -0
  9. package/dist/src/aggregate.js +132 -0
  10. package/dist/src/aggregate.js.map +1 -0
  11. package/dist/src/aggregate_path.d.ts +25 -0
  12. package/dist/src/aggregate_path.d.ts.map +1 -0
  13. package/dist/src/aggregate_path.js +35 -0
  14. package/dist/src/aggregate_path.js.map +1 -0
  15. package/dist/src/field_aliases.d.ts +2 -0
  16. package/dist/src/field_aliases.d.ts.map +1 -1
  17. package/dist/src/field_aliases.js +4 -0
  18. package/dist/src/field_aliases.js.map +1 -1
  19. package/dist/src/filter_spec.d.ts +46 -1
  20. package/dist/src/filter_spec.d.ts.map +1 -1
  21. package/dist/src/filter_spec.js +16 -0
  22. package/dist/src/filter_spec.js.map +1 -1
  23. package/dist/src/generate_client.d.ts.map +1 -1
  24. package/dist/src/generate_client.js +5 -0
  25. package/dist/src/generate_client.js.map +1 -1
  26. package/dist/src/index.d.ts +9 -5
  27. package/dist/src/index.d.ts.map +1 -1
  28. package/dist/src/index.js +6 -4
  29. package/dist/src/index.js.map +1 -1
  30. package/dist/src/lucid_adapter.d.ts +45 -1
  31. package/dist/src/lucid_adapter.d.ts.map +1 -1
  32. package/dist/src/lucid_adapter.js +123 -0
  33. package/dist/src/lucid_adapter.js.map +1 -1
  34. package/dist/src/parse_request.d.ts +21 -1
  35. package/dist/src/parse_request.d.ts.map +1 -1
  36. package/dist/src/parse_request.js +90 -4
  37. package/dist/src/parse_request.js.map +1 -1
  38. package/dist/src/runner.d.ts.map +1 -1
  39. package/dist/src/runner.js +144 -17
  40. package/dist/src/runner.js.map +1 -1
  41. package/dist/src/spatie_parser.d.ts +1 -0
  42. package/dist/src/spatie_parser.d.ts.map +1 -1
  43. package/dist/src/spatie_parser.js +5 -1
  44. package/dist/src/spatie_parser.js.map +1 -1
  45. package/dist/src/testing.d.ts +2 -0
  46. package/dist/src/testing.d.ts.map +1 -1
  47. package/dist/src/testing.js +4 -0
  48. package/dist/src/testing.js.map +1 -1
  49. package/dist/src/types.d.ts +51 -0
  50. package/dist/src/types.d.ts.map +1 -1
  51. package/package.json +16 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,141 @@
1
1
  # @adonis-agora/filter
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#23](https://github.com/DavideCarvalho/adonis-agora-filter/pull/23) [`2bd4e7b`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/2bd4e7baa50d3d0b4a25c834fc9edf612f7d854a) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - `parseSpatieRequest` now parses `distinct`, matching `parseFilterRequest`.
8
+
9
+ `parseSpatieRequest` is documented as the additive counterpart to
10
+ `parseFilterRequest` — same filter, sort and search shapes, plus cursor
11
+ pagination, includes and sparse fieldsets — so a controller can swap one for the
12
+ other. It did not read `distinct`, even though the runner applies it. Swapping
13
+ the parser turned a working `?distinct=city` into a full, un-deduped result set
14
+ with nothing to signal that the parameter had been dropped.
15
+
16
+ Both string (`distinct=city,tier`) and repeated (`distinct[]=city&distinct[]=tier`)
17
+ forms now parse identically in the two parsers.
18
+
19
+ - [#23](https://github.com/DavideCarvalho/adonis-agora-filter/pull/23) [`2bd4e7b`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/2bd4e7baa50d3d0b4a25c834fc9edf612f7d854a) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Add the `configure` hook, so `node ace add @adonis-agora/filter` actually wires the package.
20
+
21
+ The install instructions have always said to run `node ace add @adonis-agora/filter`,
22
+ but the package shipped no `configure` hook, so the command installed the
23
+ dependency and wired nothing. Two things silently did not happen: the provider
24
+ was never added to `adonisrc.ts` (no `applyFilterFromRequest` / `filterPaginate`
25
+ macros on `ModelQueryBuilder`), and the commands barrel was never registered, so
26
+ `make:filter-client` never appeared in `node ace list`.
27
+
28
+ `node ace add @adonis-agora/filter` — or `node ace configure @adonis-agora/filter`
29
+ on an already-installed package — now registers both:
30
+
31
+ ```ts
32
+ // adonisrc.ts
33
+ providers: [() => import('@adonis-agora/filter/filter_provider')],
34
+ commands: [() => import('@adonis-agora/filter/commands')],
35
+ ```
36
+
37
+ Nothing is published to `config/`: a filter policy is a per-model
38
+ `defineFilterSpec` call in your own code, not global configuration. Apps that
39
+ wired those two entries by hand need no change — the codemod is idempotent.
40
+
41
+ - [#23](https://github.com/DavideCarvalho/adonis-agora-filter/pull/23) [`2bd4e7b`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/2bd4e7baa50d3d0b4a25c834fc9edf612f7d854a) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - `parseFilterRequest` now understands the client builder's structured shape, closing a silent unfiltered-response bug.
42
+
43
+ A POST search endpoint that hands `filterQuery()…build()` straight to
44
+ `parseFilterRequest` — the pairing the guides describe — did not work. The
45
+ builder returns
46
+
47
+ ```ts
48
+ { filter: { where: [{ field: 'status', operator: 'in', value: [...] }] },
49
+ sort: [{ field: 'createdAt', direction: 'desc' }],
50
+ paginate: { page: 2, size: 25 } }
51
+ ```
52
+
53
+ and the parser reshaped it as if `where` were a column: the entire condition list
54
+ became one `in` filter on a field named `where`, which the allow-list then
55
+ pruned, while `sort` and `paginate` were discarded for not being strings. The
56
+ endpoint answered with **every row, unsorted and unpaginated**, and raised
57
+ nothing — the failure looked like a working search with a broad result set.
58
+
59
+ The same shape reaches a plain GET too, because OR/AND groups serialize to a
60
+ top-level `where[0][field]=…`, so grouped queries were silently unfiltered as
61
+ well.
62
+
63
+ `parseFilterRequest` now recognises an already-structured condition list — under
64
+ `filter.where` or at the top level — and takes it as the filters, with nested
65
+ `AND`/`OR` groups intact. It also reads `sort` in the `[{ field, direction }]`
66
+ form and maps `paginate: { page, size }` onto `page`/`size`.
67
+
68
+ Existing requests are unaffected: `filter[field]=…`, `sort=-createdAt`,
69
+ `page`/`size` and `page[number]`/`page[size]` parse exactly as before, and a real
70
+ column named `where` (`filter[where]=lobby`, `filter[where][contains]=lob`,
71
+ `filter[where][]=a&filter[where][]=b`) is still treated as a column — only an
72
+ array of `{ field, operator }` records is read as a structured list, and a query
73
+ string cannot produce one by accident.
74
+
75
+ Note that `include` is still not consumed: eager-loading stays the caller's
76
+ `preload` call.
77
+
78
+ ### Patch Changes
79
+
80
+ - [#23](https://github.com/DavideCarvalho/adonis-agora-filter/pull/23) [`2bd4e7b`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/2bd4e7baa50d3d0b4a25c834fc9edf612f7d854a) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Report the real version from the exported `VERSION` constant.
81
+
82
+ `VERSION` is a hand-written literal next to a "keep in sync with package.json"
83
+ comment, and it had not been touched since the first release: the package shipped
84
+ `0.2.0` through `0.6.0` while `VERSION` still answered `'0.1.0'`. Anything gating
85
+ on it — a feature check, a bug report, a diagnostics banner — got a wrong answer.
86
+
87
+ It now reads `0.6.0`, and a test compares it against `package.json` so the next
88
+ release cannot silently drift again.
89
+
90
+ - [#22](https://github.com/DavideCarvalho/adonis-agora-filter/pull/22) [`a33b2fe`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/a33b2febc48baa3dafcf6a703e4250bdebbde275) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Fix a 500 when `distinct` is given a relation path.
91
+
92
+ `distinct` was gated on the `allowed` list and nothing else. A relation path like
93
+ `posts.title` is filterable by design — declaring `relations: { posts: { filterable: ['title'] } }`
94
+ whitelists it, and `make:filter-client` enumerates it into the generated field
95
+ union — so `?distinct=posts.title` cleared the check and was handed to Lucid
96
+ verbatim:
97
+
98
+ ```sql
99
+ select distinct "posts"."title" from "users"
100
+ --> ERROR: missing FROM-clause entry for table "posts"
101
+ ```
102
+
103
+ Lucid filters a relation with a correlated `EXISTS` subquery, so the relation is
104
+ never joined into the outer `FROM` and there is no alias to project a column
105
+ from. A `whereHas` on the same relation in the same request does not help.
106
+
107
+ **If your `distinct` works today, nothing changes.** Root-table columns —
108
+ including ones qualified with the root table's own name — behave exactly as
109
+ before, as do the alias resolution and the allow-list drop.
110
+
111
+ **If you pass a relation path** (`posts.title`) or a to-many aggregate path
112
+ (`posts.$count`), you now get a defined refusal instead of a database error: the
113
+ field is dropped from the `distinct` list (the remaining fields still apply), or
114
+ raises `InvalidColumnFilterError` — a 400, not a 500 — when your spec sets
115
+ `throwOnInvalid`. The message names the cause: it is the missing join, not the
116
+ allow-list, so adding the path to `filterable` will not (and should not) change
117
+ it. Filtering on that path keeps working; only projecting it is refused.
118
+
119
+ - [#23](https://github.com/DavideCarvalho/adonis-agora-filter/pull/23) [`2bd4e7b`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/2bd4e7baa50d3d0b4a25c834fc9edf612f7d854a) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Declare a supported Node range in `engines` again, instead of one exact version.
120
+
121
+ Both packages shipped `"engines": { "node": "v26.7.0" }` — an exact version, and
122
+ plainly the output of `node -v`, leading `v` and all. Every consumer on any other
123
+ Node got an unsatisfied-engine warning on install, and anyone running with
124
+ `engine-strict` (or a package manager that treats it as fatal) could not install
125
+ at all. The pinned version was also higher than anything the project itself uses:
126
+ CI runs Node 22 and `.nvmrc` names Node 20.
127
+
128
+ `engines` states the floor the package actually supports, which is `>=20.6.0` —
129
+ the same range it declared before, and the one the rest of the Agora packages
130
+ use. Nothing about the code changed; this only stops a false incompatibility
131
+ signal.
132
+
133
+ ## 0.6.0
134
+
135
+ ### Minor Changes
136
+
137
+ - Parity sync from nestjs-filter: execute the server-side `distinct` projection (was a silent no-op), computed (virtual) fields for filter + sort (verbatim-string + `({alias}) => sql` forms), and native to-many aggregate fields (`$count`/`$sum`/`$avg`/`$min`/`$max`) auto-discovered from Lucid relation metadata — value stays parameterized (injection-safe), identifiers quoted.
138
+
3
139
  ## 0.5.0
4
140
 
5
141
  ### Minor Changes
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # `@adonis-agora/filter`
2
2
 
3
3
  Server-side query filtering, sorting, and pagination for AdonisJS. Parses the
4
- Spatie/JSON:API query the `@agora/filter-client` builder emits, applies it to a
4
+ Spatie/JSON:API query the `@adonis-agora/filter-client` builder emits, applies it to a
5
5
  Lucid query through a field allow-list, and resolves offset or keyset (cursor)
6
6
  pagination.
7
7
 
@@ -0,0 +1,15 @@
1
+ import type Configure from '@adonisjs/core/commands/configure';
2
+ /**
3
+ * `node ace add @adonis-agora/filter` (or `node ace configure @adonis-agora/filter`)
4
+ * — auto-wires the package into an AdonisJS app:
5
+ *
6
+ * 1. registers the service provider in `adonisrc.ts`, which installs the
7
+ * chainable `applyFilterFromRequest` / `filterPaginate` Lucid macros;
8
+ * 2. registers the ace commands barrel, which is what makes `make:filter-client`
9
+ * show up in `node ace list`.
10
+ *
11
+ * There is no config file to publish: a filter policy is a per-model
12
+ * `defineFilterSpec` call in your own code, not global configuration.
13
+ */
14
+ export declare function configure(command: Configure): Promise<void>;
15
+ //# sourceMappingURL=configure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configure.d.ts","sourceRoot":"","sources":["../configure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,mCAAmC,CAAC;AAE/D;;;;;;;;;;;GAWG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,SAAS,iBAOjD"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `node ace add @adonis-agora/filter` (or `node ace configure @adonis-agora/filter`)
3
+ * — auto-wires the package into an AdonisJS app:
4
+ *
5
+ * 1. registers the service provider in `adonisrc.ts`, which installs the
6
+ * chainable `applyFilterFromRequest` / `filterPaginate` Lucid macros;
7
+ * 2. registers the ace commands barrel, which is what makes `make:filter-client`
8
+ * show up in `node ace list`.
9
+ *
10
+ * There is no config file to publish: a filter policy is a per-model
11
+ * `defineFilterSpec` call in your own code, not global configuration.
12
+ */
13
+ export async function configure(command) {
14
+ const codemods = await command.createCodemods();
15
+ await codemods.updateRcFile((rcFile) => {
16
+ rcFile.addProvider('@adonis-agora/filter/filter_provider');
17
+ rcFile.addCommand('@adonis-agora/filter/commands');
18
+ });
19
+ }
20
+ //# sourceMappingURL=configure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configure.js","sourceRoot":"","sources":["../configure.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAkB;IAChD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,cAAc,EAAE,CAAC;IAEhD,MAAM,QAAQ,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,EAAE;QACrC,MAAM,CAAC,WAAW,CAAC,sCAAsC,CAAC,CAAC;QAC3D,MAAM,CAAC,UAAU,CAAC,+BAA+B,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,61 @@
1
+ import type { RelationSpec } from './filter_spec.js';
2
+ import type { ComputedFields } from './types.js';
3
+ /**
4
+ * The structural slice of a booted Lucid relation the aggregate-subquery
5
+ * compiler reads. A real `@adonisjs/lucid` `HasMany` / `ManyToMany` relation
6
+ * satisfies it — declared locally so this module never hard-imports Lucid. The
7
+ * key-column fields are optional because they differ by relation kind
8
+ * (one-to-many exposes `foreignKeyColumnName`; many-to-many exposes the pivot
9
+ * fields), and only the relevant ones are read per kind.
10
+ */
11
+ export interface LucidRelationLike {
12
+ /** Relation kind — `'hasMany'` / `'manyToMany'` are the to-many kinds we correlate. */
13
+ type: string;
14
+ /** Compute the relation's key columns from the naming strategy (idempotent). */
15
+ boot(): void;
16
+ /** The related (child) model — its `table` is the child table name. */
17
+ relatedModel(): {
18
+ table?: string;
19
+ };
20
+ foreignKeyColumnName?: string;
21
+ localKeyColumnName?: string;
22
+ relatedKeyColumnName?: string;
23
+ pivotTable?: string;
24
+ pivotForeignKey?: string;
25
+ pivotRelatedForeignKey?: string;
26
+ }
27
+ /**
28
+ * The structural slice of a Lucid model the aggregate discovery reads: its table
29
+ * name and a relation accessor. `$getRelation` is typed with `any` for the same
30
+ * reason `QueryBuilderLike.whereHas` is — Lucid types it with a generic
31
+ * literal-union `Name`, and `string` is not assignable to that union, so a
32
+ * narrower signature would make every real model fail to satisfy this interface.
33
+ */
34
+ export interface LucidModelLike {
35
+ table?: string;
36
+ $getRelation(name: any): LucidRelationLike | undefined;
37
+ }
38
+ /**
39
+ * Discover to-many aggregate {@link ComputedSource}s from a Lucid model's
40
+ * relation metadata, for the relations the spec whitelisted. For each declared
41
+ * to-many relation (hasMany / manyToMany) this synthesises:
42
+ *
43
+ * - `<rel>.$count` — always;
44
+ * - `<rel>.$sum|$avg|$min|$max.<col>` — one per column the relation's
45
+ * {@link RelationSpec.aggregates} declares (Lucid does not reflect SQL column
46
+ * types, so the dev asserts numeric-ness by listing the column there).
47
+ *
48
+ * To-one relations (belongsTo / hasOne) are excluded. The whole thing degrades
49
+ * gracefully: a relation that is not on the model, is to-one, or cannot be
50
+ * booted (e.g. no DB adapter yet) is simply skipped, and a model without the
51
+ * introspection capability yields an empty map — so aggregates activate only
52
+ * when the metadata is actually available.
53
+ *
54
+ * The result is merged into the spec's `computed` map, so aggregate fields ARE
55
+ * computed fields with auto-generated correlated-subquery sources — they reuse
56
+ * the exact same runner routing, allow-list bypass (their discovery IS their
57
+ * allow-list, gated by the relation whitelist), injection-safe value binding,
58
+ * and codegen surfacing.
59
+ */
60
+ export declare function discoverAggregateSources(model: LucidModelLike | undefined, relations: Readonly<Record<string, RelationSpec>>): ComputedFields;
61
+ //# sourceMappingURL=aggregate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregate.d.ts","sourceRoot":"","sources":["../../src/aggregate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,YAAY,CAAC;AAEjE;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,IAAI,IAAI,IAAI,CAAC;IACb,uEAAuE;IACvE,YAAY,IAAI;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAEnC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,YAAY,CAAC,IAAI,EAAE,GAAG,GAAG,iBAAiB,GAAG,SAAS,CAAC;CACxD;AA+ED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,cAAc,GAAG,SAAS,EACjC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,GAChD,cAAc,CA8DhB"}
@@ -0,0 +1,132 @@
1
+ /** Column functions (need a numeric child column); `count` needs none. */
2
+ const COLUMN_FNS = ['sum', 'avg', 'min', 'max'];
3
+ /** Double-quote one identifier segment for Postgres, escaping embedded quotes. */
4
+ function q(ident) {
5
+ return `"${ident.replace(/"/g, '""')}"`;
6
+ }
7
+ /**
8
+ * The scalar aggregate expression over an (already-quoted) child column. Empty
9
+ * collection semantics: `sum` coalesces to 0; `avg`/`min`/`max` stay NULL (their
10
+ * natural result over zero rows); `count` is naturally 0.
11
+ */
12
+ function aggregateExpr(fn, quotedColumn) {
13
+ switch (fn) {
14
+ case 'count':
15
+ return 'COUNT(*)';
16
+ case 'sum':
17
+ return `COALESCE(SUM(${quotedColumn}),0)`;
18
+ case 'avg':
19
+ return `AVG(${quotedColumn})`;
20
+ case 'min':
21
+ return `MIN(${quotedColumn})`;
22
+ case 'max':
23
+ return `MAX(${quotedColumn})`;
24
+ }
25
+ }
26
+ /**
27
+ * ONE_TO_MANY correlation: a direct FK on the child row points back at the
28
+ * root's PK. `column` omitted → `$count`.
29
+ *
30
+ * (SELECT <agg> FROM "child" WHERE "child"."fk" = "<alias>"."pk")
31
+ */
32
+ function oneToManySource(fn, childTable, fk, pk, column) {
33
+ const agg = aggregateExpr(fn, column ? `${q(childTable)}.${q(column)}` : '');
34
+ return ({ alias }) => `(SELECT ${agg} FROM ${q(childTable)} WHERE ${q(childTable)}.${q(fk)} = ${q(alias)}.${q(pk)})`;
35
+ }
36
+ /**
37
+ * MANY_TO_MANY correlation, mediated by a pivot table. `$count` counts pivot
38
+ * rows directly; the column functions JOIN pivot → child INSIDE the scalar
39
+ * subquery to reach the child column.
40
+ *
41
+ * $count: (SELECT COUNT(*) FROM "pivot" WHERE "pivot"."ownerFk" = "<alias>"."ownerPk")
42
+ * $agg: (SELECT <agg> FROM "child"
43
+ * JOIN "pivot" ON "child"."childPk" = "pivot"."inverseFk"
44
+ * WHERE "pivot"."ownerFk" = "<alias>"."ownerPk")
45
+ */
46
+ function manyToManySource(fn, meta, column) {
47
+ if (fn === 'count') {
48
+ return ({ alias }) => `(SELECT COUNT(*) FROM ${q(meta.pivot)} WHERE ${q(meta.pivot)}.${q(meta.ownerFk)} = ${q(alias)}.${q(meta.ownerPk)})`;
49
+ }
50
+ const agg = aggregateExpr(fn, column ? `${q(meta.childTable)}.${q(column)}` : '');
51
+ return ({ alias }) => `(SELECT ${agg} FROM ${q(meta.childTable)} JOIN ${q(meta.pivot)} ON ${q(meta.childTable)}.${q(meta.childPk)} = ${q(meta.pivot)}.${q(meta.inverseFk)} WHERE ${q(meta.pivot)}.${q(meta.ownerFk)} = ${q(alias)}.${q(meta.ownerPk)})`;
52
+ }
53
+ /**
54
+ * Discover to-many aggregate {@link ComputedSource}s from a Lucid model's
55
+ * relation metadata, for the relations the spec whitelisted. For each declared
56
+ * to-many relation (hasMany / manyToMany) this synthesises:
57
+ *
58
+ * - `<rel>.$count` — always;
59
+ * - `<rel>.$sum|$avg|$min|$max.<col>` — one per column the relation's
60
+ * {@link RelationSpec.aggregates} declares (Lucid does not reflect SQL column
61
+ * types, so the dev asserts numeric-ness by listing the column there).
62
+ *
63
+ * To-one relations (belongsTo / hasOne) are excluded. The whole thing degrades
64
+ * gracefully: a relation that is not on the model, is to-one, or cannot be
65
+ * booted (e.g. no DB adapter yet) is simply skipped, and a model without the
66
+ * introspection capability yields an empty map — so aggregates activate only
67
+ * when the metadata is actually available.
68
+ *
69
+ * The result is merged into the spec's `computed` map, so aggregate fields ARE
70
+ * computed fields with auto-generated correlated-subquery sources — they reuse
71
+ * the exact same runner routing, allow-list bypass (their discovery IS their
72
+ * allow-list, gated by the relation whitelist), injection-safe value binding,
73
+ * and codegen surfacing.
74
+ */
75
+ export function discoverAggregateSources(model, relations) {
76
+ if (!model || typeof model.$getRelation !== 'function')
77
+ return {};
78
+ const out = {};
79
+ for (const [relName, relSpec] of Object.entries(relations)) {
80
+ let rel;
81
+ try {
82
+ rel = model.$getRelation(relName);
83
+ if (!rel)
84
+ continue;
85
+ rel.boot();
86
+ }
87
+ catch {
88
+ continue; // Un-bootable relation (no adapter yet, etc.) → skip its aggregates.
89
+ }
90
+ const columns = relSpec.aggregates ?? [];
91
+ const childTable = rel.relatedModel?.().table;
92
+ if (rel.type === 'hasMany') {
93
+ const fk = rel.foreignKeyColumnName;
94
+ const pk = rel.localKeyColumnName;
95
+ if (!childTable || !fk || !pk)
96
+ continue;
97
+ out[`${relName}.$count`] = oneToManySource('count', childTable, fk, pk, undefined);
98
+ for (const col of columns) {
99
+ for (const fn of COLUMN_FNS) {
100
+ out[`${relName}.$${fn}.${col}`] = oneToManySource(fn, childTable, fk, pk, col);
101
+ }
102
+ }
103
+ }
104
+ else if (rel.type === 'manyToMany') {
105
+ const pivot = rel.pivotTable;
106
+ const ownerFk = rel.pivotForeignKey;
107
+ const inverseFk = rel.pivotRelatedForeignKey;
108
+ const ownerPk = rel.localKeyColumnName;
109
+ const childPk = rel.relatedKeyColumnName;
110
+ if (!pivot || !ownerFk || !ownerPk)
111
+ continue;
112
+ out[`${relName}.$count`] = manyToManySource('count', {
113
+ childTable: childTable ?? '',
114
+ childPk: childPk ?? '',
115
+ pivot,
116
+ ownerFk,
117
+ inverseFk: inverseFk ?? '',
118
+ ownerPk,
119
+ }, undefined);
120
+ if (childTable && inverseFk && childPk) {
121
+ for (const col of columns) {
122
+ for (const fn of COLUMN_FNS) {
123
+ out[`${relName}.$${fn}.${col}`] = manyToManySource(fn, { childTable, childPk, pivot, ownerFk, inverseFk, ownerPk }, col);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ // to-one (belongsTo / hasOne) and any other kind → no aggregate keys.
129
+ }
130
+ return out;
131
+ }
132
+ //# sourceMappingURL=aggregate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregate.js","sourceRoot":"","sources":["../../src/aggregate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Pure, ORM-agnostic parser for to-many aggregate field paths — a verbatim port
3
+ * of the NestJS `nestjs-filter` `aggregate/aggregate-path.ts` (commit 3f21e1f).
4
+ * It only parses the client-facing field *string* into a structured
5
+ * {@link AggregatePath}; turning that into SQL (a correlated subquery) is the
6
+ * Lucid-specific compiler's job (see `filter_spec.ts` aggregate discovery).
7
+ */
8
+ export type AggregateFn = 'count' | 'sum' | 'avg' | 'min' | 'max';
9
+ export interface AggregatePath {
10
+ relation: string;
11
+ fn: AggregateFn;
12
+ column?: string;
13
+ }
14
+ /**
15
+ * Parses a to-many aggregate field path.
16
+ *
17
+ * Grammar (single relation hop only):
18
+ * - `<relation>.$count` → { relation, fn: 'count' }
19
+ * - `<relation>.$<fn>.<childColumn>` → { relation, fn, column } for fn in sum|avg|min|max
20
+ *
21
+ * Returns `null` for anything else so callers can fall through to
22
+ * non-aggregate field handling instead of throwing.
23
+ */
24
+ export declare function parseAggregatePath(path: string): AggregatePath | null;
25
+ //# sourceMappingURL=aggregate_path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregate_path.d.ts","sourceRoot":"","sources":["../../src/aggregate_path.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAClE,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,WAAW,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAID;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAWrE"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Pure, ORM-agnostic parser for to-many aggregate field paths — a verbatim port
3
+ * of the NestJS `nestjs-filter` `aggregate/aggregate-path.ts` (commit 3f21e1f).
4
+ * It only parses the client-facing field *string* into a structured
5
+ * {@link AggregatePath}; turning that into SQL (a correlated subquery) is the
6
+ * Lucid-specific compiler's job (see `filter_spec.ts` aggregate discovery).
7
+ */
8
+ const COLUMN_FNS = new Set(['sum', 'avg', 'min', 'max']);
9
+ /**
10
+ * Parses a to-many aggregate field path.
11
+ *
12
+ * Grammar (single relation hop only):
13
+ * - `<relation>.$count` → { relation, fn: 'count' }
14
+ * - `<relation>.$<fn>.<childColumn>` → { relation, fn, column } for fn in sum|avg|min|max
15
+ *
16
+ * Returns `null` for anything else so callers can fall through to
17
+ * non-aggregate field handling instead of throwing.
18
+ */
19
+ export function parseAggregatePath(path) {
20
+ const parts = path.split('.');
21
+ // <rel>.$<fn> → 2 parts, $count
22
+ // <rel>.$<fn>.<col> → 3 parts, column fns
23
+ if (parts.length < 2 || parts.length > 3)
24
+ return null;
25
+ const [relation, token, column] = parts;
26
+ if (!relation || !token || !token.startsWith('$'))
27
+ return null;
28
+ const fn = token.slice(1);
29
+ if (fn === 'count')
30
+ return parts.length === 2 ? { relation, fn } : null;
31
+ if (COLUMN_FNS.has(fn))
32
+ return parts.length === 3 && column ? { relation, fn, column } : null;
33
+ return null;
34
+ }
35
+ //# sourceMappingURL=aggregate_path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregate_path.js","sourceRoot":"","sources":["../../src/aggregate_path.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,MAAM,UAAU,GAAG,IAAI,GAAG,CAAc,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;AAEtE;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9B,2CAA2C;IAC3C,+CAA+C;IAC/C,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACtD,MAAM,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,KAAK,CAAC;IACxC,IAAI,CAAC,QAAQ,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/D,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAgB,CAAC;IACzC,IAAI,EAAE,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACxE,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAAE,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9F,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -25,4 +25,6 @@ export declare function resolveFieldAlias(aliases: FieldAliases | undefined, fie
25
25
  export declare function remapFilterAliases(filter: ColumnFilter, aliases: FieldAliases): ColumnFilter;
26
26
  /** Resolves every sort directive's field through the alias map (returns a new array). */
27
27
  export declare function remapSortAliases(sorts: SortItem[], aliases: FieldAliases): SortItem[];
28
+ /** Resolves every distinct field name through the alias map (returns a new array). */
29
+ export declare function remapDistinctAliases(fields: string[], aliases: FieldAliases): string[];
28
30
  //# sourceMappingURL=field_aliases.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"field_aliases.d.ts","sourceRoot":"","sources":["../../src/field_aliases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAkB3C,wFAAwF;AACxF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAElD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK1F;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,GAAG,YAAY,CAS5F;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,YAAY,GAAG,QAAQ,EAAE,CAKrF"}
1
+ {"version":3,"file":"field_aliases.d.ts","sourceRoot":"","sources":["../../src/field_aliases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAkB3C,wFAAwF;AACxF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAElD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK1F;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,GAAG,YAAY,CAS5F;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,YAAY,GAAG,QAAQ,EAAE,CAKrF;AAED,sFAAsF;AACtF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,EAAE,CAEtF"}
@@ -58,4 +58,8 @@ export function remapSortAliases(sorts, aliases) {
58
58
  direction: sort.direction,
59
59
  }));
60
60
  }
61
+ /** Resolves every distinct field name through the alias map (returns a new array). */
62
+ export function remapDistinctAliases(fields, aliases) {
63
+ return fields.map((field) => resolveFieldAlias(aliases, field));
64
+ }
61
65
  //# sourceMappingURL=field_aliases.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"field_aliases.js","sourceRoot":"","sources":["../../src/field_aliases.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC;IACjC,WAAW;IACX,aAAa;IACb,WAAW;IACX,UAAU;IACV,SAAS;CACV,CAAC,CAAC;AAKH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAiC,EAAE,KAAa;IAChF,IAAI,CAAC,OAAO,IAAI,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC;QAChF,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC;AACjC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAoB,EAAE,OAAqB;IAC5E,MAAM,IAAI,GAAiB;QACzB,KAAK,EAAE,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC;QAC/C,QAAQ,EAAE,MAAM,CAAC,QAAQ;KAC1B,CAAC;IACF,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;QAAE,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC1D,IAAI,MAAM,CAAC,GAAG;QAAE,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IACjF,IAAI,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IAC9E,OAAO,IAAI,CAAC;AACd,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,gBAAgB,CAAC,KAAiB,EAAE,OAAqB;IACvE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC1B,KAAK,EAAE,iBAAiB,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC;QAC7C,SAAS,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAC,CAAC,CAAC;AACN,CAAC"}
1
+ {"version":3,"file":"field_aliases.js","sourceRoot":"","sources":["../../src/field_aliases.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC;IACjC,WAAW;IACX,aAAa;IACb,WAAW;IACX,UAAU;IACV,SAAS;CACV,CAAC,CAAC;AAKH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAiC,EAAE,KAAa;IAChF,IAAI,CAAC,OAAO,IAAI,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC;QAChF,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC;AACjC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAoB,EAAE,OAAqB;IAC5E,MAAM,IAAI,GAAiB;QACzB,KAAK,EAAE,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC;QAC/C,QAAQ,EAAE,MAAM,CAAC,QAAQ;KAC1B,CAAC;IACF,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;QAAE,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC1D,IAAI,MAAM,CAAC,GAAG;QAAE,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IACjF,IAAI,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IAC9E,OAAO,IAAI,CAAC;AACd,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,gBAAgB,CAAC,KAAiB,EAAE,OAAqB;IACvE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC1B,KAAK,EAAE,iBAAiB,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC;QAC7C,SAAS,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAC,CAAC,CAAC;AACN,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,oBAAoB,CAAC,MAAgB,EAAE,OAAqB;IAC1E,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;AAClE,CAAC"}
@@ -1,7 +1,8 @@
1
+ import { type LucidModelLike } from './aggregate.js';
1
2
  import type { FieldAliases } from './field_aliases.js';
2
3
  import type { FilterFieldTypeInfo } from './generate_client.js';
3
4
  import type { ColumnFilter } from './operators.js';
4
- import type { FilterFieldKind } from './types.js';
5
+ import type { ComputedFields, FilterFieldKind } from './types.js';
5
6
  import type { FilterConfig, FullTextSearchConfig, SortItem, VectorSimilarityConfig } from './types.js';
6
7
  /**
7
8
  * Thrown when a {@link defineFilter} declaration is itself invalid (a developer
@@ -42,6 +43,18 @@ export interface RelationSpec {
42
43
  sortable?: RelationColumns;
43
44
  /** Further whitelisted relations reachable from this one (one hop deeper). */
44
45
  relations?: Record<string, RelationSpec>;
46
+ /**
47
+ * Numeric child columns exposed to the to-many aggregate functions
48
+ * (`$sum`/`$avg`/`$min`/`$max`) — e.g. `['views', 'total']` makes
49
+ * `posts.$sum.views` and `posts.$max.total` filterable/sortable. `$count`
50
+ * needs no column and is synthesised for every to-many relation regardless.
51
+ *
52
+ * Because Lucid does not reflect a column's SQL type, listing a column here is
53
+ * how the developer asserts it is numeric (the aggregate allow-list). Only
54
+ * meaningful when the owning {@link DefineFilterOptions.model} is set so the
55
+ * relation's FK/pivot metadata can be discovered; ignored otherwise.
56
+ */
57
+ aggregates?: string[];
45
58
  }
46
59
  /**
47
60
  * Reads the current tenant id from the request context (the AdonisJS
@@ -103,6 +116,17 @@ export interface DefineFilterOptions {
103
116
  fullText?: FullTextSearchConfig;
104
117
  /** Whitelisted relations and their nested filterable/sortable columns. */
105
118
  relations?: Record<string, RelationSpec>;
119
+ /**
120
+ * The owning Lucid model — enables to-many aggregate fields
121
+ * (`$count`/`$sum`/`$avg`/`$min`/`$max`). Its relation metadata (hasMany /
122
+ * manyToMany FK + pivot columns) is introspected at build time to synthesise
123
+ * aggregate computed sources for the whitelisted {@link relations}. Optional;
124
+ * without it, aggregate fields are simply not available (the feature degrades
125
+ * gracefully) — every other feature works with or without it. Also supplies
126
+ * the default {@link table} (the model's table name) used as the correlated
127
+ * subquery's outer alias.
128
+ */
129
+ model?: LucidModelLike;
106
130
  /**
107
131
  * Maximum relation-path depth (number of relation hops; a base column is
108
132
  * depth 0, `posts.title` is depth 1, `posts.comments.body` is depth 2).
@@ -119,6 +143,23 @@ export interface DefineFilterOptions {
119
143
  * is unchanged.
120
144
  */
121
145
  vectorSimilarity?: VectorSimilarityConfig;
146
+ /**
147
+ * Virtual/computed fields — alias → dev-declared SQL expression
148
+ * ({@link ComputedFields}). Two source forms: a verbatim string
149
+ * (`{ fullName: "first || ' ' || last" }`) or a function
150
+ * (`{ postCount: ({ alias }) => \`(SELECT COUNT(*) FROM posts WHERE posts.author_id = ${alias}.id)\` }`)
151
+ * for correlated subqueries. A declared alias becomes filterable and sortable
152
+ * as if it were a real column; the client value stays parameterized and only
153
+ * the dev expression is inlined. Function-form sources need {@link table}.
154
+ */
155
+ computed?: ComputedFields;
156
+ /**
157
+ * The root model's table name, surfaced to computed-field functions as the
158
+ * outer alias (Lucid gives the main table no generated alias, so the table
159
+ * name IS the alias). Required for function-form computed fields and to-many
160
+ * aggregate fields whose correlated subqueries reference the outer row.
161
+ */
162
+ table?: string;
122
163
  /** Opt-in tenant auto-scope read from ctx. */
123
164
  tenant?: TenantScopeSpec;
124
165
  /**
@@ -153,6 +194,10 @@ export interface FilterSpec {
153
194
  readonly maxDepth: number;
154
195
  readonly aliases: FieldAliases | undefined;
155
196
  readonly vectorSimilarity: VectorSimilarityConfig | undefined;
197
+ /** Declared computed/virtual fields (alias → SQL source). */
198
+ readonly computed: ComputedFields | undefined;
199
+ /** Root table name surfaced to computed-field functions as the outer alias. */
200
+ readonly table: string | undefined;
156
201
  readonly tenant: TenantScopeSpec | undefined;
157
202
  readonly defaultFilters: readonly ColumnFilter[];
158
203
  readonly defaultSort: readonly SortItem[];
@@ -1 +1 @@
1
- {"version":3,"file":"filter_spec.d.ts","sourceRoot":"","sources":["../../src/filter_spec.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"filter_spec.d.ts","sourceRoot":"","sources":["../../src/filter_spec.ts"],"names":[],"mappings":"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"}