@adonis-agora/filter 0.0.0-beta-20261110131309
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 +417 -0
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/commands/main.d.ts +19 -0
- package/dist/commands/main.d.ts.map +1 -0
- package/dist/commands/main.js +19 -0
- package/dist/commands/main.js.map +1 -0
- package/dist/commands/make_filter.d.ts +18 -0
- package/dist/commands/make_filter.d.ts.map +1 -0
- package/dist/commands/make_filter.js +32 -0
- package/dist/commands/make_filter.js.map +1 -0
- package/dist/commands/make_filter_client.d.ts +35 -0
- package/dist/commands/make_filter_client.d.ts.map +1 -0
- package/dist/commands/make_filter_client.js +85 -0
- package/dist/commands/make_filter_client.js.map +1 -0
- 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/providers/filter_provider.d.ts +28 -0
- package/dist/providers/filter_provider.d.ts.map +1 -0
- package/dist/providers/filter_provider.js +33 -0
- package/dist/providers/filter_provider.js.map +1 -0
- package/dist/src/aggregate.d.ts +70 -0
- package/dist/src/aggregate.d.ts.map +1 -0
- package/dist/src/aggregate.js +195 -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/apply_from_request.d.ts +85 -0
- package/dist/src/apply_from_request.d.ts.map +1 -0
- package/dist/src/apply_from_request.js +164 -0
- package/dist/src/apply_from_request.js.map +1 -0
- package/dist/src/base_filter.d.ts +49 -0
- package/dist/src/base_filter.d.ts.map +1 -0
- package/dist/src/base_filter.js +33 -0
- package/dist/src/base_filter.js.map +1 -0
- package/dist/src/base_model_filter.d.ts +90 -0
- package/dist/src/base_model_filter.d.ts.map +1 -0
- package/dist/src/base_model_filter.js +85 -0
- package/dist/src/base_model_filter.js.map +1 -0
- package/dist/src/coerce_value.d.ts +26 -0
- package/dist/src/coerce_value.d.ts.map +1 -0
- package/dist/src/coerce_value.js +82 -0
- package/dist/src/coerce_value.js.map +1 -0
- package/dist/src/cursor.d.ts +92 -0
- package/dist/src/cursor.d.ts.map +1 -0
- package/dist/src/cursor.js +107 -0
- package/dist/src/cursor.js.map +1 -0
- package/dist/src/custom_filter.d.ts +36 -0
- package/dist/src/custom_filter.d.ts.map +1 -0
- package/dist/src/custom_filter.js +127 -0
- package/dist/src/custom_filter.js.map +1 -0
- package/dist/src/decorator_metadata.d.ts +41 -0
- package/dist/src/decorator_metadata.d.ts.map +1 -0
- package/dist/src/decorator_metadata.js +83 -0
- package/dist/src/decorator_metadata.js.map +1 -0
- package/dist/src/decorators.d.ts +73 -0
- package/dist/src/decorators.d.ts.map +1 -0
- package/dist/src/decorators.js +118 -0
- package/dist/src/decorators.js.map +1 -0
- package/dist/src/escape-like.d.ts +10 -0
- package/dist/src/escape-like.d.ts.map +1 -0
- package/dist/src/escape-like.js +15 -0
- package/dist/src/escape-like.js.map +1 -0
- package/dist/src/field_aliases.d.ts +30 -0
- package/dist/src/field_aliases.d.ts.map +1 -0
- package/dist/src/field_aliases.js +65 -0
- package/dist/src/field_aliases.js.map +1 -0
- package/dist/src/filter_class.d.ts +46 -0
- package/dist/src/filter_class.d.ts.map +1 -0
- package/dist/src/filter_class.js +244 -0
- package/dist/src/filter_class.js.map +1 -0
- package/dist/src/filter_spec.d.ts +231 -0
- package/dist/src/filter_spec.d.ts.map +1 -0
- package/dist/src/filter_spec.js +182 -0
- package/dist/src/filter_spec.js.map +1 -0
- package/dist/src/filterable_mixin.d.ts +49 -0
- package/dist/src/filterable_mixin.d.ts.map +1 -0
- package/dist/src/filterable_mixin.js +47 -0
- package/dist/src/filterable_mixin.js.map +1 -0
- package/dist/src/generate_client.d.ts +107 -0
- package/dist/src/generate_client.d.ts.map +1 -0
- package/dist/src/generate_client.js +164 -0
- package/dist/src/generate_client.js.map +1 -0
- package/dist/src/group_by_count.d.ts +67 -0
- package/dist/src/group_by_count.d.ts.map +1 -0
- package/dist/src/group_by_count.js +71 -0
- package/dist/src/group_by_count.js.map +1 -0
- package/dist/src/in_memory.d.ts +279 -0
- package/dist/src/in_memory.d.ts.map +1 -0
- package/dist/src/in_memory.js +810 -0
- package/dist/src/in_memory.js.map +1 -0
- package/dist/src/index.d.ts +51 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +33 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lucid_adapter.d.ts +311 -0
- package/dist/src/lucid_adapter.d.ts.map +1 -0
- package/dist/src/lucid_adapter.js +469 -0
- package/dist/src/lucid_adapter.js.map +1 -0
- package/dist/src/lucid_macros.d.ts +87 -0
- package/dist/src/lucid_macros.d.ts.map +1 -0
- package/dist/src/lucid_macros.js +67 -0
- package/dist/src/lucid_macros.js.map +1 -0
- package/dist/src/normalizer.d.ts +21 -0
- package/dist/src/normalizer.d.ts.map +1 -0
- package/dist/src/normalizer.js +61 -0
- package/dist/src/normalizer.js.map +1 -0
- package/dist/src/operators.d.ts +37 -0
- package/dist/src/operators.d.ts.map +1 -0
- package/dist/src/operators.js +43 -0
- package/dist/src/operators.js.map +1 -0
- package/dist/src/paginate.d.ts +12 -0
- package/dist/src/paginate.d.ts.map +1 -0
- package/dist/src/paginate.js +88 -0
- package/dist/src/paginate.js.map +1 -0
- package/dist/src/pagination_stash.d.ts +4 -0
- package/dist/src/pagination_stash.d.ts.map +1 -0
- package/dist/src/pagination_stash.js +19 -0
- package/dist/src/pagination_stash.js.map +1 -0
- package/dist/src/parse_request.d.ts +50 -0
- package/dist/src/parse_request.d.ts.map +1 -0
- package/dist/src/parse_request.js +220 -0
- package/dist/src/parse_request.js.map +1 -0
- package/dist/src/runner.d.ts +65 -0
- package/dist/src/runner.d.ts.map +1 -0
- package/dist/src/runner.js +371 -0
- package/dist/src/runner.js.map +1 -0
- package/dist/src/source_resolver.d.ts +18 -0
- package/dist/src/source_resolver.d.ts.map +1 -0
- package/dist/src/source_resolver.js +64 -0
- package/dist/src/source_resolver.js.map +1 -0
- package/dist/src/spatie_parser.d.ts +39 -0
- package/dist/src/spatie_parser.d.ts.map +1 -0
- package/dist/src/spatie_parser.js +134 -0
- package/dist/src/spatie_parser.js.map +1 -0
- package/dist/src/testing.d.ts +73 -0
- package/dist/src/testing.d.ts.map +1 -0
- package/dist/src/testing.js +136 -0
- package/dist/src/testing.js.map +1 -0
- package/dist/src/types.d.ts +226 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/validate-column-filter.d.ts +30 -0
- package/dist/src/validate-column-filter.d.ts.map +1 -0
- package/dist/src/validate-column-filter.js +148 -0
- package/dist/src/validate-column-filter.js.map +1 -0
- package/dist/src/value-shape.d.ts +18 -0
- package/dist/src/value-shape.d.ts.map +1 -0
- package/dist/src/value-shape.js +36 -0
- package/dist/src/value-shape.js.map +1 -0
- package/dist/stubs/main.d.ts +3 -0
- package/dist/stubs/main.d.ts.map +1 -0
- package/dist/stubs/main.js +4 -0
- package/dist/stubs/main.js.map +1 -0
- package/dist/stubs/make/filter/main.stub +45 -0
- package/package.json +102 -0
- package/skills/filter-basics/SKILL.md +285 -0
- package/skills/filter-codegen/SKILL.md +246 -0
- package/skills/filter-definitions/SKILL.md +340 -0
- package/skills/filter-querying/SKILL.md +319 -0
- package/skills/filter-safety/SKILL.md +304 -0
- package/stubs/main.ts +4 -0
- package/stubs/make/filter/main.stub +45 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: filter-codegen
|
|
3
|
+
description: >
|
|
4
|
+
Generate a typed @adonis-agora/filter-client builder from a defineFilter spec
|
|
5
|
+
in @adonis-agora/filter — generateFilterClient(spec, options) pure string
|
|
6
|
+
transform, generateFilterClients(manifest), the make:filter-client ace command
|
|
7
|
+
with a FilterClientManifest in config/filter.ts, emitted artifacts
|
|
8
|
+
(<Name>FilterFields union, <Name>FilterFieldTypes interface,
|
|
9
|
+
<Name>FilterFieldMeta, <Name>FilterQuery factory), fieldTypes operator
|
|
10
|
+
narrowing (kind | enumValues | typeRef | nullable), maxDepth/banner/clientModule
|
|
11
|
+
options, filterableFieldPaths/sortableFieldPaths enumerators, and adonisrc.ts
|
|
12
|
+
commands-barrel registration. Use when sharing one field allow-list between
|
|
13
|
+
browser and server, regenerating clients after spec changes, or emitting
|
|
14
|
+
enum-aware typed builders.
|
|
15
|
+
metadata:
|
|
16
|
+
type: core
|
|
17
|
+
library: "@adonis-agora/filter"
|
|
18
|
+
library_version: "0.7.0"
|
|
19
|
+
framework: adonisjs
|
|
20
|
+
sources:
|
|
21
|
+
- "DavideCarvalho/adonis-filter:docs/codegen/index.mdx"
|
|
22
|
+
- "DavideCarvalho/adonis-filter:docs/definitions/index.mdx"
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Client codegen: one spec, both ends of the wire
|
|
26
|
+
|
|
27
|
+
A `defineFilter` spec already knows which fields are filterable. Codegen turns
|
|
28
|
+
that spec into a typed front-end module — a `filterQueryTyped<Fields, FieldTypes>()`
|
|
29
|
+
builder scoped to the spec's fields — so browser and server share one
|
|
30
|
+
field-name allow-list, checked at compile time. Generation is a pure function
|
|
31
|
+
of the spec: string in, string out, no AST walk, no reflection.
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
Register the command barrel, declare a manifest, run the command:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// adonisrc.ts
|
|
39
|
+
{
|
|
40
|
+
commands: [
|
|
41
|
+
// ...
|
|
42
|
+
() => import('@adonis-agora/filter/commands'),
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// config/filter.ts
|
|
49
|
+
import { defineFilter } from '@adonis-agora/filter'
|
|
50
|
+
import type { FilterClientManifest } from '@adonis-agora/filter'
|
|
51
|
+
|
|
52
|
+
export const filters: FilterClientManifest = {
|
|
53
|
+
people: {
|
|
54
|
+
spec: defineFilter({
|
|
55
|
+
filterable: ['age', 'name', 'status'],
|
|
56
|
+
sortable: ['age', 'name'],
|
|
57
|
+
searchable: ['name'],
|
|
58
|
+
relations: { posts: { filterable: ['title', 'published'] } },
|
|
59
|
+
defaultSort: [{ field: 'name', direction: 'asc' }],
|
|
60
|
+
defaultSize: 25,
|
|
61
|
+
maxSize: 100,
|
|
62
|
+
}),
|
|
63
|
+
fieldTypes: { age: { kind: 'number' }, status: { enumValues: ['active', 'inactive'] } },
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
node ace make:filter-client
|
|
70
|
+
# create app/generated/filters/people_filter_client.ts
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Defaults: entrypoint `config/filter.js`, output `app/generated/filters`. Pass
|
|
74
|
+
an explicit entrypoint/dir with
|
|
75
|
+
`node ace make:filter-client config/filter.js --output app/generated/filters`.
|
|
76
|
+
|
|
77
|
+
## Core patterns
|
|
78
|
+
|
|
79
|
+
### 1. The pure core — generateFilterClient
|
|
80
|
+
|
|
81
|
+
Skip the command entirely when you want to own the IO:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { defineFilter, generateFilterClient } from '@adonis-agora/filter'
|
|
85
|
+
|
|
86
|
+
const spec = defineFilter({
|
|
87
|
+
filterable: ['age', 'name', 'status'],
|
|
88
|
+
sortable: ['age', 'name'],
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const code = generateFilterClient(spec, {
|
|
92
|
+
name: 'people',
|
|
93
|
+
fieldTypes: { age: { kind: 'number' }, status: { enumValues: ['active', 'inactive'] } },
|
|
94
|
+
})
|
|
95
|
+
// code is a TypeScript module string — write it wherever you like
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For `name: 'people'` the module exports: `type PeopleFilterFields` (union of
|
|
99
|
+
filterable paths — base plus relation-dotted), `interface PeopleFilterFieldTypes`
|
|
100
|
+
(only when `fieldTypes` supplied), `const peopleFilterMeta` (runtime metadata),
|
|
101
|
+
and `function peopleFilterQuery()` (a `filterQueryTyped` factory).
|
|
102
|
+
|
|
103
|
+
Options: `clientModule` (import specifier, default `@adonis-agora/filter-client`),
|
|
104
|
+
`maxDepth` (relation-path cap, defaults to `spec.maxDepth`), `banner`
|
|
105
|
+
(the DO NOT EDIT header, default `true`).
|
|
106
|
+
|
|
107
|
+
Source: `docs/codegen/index.mdx`
|
|
108
|
+
|
|
109
|
+
### 2. Consume the generated client
|
|
110
|
+
|
|
111
|
+
Import the generated factory and get field-name-checked, operator-narrowed
|
|
112
|
+
queries:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// web/people-table.ts
|
|
116
|
+
import { peopleFilterQuery } from '#generated/filters/people_filter_client'
|
|
117
|
+
|
|
118
|
+
const qs = peopleFilterQuery()
|
|
119
|
+
.contains('name', 'Al') // 'name' checked against PeopleFilterFields
|
|
120
|
+
.gte('age', 18) // number ops via fieldTypes narrowing
|
|
121
|
+
.toQueryString()
|
|
122
|
+
|
|
123
|
+
await fetch(`/people?${qs}`)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Source: `docs/codegen/index.mdx`
|
|
127
|
+
|
|
128
|
+
### 3. fieldTypes drives operator narrowing
|
|
129
|
+
|
|
130
|
+
The spec carries the allow-list but not column value types (Lucid models are
|
|
131
|
+
not reflected). Supply kinds per field path to unlock type-aware narrowing:
|
|
132
|
+
|
|
133
|
+
| `FilterFieldTypeInfo` | Emits |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `{ kind: 'string' \| 'number' \| 'boolean' \| 'date' \| 'json' }` | matching TS type |
|
|
136
|
+
| `{ enumValues: ['A', 'B'] }` | union `"A" \| "B"` (wins over `kind`) |
|
|
137
|
+
| `{ typeRef: 'Role' }` | named type verbatim (wins over everything) |
|
|
138
|
+
| `{ nullable: true }` | appends `\| null` |
|
|
139
|
+
|
|
140
|
+
Without `fieldTypes` the client stays field-name-safe but operator-permissive.
|
|
141
|
+
On the server side the same declaration powers value coercion — one
|
|
142
|
+
declaration, both ends.
|
|
143
|
+
|
|
144
|
+
Source: `docs/codegen/index.mdx`, `docs/definitions/index.mdx`
|
|
145
|
+
|
|
146
|
+
### 4. Just enumerate the paths
|
|
147
|
+
|
|
148
|
+
`filterableFieldPaths(spec)` / `sortableFieldPaths(spec)` export the enumerated
|
|
149
|
+
field-path lists if you're building your own artifact:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { filterableFieldPaths } from '@adonis-agora/filter'
|
|
153
|
+
|
|
154
|
+
const paths = filterableFieldPaths(peopleSpec)
|
|
155
|
+
// ['age', 'name', 'status', 'posts.title', 'posts.published']
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Source: `docs/codegen/index.mdx`
|
|
159
|
+
|
|
160
|
+
## Common mistakes
|
|
161
|
+
|
|
162
|
+
### [HIGH] Running make:filter-client with the commands barrel unregistered
|
|
163
|
+
|
|
164
|
+
Wrong:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// adonisrc.ts — no filter commands entry
|
|
168
|
+
{ commands: [] }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Correct:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
{ commands: [() => import('@adonis-agora/filter/commands')] }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Ace only sees commands an app opts into; without the barrel the command fails
|
|
178
|
+
with "command not found", which reads as though it does not exist.
|
|
179
|
+
`node ace add @adonis-agora/filter` adds the entry for you.
|
|
180
|
+
|
|
181
|
+
Source: docs/codegen/index.mdx (Step 1 callout)
|
|
182
|
+
|
|
183
|
+
### [HIGH] Hand-editing generated clients instead of regenerating
|
|
184
|
+
|
|
185
|
+
Wrong:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// renamed a spec field, then edited people_filter_client.ts by hand
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Correct:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
node ace make:filter-client # regenerate after every spec change
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Generated modules carry a DO NOT EDIT banner and are a pure function of the
|
|
198
|
+
spec; a stale client keeps sending fields the server now drops, so filters
|
|
199
|
+
silently stop applying with no error on either end.
|
|
200
|
+
|
|
201
|
+
Source: docs/codegen/index.mdx (banner option, Step 4)
|
|
202
|
+
|
|
203
|
+
### [MEDIUM] Expecting codegen unions from a '*' allow-list
|
|
204
|
+
|
|
205
|
+
Wrong:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
defineFilter({ filterable: ['*'] }) // PeopleFilterFields falls back to string
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Correct:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
defineFilter({ filterable: ['age', 'name', 'status'] }) // enumerable literal union
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
A star list cannot be enumerated, so the emitter degrades to a permissive
|
|
218
|
+
string union — the generated client looks typed but checks nothing.
|
|
219
|
+
|
|
220
|
+
Source: docs/codegen/index.mdx (filterableFieldPaths callout)
|
|
221
|
+
|
|
222
|
+
### [MEDIUM] Skipping fieldTypes then expecting operator narrowing
|
|
223
|
+
|
|
224
|
+
Wrong:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
generateFilterClient(spec, { name: 'people' })
|
|
228
|
+
peopleFilterQuery().gte('name', 5) // compiles — gte gated to orderable fields only
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Correct:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
generateFilterClient(spec, {
|
|
235
|
+
name: 'people',
|
|
236
|
+
fieldTypes: { age: { kind: 'number' }, status: { enumValues: ['active', 'inactive'] } },
|
|
237
|
+
})
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Without a field-type map every field is kind-unknown, so all operators compile
|
|
241
|
+
and mismatches surface only server-side as drops or coercion errors.
|
|
242
|
+
|
|
243
|
+
Source: docs/codegen/index.mdx ("fieldTypes — unlock operator narrowing")
|
|
244
|
+
|
|
245
|
+
See also: `packages/client/skills/filter-query-builder/SKILL.md` (the runtime
|
|
246
|
+
the generated code targets), `../filter-definitions/SKILL.md` (spec authoring).
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: filter-definitions
|
|
3
|
+
description: >
|
|
4
|
+
Declare reusable FilterSpec policies with defineFilter from @adonis-agora/filter.
|
|
5
|
+
Covers the full options map — filterable (array, '*', or colocated kind map),
|
|
6
|
+
sortable/searchable defaults, relations whitelists translating dotted paths
|
|
7
|
+
into Lucid whereHas subqueries with maxDepth caps, aliases remapping client
|
|
8
|
+
names to real targets (one hop, cycle-free), computed virtual columns
|
|
9
|
+
(string vs correlated-subquery function forms, table/model requirement),
|
|
10
|
+
to-many aggregates ($count/$sum/$avg/$min/$max unlocked via model +
|
|
11
|
+
RelationSpec.aggregates), fieldTypes value coercion and the colocated
|
|
12
|
+
filterable map form, tenant scope, defaultFilters/defaultSort/defaultSize/
|
|
13
|
+
maxSize, throwOnInvalid, FilterDefinitionError at wiring time, specToFilterConfig,
|
|
14
|
+
and resolveFieldAlias/remap* helpers. Use when authoring a filter policy,
|
|
15
|
+
exposing relation/computed/aggregate fields, or renaming public field names.
|
|
16
|
+
metadata:
|
|
17
|
+
type: core
|
|
18
|
+
library: "@adonis-agora/filter"
|
|
19
|
+
library_version: "0.7.0"
|
|
20
|
+
framework: adonisjs
|
|
21
|
+
sources:
|
|
22
|
+
- "DavideCarvalho/adonis-filter:docs/definitions/index.mdx"
|
|
23
|
+
- "DavideCarvalho/adonis-filter:docs/definitions/relations.mdx"
|
|
24
|
+
- "DavideCarvalho/adonis-filter:docs/definitions/computed.mdx"
|
|
25
|
+
- "DavideCarvalho/adonis-filter:docs/definitions/aggregates.mdx"
|
|
26
|
+
- "DavideCarvalho/adonis-filter:docs/definitions/aliases.mdx"
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
# Filter definitions: declare the policy once with defineFilter
|
|
30
|
+
|
|
31
|
+
A `FilterSpec` is a plain, framework-free object built once at module scope by
|
|
32
|
+
`defineFilter`. It captures what is filterable/sortable, which relations are
|
|
33
|
+
reachable, tenant scoping, and server-side defaults — then
|
|
34
|
+
`applyFilterFromRequest(query, spec, ctx)` applies it on every request. The same
|
|
35
|
+
object also drives client codegen (`generateFilterClient`).
|
|
36
|
+
|
|
37
|
+
## Setup
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// app/filters/user_filter.ts
|
|
41
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
42
|
+
import { defineFilter } from '@adonis-agora/filter'
|
|
43
|
+
import User from '#models/user'
|
|
44
|
+
|
|
45
|
+
export const userFilter = defineFilter({
|
|
46
|
+
filterable: ['name', 'email', 'age', 'status'],
|
|
47
|
+
sortable: ['name', 'createdAt'],
|
|
48
|
+
searchable: ['name', 'email'],
|
|
49
|
+
|
|
50
|
+
relations: {
|
|
51
|
+
posts: {
|
|
52
|
+
filterable: ['title', 'status'],
|
|
53
|
+
sortable: ['createdAt'],
|
|
54
|
+
relations: { comments: { filterable: ['body'] } }, // posts.comments.body
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
tenant: {
|
|
59
|
+
column: 'tenantId',
|
|
60
|
+
resolve: (ctx) => (ctx as HttpContext).auth.user!.tenantId,
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
defaultFilters: [{ field: 'deletedAt', operator: 'isNull' }],
|
|
64
|
+
defaultSort: [{ field: 'createdAt', direction: 'desc' }],
|
|
65
|
+
defaultSize: 25,
|
|
66
|
+
maxSize: 100,
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Apply it in the controller:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { applyFilterFromRequest } from '@adonis-agora/filter'
|
|
74
|
+
import { userFilter } from '#filters/user_filter'
|
|
75
|
+
import User from '#models/user'
|
|
76
|
+
|
|
77
|
+
export default class UsersController {
|
|
78
|
+
async index(ctx: HttpContext) {
|
|
79
|
+
const query = User.query()
|
|
80
|
+
const { page, size } = applyFilterFromRequest(query, userFilter, ctx)
|
|
81
|
+
return query.paginate(page, size)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`defineFilter` validates the declaration itself: a missing `filterable` or a
|
|
87
|
+
negative/non-integer `maxDepth` throws `FilterDefinitionError` at wiring time —
|
|
88
|
+
distinct from request-time `InvalidColumnFilterError`.
|
|
89
|
+
|
|
90
|
+
## Core patterns
|
|
91
|
+
|
|
92
|
+
### 1. Relations — dotted request paths become whereHas subqueries
|
|
93
|
+
|
|
94
|
+
Declare each relation by its model name; columns stay bare. Every dotted path
|
|
95
|
+
segment but the last is a relation hop, translated into nested
|
|
96
|
+
`whereHas` subqueries — no manual join:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
?filter[posts.title]=Release
|
|
100
|
+
→ whereHas('posts', (sub) => sub.where('title', 'Release'))
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`maxDepth` bounds hops (base column = 0) and defaults to your deepest declared
|
|
104
|
+
nesting; an explicit smaller value caps paths even when deeper ones exist.
|
|
105
|
+
Relation sorting forwards through the sort stage, but ordering across a to-many
|
|
106
|
+
relation may need joins/aggregation — prefer relation filters, and sort by
|
|
107
|
+
aggregate aliases (`withCount`'s `posts_count`) where possible.
|
|
108
|
+
|
|
109
|
+
Source: `docs/definitions/relations.mdx`, `docs/guides/relations.mdx`
|
|
110
|
+
|
|
111
|
+
### 2. Computed fields — virtual columns backed by your SQL
|
|
112
|
+
|
|
113
|
+
String sources inline verbatim; function sources receive `{ alias }` (the root
|
|
114
|
+
table name) for correlated subqueries. A declared alias is filterable and
|
|
115
|
+
sortable like any column, and its *declaration* is its allow-list — never add
|
|
116
|
+
it to `filterable`:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
export const userFilter = defineFilter({
|
|
120
|
+
filterable: ['status'],
|
|
121
|
+
model: User, // supplies table: 'users' — required for the function form
|
|
122
|
+
computed: {
|
|
123
|
+
fullName: "first_name || ' ' || last_name",
|
|
124
|
+
postCount: ({ alias }) =>
|
|
125
|
+
`(SELECT COUNT(*) FROM posts WHERE posts.author_id = ${alias}.id)`,
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Client values always ride as positional bindings; only dev-authored SQL is
|
|
131
|
+
inlined. Keep computed conditions at top level — see mistakes below.
|
|
132
|
+
|
|
133
|
+
Source: `docs/definitions/computed.mdx`
|
|
134
|
+
|
|
135
|
+
### 3. To-many aggregates — `$count` / `$sum` / `$avg` / `$min` / `$max`
|
|
136
|
+
|
|
137
|
+
Aggregates are auto-generated computed fields, off until the spec can
|
|
138
|
+
introspect relations: pass `model` plus a `relations` entry naming the to-many
|
|
139
|
+
relations; column functions additionally require those child columns listed
|
|
140
|
+
under `aggregates` (you assert numericness — Lucid does not reflect SQL types):
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
export const userFilter = defineFilter({
|
|
144
|
+
filterable: ['status'],
|
|
145
|
+
model: User,
|
|
146
|
+
relations: {
|
|
147
|
+
posts: { filterable: ['status'], aggregates: ['views'] },
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
This makes `posts.$count` plus `posts.$sum.views` / `$avg` / `$min` / `$max`
|
|
153
|
+
filterable and sortable. Discovery degrades gracefully — no introspection means
|
|
154
|
+
no aggregates, never a wiring-time throw. Each aggregate is a correlated
|
|
155
|
+
subquery evaluated per row, except an emptiness-only `$count` filter (`gt 0`,
|
|
156
|
+
`gte 1`, `notEquals 0` for non-empty; `equals 0`, `lte 0`, `lt 1` for empty)
|
|
157
|
+
which compiles to `EXISTS` / `NOT EXISTS` instead, returning the same rows
|
|
158
|
+
while stopping at the first related row; index the child FK (or materialise a
|
|
159
|
+
counter) on hot paths.
|
|
160
|
+
|
|
161
|
+
Source: `docs/definitions/aggregates.mdx`
|
|
162
|
+
|
|
163
|
+
### 4. Aliases and field types
|
|
164
|
+
|
|
165
|
+
Aliases remap a public name onto a whitelisted target (base column or relation
|
|
166
|
+
path); resolution runs before allow-listing, exactly one hop:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
export const userFilter = defineFilter({
|
|
170
|
+
filterable: ['status', 'name'],
|
|
171
|
+
relations: { posts: { filterable: ['title'] } },
|
|
172
|
+
aliases: {
|
|
173
|
+
legacyStatus: 'status',
|
|
174
|
+
postTitle: 'posts.title',
|
|
175
|
+
},
|
|
176
|
+
fieldTypes: { age: { kind: 'number' } },
|
|
177
|
+
})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Prefer the colocated map form when several fields have non-string kinds —
|
|
181
|
+
`filterable: { advisorId: 'string', dayOfWeek: 'number', isRecurring: 'boolean' }`
|
|
182
|
+
desugars into the array plus `fieldTypes`. An explicit `fieldTypes` entry still
|
|
183
|
+
wins per field, which is how you add codegen-only richness (`enumValues`,
|
|
184
|
+
`typeRef`).
|
|
185
|
+
|
|
186
|
+
Source: `docs/definitions/aliases.mdx`, `docs/definitions/index.mdx`
|
|
187
|
+
|
|
188
|
+
### 5. Reuse the spec outside HTTP
|
|
189
|
+
|
|
190
|
+
`specToFilterConfig(spec)` projects the spec onto the per-call `FilterConfig`;
|
|
191
|
+
pass pre-parsed input via `options.input` for jobs/tests;
|
|
192
|
+
`resolveFieldAlias` / `remapFilterAliases` / `remapSortAliases` are exported
|
|
193
|
+
for custom pipelines:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { applyFilterFromRequest } from '@adonis-agora/filter'
|
|
197
|
+
|
|
198
|
+
applyFilterFromRequest(query, userFilter, ctx, {
|
|
199
|
+
input: { filters: [{ field: 'status', operator: 'equals', value: 'active' }] },
|
|
200
|
+
})
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Source: `docs/definitions/index.mdx` (Under the hood)
|
|
204
|
+
|
|
205
|
+
## Common mistakes
|
|
206
|
+
|
|
207
|
+
### [HIGH] Declaring aggregate paths without the owning `model`
|
|
208
|
+
|
|
209
|
+
Wrong:
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
defineFilter({
|
|
213
|
+
filterable: ['status'],
|
|
214
|
+
relations: { posts: { filterable: ['title'], aggregates: ['views'] } },
|
|
215
|
+
}) // posts.$count etc. simply don't exist — referencing requests get pruned
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Correct:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
defineFilter({
|
|
222
|
+
filterable: ['status'],
|
|
223
|
+
model: User,
|
|
224
|
+
relations: { posts: { filterable: ['title'], aggregates: ['views'] } },
|
|
225
|
+
})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Aggregates activate only when the spec can read relation metadata off the model
|
|
229
|
+
— without `model`, nothing is synthesised and no error points at the cause.
|
|
230
|
+
|
|
231
|
+
Source: docs/definitions/aggregates.mdx ("Unlocking aggregates — the model option")
|
|
232
|
+
|
|
233
|
+
### [HIGH] Function-form computed fields without a root table
|
|
234
|
+
|
|
235
|
+
Wrong:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
defineFilter({
|
|
239
|
+
filterable: ['status'],
|
|
240
|
+
computed: { postCount: ({ alias }) => `(SELECT COUNT(*) FROM posts WHERE posts.author_id = ${alias}.id)` },
|
|
241
|
+
}) // alias resolves to '' → malformed SQL
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Correct:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
defineFilter({
|
|
248
|
+
filterable: ['status'],
|
|
249
|
+
model: User, // or table: 'users'
|
|
250
|
+
computed: { postCount: ({ alias }) => `(SELECT COUNT(*) FROM posts WHERE posts.author_id = ${alias}.id)` },
|
|
251
|
+
})
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Correlated subqueries splice `spec.table` as the outer alias; with neither
|
|
255
|
+
`table` nor `model` set, the alias is an empty string and the SQL breaks at the
|
|
256
|
+
database.
|
|
257
|
+
|
|
258
|
+
Source: docs/definitions/computed.mdx ("table is required for the function form")
|
|
259
|
+
|
|
260
|
+
### [HIGH] Reading a dotted alias target as a JSON path
|
|
261
|
+
|
|
262
|
+
Wrong:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
aliases: { tier: 'metadata.tier' }
|
|
266
|
+
// → whereHas('metadata') — looks for a RELATION named metadata, fails at the DB
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Correct:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
computed: { tier: "metadata->>'tier'" } // JSON access needs computed SQL you write
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
In alias targets everything before the last dot is a relation hop; there is no
|
|
276
|
+
JSON-path reading, and adding the path to `relations` will not make it one.
|
|
277
|
+
|
|
278
|
+
Source: docs/definitions/aliases.mdx ("A dot in the target means a relation, not a JSON path")
|
|
279
|
+
|
|
280
|
+
### [MEDIUM] Nesting computed aliases inside AND/OR groups
|
|
281
|
+
|
|
282
|
+
Wrong:
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
filter[OR][0][postCount][gte]=5 // treated as a normal column → dropped
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Correct:
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
filter[postCount][gte]=5 // keep computed conditions top-level
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Computed filtering is recognized only on top-level leaf filters; inside a
|
|
295
|
+
group the alias falls back to the column pipeline and vanishes unless it is
|
|
296
|
+
also a real column.
|
|
297
|
+
|
|
298
|
+
Source: docs/definitions/computed.mdx ("How a computed filter is routed")
|
|
299
|
+
|
|
300
|
+
### [MEDIUM] Chaining aliases expecting multi-hop resolution
|
|
301
|
+
|
|
302
|
+
Wrong:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
resolveFieldAlias({ a: 'b', b: 'c' }, 'a') // expecting 'c' — actually returns 'b'
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Correct:
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
resolveFieldAlias({ a: 'c', b: 'c' }, 'a') // point aliases directly at real targets
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Resolution runs exactly one hop and never re-runs the target through the map,
|
|
315
|
+
which makes cycles structurally impossible — and multi-hop chains silently
|
|
316
|
+
resolve short.
|
|
317
|
+
|
|
318
|
+
Source: docs/definitions/aliases.mdx ("No cascading, no cycles")
|
|
319
|
+
|
|
320
|
+
### [MEDIUM] Expecting date-coerced values back as Date objects
|
|
321
|
+
|
|
322
|
+
Wrong:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// fieldTypes: { bornOn: { kind: 'date' } } — then expecting value instanceof Date downstream
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Correct:
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
// the bound value stays the validated ISO string ('2026-07-15'); the driver parses it
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
A `date` kind validates syntax but hands the string back verbatim — re-zoning
|
|
335
|
+
it to midnight UTC would shift days for negative-offset clients.
|
|
336
|
+
|
|
337
|
+
Source: docs/definitions/index.mdx (coercion rules)
|
|
338
|
+
|
|
339
|
+
See also: `../filter-codegen/SKILL.md` (the same spec generates the typed
|
|
340
|
+
client), `../filter-safety/SKILL.md` (the boundary these declarations enforce).
|