@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.
- package/CHANGELOG.md +136 -0
- package/README.md +1 -1
- package/dist/configure.d.ts +15 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +20 -0
- package/dist/configure.js.map +1 -0
- package/dist/src/aggregate.d.ts +61 -0
- package/dist/src/aggregate.d.ts.map +1 -0
- package/dist/src/aggregate.js +132 -0
- package/dist/src/aggregate.js.map +1 -0
- package/dist/src/aggregate_path.d.ts +25 -0
- package/dist/src/aggregate_path.d.ts.map +1 -0
- package/dist/src/aggregate_path.js +35 -0
- package/dist/src/aggregate_path.js.map +1 -0
- package/dist/src/field_aliases.d.ts +2 -0
- package/dist/src/field_aliases.d.ts.map +1 -1
- package/dist/src/field_aliases.js +4 -0
- package/dist/src/field_aliases.js.map +1 -1
- package/dist/src/filter_spec.d.ts +46 -1
- package/dist/src/filter_spec.d.ts.map +1 -1
- package/dist/src/filter_spec.js +16 -0
- package/dist/src/filter_spec.js.map +1 -1
- package/dist/src/generate_client.d.ts.map +1 -1
- package/dist/src/generate_client.js +5 -0
- package/dist/src/generate_client.js.map +1 -1
- package/dist/src/index.d.ts +9 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/lucid_adapter.d.ts +45 -1
- package/dist/src/lucid_adapter.d.ts.map +1 -1
- package/dist/src/lucid_adapter.js +123 -0
- package/dist/src/lucid_adapter.js.map +1 -1
- package/dist/src/parse_request.d.ts +21 -1
- package/dist/src/parse_request.d.ts.map +1 -1
- package/dist/src/parse_request.js +90 -4
- package/dist/src/parse_request.js.map +1 -1
- package/dist/src/runner.d.ts.map +1 -1
- package/dist/src/runner.js +144 -17
- package/dist/src/runner.js.map +1 -1
- package/dist/src/spatie_parser.d.ts +1 -0
- package/dist/src/spatie_parser.d.ts.map +1 -1
- package/dist/src/spatie_parser.js +5 -1
- package/dist/src/spatie_parser.js.map +1 -1
- package/dist/src/testing.d.ts +2 -0
- package/dist/src/testing.d.ts.map +1 -1
- package/dist/src/testing.js +4 -0
- package/dist/src/testing.js.map +1 -1
- package/dist/src/types.d.ts +51 -0
- package/dist/src/types.d.ts.map +1 -1
- 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":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"filter_spec.d.ts","sourceRoot":"","sources":["../../src/filter_spec.ts"],"names":[],"mappings":"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"}
|