@adonis-agora/filter 0.7.0 → 0.7.1
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 +6 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/package.json +6 -3
- package/skills/filter-basics/SKILL.md +248 -0
- package/skills/filter-codegen/SKILL.md +246 -0
- package/skills/filter-definitions/SKILL.md +337 -0
- package/skills/filter-querying/SKILL.md +319 -0
- package/skills/filter-safety/SKILL.md +304 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @adonis-agora/filter
|
|
2
2
|
|
|
3
|
+
## 0.7.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#26](https://github.com/DavideCarvalho/adonis-agora-filter/pull/26) [`9b07c15`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/9b07c159716c5032812bbf5c2360ff82df7be4c5) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Ship TanStack Intent AI-agent skills: five core skills under `packages/core/skills/` (filter-basics, filter-safety, filter-definitions, filter-querying, filter-codegen) and one under `packages/client/skills/` (filter-query-builder), plus repo-level `_artifacts/` (domain map, skill spec, skill tree) and a `check-skills` GitHub workflow validating them on PRs. Skills are included in each package's `files` so they land in `node_modules` on install.
|
|
8
|
+
|
|
3
9
|
## 0.7.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Keep in sync with this package's `version` in package.json. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.7.0";
|
|
3
3
|
export type { ColumnFilter, FilterOperator, FilterOperatorAlias, FilterOperatorInput, } from './operators.js';
|
|
4
4
|
export { FILTER_OPERATORS, OPERATOR_ALIASES } from './operators.js';
|
|
5
5
|
export { InvalidColumnFilterError, MAX_FILTER_DEPTH, normalizeOperator, validateColumnFilter, validateColumnFilters, } from './validate-column-filter.js';
|
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Keep in sync with this package's `version` in package.json. */
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.7.0';
|
|
3
3
|
export { FILTER_OPERATORS, OPERATOR_ALIASES } from './operators.js';
|
|
4
4
|
export { InvalidColumnFilterError, MAX_FILTER_DEPTH, normalizeOperator, validateColumnFilter, validateColumnFilters, } from './validate-column-filter.js';
|
|
5
5
|
export { isOperatorObject, valueToColumnFilters } from './value-shape.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/filter",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Server-side query filtering/sorting/pagination for AdonisJS — Spatie-style input, a Lucid adapter, and field allow-listing. Part of the Agora ecosystem.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"files": [
|
|
43
43
|
"dist/",
|
|
44
44
|
"README.md",
|
|
45
|
-
"CHANGELOG.md"
|
|
45
|
+
"CHANGELOG.md",
|
|
46
|
+
"skills"
|
|
46
47
|
],
|
|
47
48
|
"sideEffects": false,
|
|
48
49
|
"peerDependencies": {
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"@adonisjs/assembler": "8.4.0",
|
|
63
|
+
"@tanstack/intent": "^0.3.2",
|
|
62
64
|
"@adonisjs/core": "7.4.0",
|
|
63
65
|
"@adonisjs/lucid": "22.4.2",
|
|
64
66
|
"@types/luxon": "3.7.4",
|
|
@@ -80,7 +82,8 @@
|
|
|
80
82
|
"query",
|
|
81
83
|
"spatie",
|
|
82
84
|
"pagination",
|
|
83
|
-
"agora"
|
|
85
|
+
"agora",
|
|
86
|
+
"tanstack-intent"
|
|
84
87
|
],
|
|
85
88
|
"scripts": {
|
|
86
89
|
"build": "tsc -p tsconfig.json",
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: filter-basics
|
|
3
|
+
description: >
|
|
4
|
+
Add Spatie/JSON:API-style query filtering to an AdonisJS endpoint with
|
|
5
|
+
@adonis-agora/filter. Covers parseFilterRequest(qs), applyFilter(query, input,
|
|
6
|
+
{ allowed, sortable, searchable }) with the returned/clamped { page, size },
|
|
7
|
+
the declarative defineFilter + applyFilterFromRequest flow (tenant scope,
|
|
8
|
+
defaultFilters, defaultSort, defaultSize/maxSize), the optional provider
|
|
9
|
+
macros .applyFilterFromRequest() / .filterPaginate(), wire format parsing
|
|
10
|
+
(filter[field][op]=value, sort=-createdAt, page/size), and input sourcing from
|
|
11
|
+
query string, POST body, or a nested key. Use when building a filtered list
|
|
12
|
+
endpoint, wiring a reusable filter policy, or deciding free functions vs macros.
|
|
13
|
+
metadata:
|
|
14
|
+
type: core
|
|
15
|
+
library: "@adonis-agora/filter"
|
|
16
|
+
library_version: "0.7.0"
|
|
17
|
+
framework: adonisjs
|
|
18
|
+
sources:
|
|
19
|
+
- "DavideCarvalho/adonis-filter:docs/getting-started.mdx"
|
|
20
|
+
- "DavideCarvalho/adonis-filter:docs/guides/controllers.mdx"
|
|
21
|
+
- "DavideCarvalho/adonis-filter:docs/guides/filter-classes.mdx"
|
|
22
|
+
- "DavideCarvalho/adonis-filter:docs/guides/provider.mdx"
|
|
23
|
+
- "DavideCarvalho/adonis-filter:docs/guides/operators.mdx"
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# Filter basics: parse a request, apply it to a Lucid query
|
|
27
|
+
|
|
28
|
+
The whole server surface is two functions: `parseFilterRequest(request.qs())`
|
|
29
|
+
reshapes the request into a structured `FilterInput`, then
|
|
30
|
+
`applyFilter(query, input, config)` validates it, prunes it against an
|
|
31
|
+
allow-list, mutates your Lucid query in place, and returns the resolved
|
|
32
|
+
`{ page, size }`. No config file, no decorators; the optional service provider
|
|
33
|
+
only adds chainable macros.
|
|
34
|
+
|
|
35
|
+
## Setup
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @adonis-agora/filter
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// app/controllers/users_controller.ts
|
|
43
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
44
|
+
import { parseFilterRequest, applyFilter } from '@adonis-agora/filter'
|
|
45
|
+
import User from '#models/user'
|
|
46
|
+
|
|
47
|
+
export default class UsersController {
|
|
48
|
+
async index({ request }: HttpContext) {
|
|
49
|
+
const input = parseFilterRequest(request.qs())
|
|
50
|
+
const query = User.query()
|
|
51
|
+
|
|
52
|
+
const { page, size } = applyFilter(query, input, {
|
|
53
|
+
allowed: ['name', 'email', 'age', 'status'],
|
|
54
|
+
sortable: ['name', 'createdAt'],
|
|
55
|
+
searchable: ['name', 'email'],
|
|
56
|
+
defaultSize: 25,
|
|
57
|
+
maxSize: 100,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return query.paginate(page, size)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// start/routes.ts
|
|
67
|
+
import router from '@adonisjs/core/services/router'
|
|
68
|
+
const UsersController = () => import('#controllers/users_controller')
|
|
69
|
+
|
|
70
|
+
router.get('/users', [UsersController, 'index'])
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A request like
|
|
74
|
+
`GET /users?filter[status]=active&filter[age][gte]=18&search=fleet&sort=-createdAt&page=1&size=25`
|
|
75
|
+
parses, validates, prunes against the allow-list, applies WHERE/ORDER BY/ILIKE,
|
|
76
|
+
and returns `{ page: 1, size: 25 }` for `query.paginate()`.
|
|
77
|
+
|
|
78
|
+
## Core patterns
|
|
79
|
+
|
|
80
|
+
### 1. Two-function primitive — parse once, apply once
|
|
81
|
+
|
|
82
|
+
`parseFilterRequest` is a pure reshape (no validation, no allow-listing);
|
|
83
|
+
`applyFilter` owns validation and pruning. It reads any decoded object, so the
|
|
84
|
+
same controller serves GET query strings and structured POST bodies:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { parseFilterRequest, applyFilter } from '@adonis-agora/filter'
|
|
88
|
+
|
|
89
|
+
// GET — decoded query string
|
|
90
|
+
const input = parseFilterRequest(request.qs())
|
|
91
|
+
|
|
92
|
+
// POST /search — the body may be the flat Spatie shape or the structured
|
|
93
|
+
// object the client builder's .build() returns ({ filter: { where }, sort, paginate })
|
|
94
|
+
const bodyInput = parseFilterRequest(request.body())
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Source: `docs/guides/controllers.mdx`, `docs/guides/operators.mdx`
|
|
98
|
+
|
|
99
|
+
### 2. Reusable policies with defineFilter + applyFilterFromRequest
|
|
100
|
+
|
|
101
|
+
When a policy spans endpoints, declare a `FilterSpec` once at module scope and
|
|
102
|
+
apply it in one call. Server scope (tenant + `defaultFilters`) is injected
|
|
103
|
+
before allow-listed request filters; `defaultSort` applies when the request has
|
|
104
|
+
none:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// app/filters/user_filter.ts
|
|
108
|
+
import { defineFilter } from '@adonis-agora/filter'
|
|
109
|
+
import User from '#models/user'
|
|
110
|
+
|
|
111
|
+
export const userFilter = defineFilter({
|
|
112
|
+
filterable: ['name', 'email', 'age', 'status'],
|
|
113
|
+
sortable: ['name', 'createdAt'],
|
|
114
|
+
searchable: ['name', 'email'],
|
|
115
|
+
relations: { posts: { filterable: ['title', 'status'] } },
|
|
116
|
+
tenant: { column: 'tenantId', resolve: (ctx) => (ctx as any).auth.user!.tenantId },
|
|
117
|
+
defaultFilters: [{ field: 'deletedAt', operator: 'isNull' }],
|
|
118
|
+
defaultSort: [{ field: 'createdAt', direction: 'desc' }],
|
|
119
|
+
defaultSize: 25,
|
|
120
|
+
maxSize: 100,
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// app/controllers/users_controller.ts
|
|
126
|
+
import { applyFilterFromRequest } from '@adonis-agora/filter'
|
|
127
|
+
import { userFilter } from '#filters/user_filter'
|
|
128
|
+
import User from '#models/user'
|
|
129
|
+
|
|
130
|
+
async index(ctx: HttpContext) {
|
|
131
|
+
const query = User.query()
|
|
132
|
+
const { page, size } = applyFilterFromRequest(query, userFilter, ctx)
|
|
133
|
+
return query.paginate(page, size)
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Source: `docs/definitions/index.mdx`
|
|
138
|
+
|
|
139
|
+
### 3. Optional macros — inline chaining
|
|
140
|
+
|
|
141
|
+
Register the provider (`node ace add @adonis-agora/filter`, or add
|
|
142
|
+
`() => import('@adonis-agora/filter/filter_provider')` under `providers` in
|
|
143
|
+
`adonisrc.ts`) and every model query gains `.applyFilterFromRequest(spec)` /
|
|
144
|
+
`.filterPaginate(spec)`:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { userFilter } from '#filters/user_filter'
|
|
148
|
+
import User from '#models/user'
|
|
149
|
+
|
|
150
|
+
// ctx read from AsyncLocalStorage inside a request
|
|
151
|
+
return User.query().where('active', true).applyFilterFromRequest(userFilter)
|
|
152
|
+
|
|
153
|
+
// filter + paginate in one terminal call
|
|
154
|
+
return User.query().filterPaginate(userFilter)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The free `applyFilterFromRequest(query, spec, ctx, options?)` always needs an
|
|
158
|
+
explicit `ctx` — the AsyncLocalStorage fallback exists only in the macro layer.
|
|
159
|
+
|
|
160
|
+
Source: `docs/guides/provider.mdx`
|
|
161
|
+
|
|
162
|
+
### 4. Pagination is returned, not applied
|
|
163
|
+
|
|
164
|
+
`applyFilter` resolves `size = clamp(input.size ?? defaultSize ?? 25, 1, maxSize ?? 100)`
|
|
165
|
+
and `page = max(1, input.page ?? 1)` but leaves execution to you — so you can
|
|
166
|
+
paginate, run plain `.exec()`, or layer a DISTINCT projection on the same
|
|
167
|
+
filtered query.
|
|
168
|
+
|
|
169
|
+
Source: `docs/getting-started.mdx`, `docs/guides/filter-classes.mdx`
|
|
170
|
+
|
|
171
|
+
## Common mistakes
|
|
172
|
+
|
|
173
|
+
### [CRITICAL] Feeding raw request page/size to paginate()
|
|
174
|
+
|
|
175
|
+
Wrong:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const input = parseFilterRequest(request.qs())
|
|
179
|
+
await User.query().paginate(input.page ?? 1, input.size ?? 25)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Correct:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
const query = User.query()
|
|
186
|
+
const { page, size } = applyFilter(query, input, { allowed: ['name'], maxSize: 100 })
|
|
187
|
+
return query.paginate(page, size)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`applyFilter` clamps size into `[1, maxSize]` and floors page at 1; values taken
|
|
191
|
+
straight off the query string skip the clamp entirely, so `?size=500000`
|
|
192
|
+
executes against the database.
|
|
193
|
+
|
|
194
|
+
Source: docs/getting-started.mdx ("Why pagination is returned, not applied"); docs/guides/filter-classes.mdx (Pagination bounds)
|
|
195
|
+
|
|
196
|
+
### [HIGH] Putting the tenant column in the allow-list
|
|
197
|
+
|
|
198
|
+
Wrong:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
export const userFilter = defineFilter({
|
|
202
|
+
filterable: ['name', 'tenantId'], // client can now constrain tenantId too
|
|
203
|
+
tenant: { column: 'tenantId', resolve: (ctx) => ctx.tenantId },
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Correct:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
export const userFilter = defineFilter({
|
|
211
|
+
filterable: ['name'],
|
|
212
|
+
tenant: { column: 'tenantId', resolve: (ctx) => ctx.tenantId },
|
|
213
|
+
})
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The tenant scope always lands as trusted server policy; if the same column is
|
|
217
|
+
also client-filterable, a sent value AND-combines with it and silently returns
|
|
218
|
+
an empty result set. Same rule for pre-scoped queries: constrain before
|
|
219
|
+
`applyFilter` and leave the column out of `allowed`.
|
|
220
|
+
|
|
221
|
+
Source: docs/definitions/index.mdx ("Tenant scope is un-tamperable"); docs/guides/controllers.mdx
|
|
222
|
+
|
|
223
|
+
### [MEDIUM] Expecting `search` to work without `searchable`
|
|
224
|
+
|
|
225
|
+
Wrong:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
applyFilter(query, input, { allowed: ['name'] })
|
|
229
|
+
// ?search=fleet is silently skipped — no searchable columns declared
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Correct:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
applyFilter(query, input, {
|
|
236
|
+
allowed: ['name'],
|
|
237
|
+
searchable: ['name', 'email'],
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The free-text term only routes anywhere when `searchable` is non-empty;
|
|
242
|
+
otherwise it is dropped with no error and rows come back unfiltered by search.
|
|
243
|
+
|
|
244
|
+
Source: docs/guides/filter-classes.mdx ("searchable enables free-text search")
|
|
245
|
+
|
|
246
|
+
See also: `../filter-safety/SKILL.md` (tightening the boundary before exposing
|
|
247
|
+
the endpoint publicly), `../filter-definitions/SKILL.md` (growing the policy
|
|
248
|
+
into a reusable spec).
|
|
@@ -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).
|