@adonis-agora/filter 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/commands/main.d.ts +5 -3
  3. package/dist/commands/main.d.ts.map +1 -1
  4. package/dist/commands/main.js +4 -2
  5. package/dist/commands/main.js.map +1 -1
  6. package/dist/commands/make_filter.d.ts +18 -0
  7. package/dist/commands/make_filter.d.ts.map +1 -0
  8. package/dist/commands/make_filter.js +32 -0
  9. package/dist/commands/make_filter.js.map +1 -0
  10. package/dist/src/apply_from_request.d.ts +8 -0
  11. package/dist/src/apply_from_request.d.ts.map +1 -1
  12. package/dist/src/apply_from_request.js +70 -21
  13. package/dist/src/apply_from_request.js.map +1 -1
  14. package/dist/src/base_model_filter.d.ts +109 -0
  15. package/dist/src/base_model_filter.d.ts.map +1 -0
  16. package/dist/src/base_model_filter.js +89 -0
  17. package/dist/src/base_model_filter.js.map +1 -0
  18. package/dist/src/decorator_metadata.d.ts +41 -0
  19. package/dist/src/decorator_metadata.d.ts.map +1 -0
  20. package/dist/src/decorator_metadata.js +83 -0
  21. package/dist/src/decorator_metadata.js.map +1 -0
  22. package/dist/src/decorators.d.ts +73 -0
  23. package/dist/src/decorators.d.ts.map +1 -0
  24. package/dist/src/decorators.js +118 -0
  25. package/dist/src/decorators.js.map +1 -0
  26. package/dist/src/filter_class.d.ts +46 -0
  27. package/dist/src/filter_class.d.ts.map +1 -0
  28. package/dist/src/filter_class.js +238 -0
  29. package/dist/src/filter_class.js.map +1 -0
  30. package/dist/src/filterable_mixin.d.ts +49 -0
  31. package/dist/src/filterable_mixin.d.ts.map +1 -0
  32. package/dist/src/filterable_mixin.js +46 -0
  33. package/dist/src/filterable_mixin.js.map +1 -0
  34. package/dist/src/index.d.ts +8 -1
  35. package/dist/src/index.d.ts.map +1 -1
  36. package/dist/src/index.js +5 -1
  37. package/dist/src/index.js.map +1 -1
  38. package/dist/src/lucid_macros.d.ts +31 -0
  39. package/dist/src/lucid_macros.d.ts.map +1 -1
  40. package/dist/src/lucid_macros.js +28 -4
  41. package/dist/src/lucid_macros.js.map +1 -1
  42. package/dist/src/pagination_stash.d.ts +4 -0
  43. package/dist/src/pagination_stash.d.ts.map +1 -0
  44. package/dist/src/pagination_stash.js +19 -0
  45. package/dist/src/pagination_stash.js.map +1 -0
  46. package/dist/stubs/main.d.ts +3 -0
  47. package/dist/stubs/main.d.ts.map +1 -0
  48. package/dist/stubs/main.js +4 -0
  49. package/dist/stubs/main.js.map +1 -0
  50. package/dist/stubs/make/filter/main.stub +45 -0
  51. package/package.json +10 -5
  52. package/skills/filter-basics/SKILL.md +248 -0
  53. package/skills/filter-codegen/SKILL.md +246 -0
  54. package/skills/filter-definitions/SKILL.md +337 -0
  55. package/skills/filter-querying/SKILL.md +319 -0
  56. package/skills/filter-safety/SKILL.md +304 -0
  57. package/stubs/main.ts +4 -0
  58. 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,337 @@
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; index the child FK (or materialise a counter) on
156
+ hot paths.
157
+
158
+ Source: `docs/definitions/aggregates.mdx`
159
+
160
+ ### 4. Aliases and field types
161
+
162
+ Aliases remap a public name onto a whitelisted target (base column or relation
163
+ path); resolution runs before allow-listing, exactly one hop:
164
+
165
+ ```typescript
166
+ export const userFilter = defineFilter({
167
+ filterable: ['status', 'name'],
168
+ relations: { posts: { filterable: ['title'] } },
169
+ aliases: {
170
+ legacyStatus: 'status',
171
+ postTitle: 'posts.title',
172
+ },
173
+ fieldTypes: { age: { kind: 'number' } },
174
+ })
175
+ ```
176
+
177
+ Prefer the colocated map form when several fields have non-string kinds —
178
+ `filterable: { advisorId: 'string', dayOfWeek: 'number', isRecurring: 'boolean' }`
179
+ desugars into the array plus `fieldTypes`. An explicit `fieldTypes` entry still
180
+ wins per field, which is how you add codegen-only richness (`enumValues`,
181
+ `typeRef`).
182
+
183
+ Source: `docs/definitions/aliases.mdx`, `docs/definitions/index.mdx`
184
+
185
+ ### 5. Reuse the spec outside HTTP
186
+
187
+ `specToFilterConfig(spec)` projects the spec onto the per-call `FilterConfig`;
188
+ pass pre-parsed input via `options.input` for jobs/tests;
189
+ `resolveFieldAlias` / `remapFilterAliases` / `remapSortAliases` are exported
190
+ for custom pipelines:
191
+
192
+ ```typescript
193
+ import { applyFilterFromRequest } from '@adonis-agora/filter'
194
+
195
+ applyFilterFromRequest(query, userFilter, ctx, {
196
+ input: { filters: [{ field: 'status', operator: 'equals', value: 'active' }] },
197
+ })
198
+ ```
199
+
200
+ Source: `docs/definitions/index.mdx` (Under the hood)
201
+
202
+ ## Common mistakes
203
+
204
+ ### [HIGH] Declaring aggregate paths without the owning `model`
205
+
206
+ Wrong:
207
+
208
+ ```typescript
209
+ defineFilter({
210
+ filterable: ['status'],
211
+ relations: { posts: { filterable: ['title'], aggregates: ['views'] } },
212
+ }) // posts.$count etc. simply don't exist — referencing requests get pruned
213
+ ```
214
+
215
+ Correct:
216
+
217
+ ```typescript
218
+ defineFilter({
219
+ filterable: ['status'],
220
+ model: User,
221
+ relations: { posts: { filterable: ['title'], aggregates: ['views'] } },
222
+ })
223
+ ```
224
+
225
+ Aggregates activate only when the spec can read relation metadata off the model
226
+ — without `model`, nothing is synthesised and no error points at the cause.
227
+
228
+ Source: docs/definitions/aggregates.mdx ("Unlocking aggregates — the model option")
229
+
230
+ ### [HIGH] Function-form computed fields without a root table
231
+
232
+ Wrong:
233
+
234
+ ```typescript
235
+ defineFilter({
236
+ filterable: ['status'],
237
+ computed: { postCount: ({ alias }) => `(SELECT COUNT(*) FROM posts WHERE posts.author_id = ${alias}.id)` },
238
+ }) // alias resolves to '' → malformed SQL
239
+ ```
240
+
241
+ Correct:
242
+
243
+ ```typescript
244
+ defineFilter({
245
+ filterable: ['status'],
246
+ model: User, // or table: 'users'
247
+ computed: { postCount: ({ alias }) => `(SELECT COUNT(*) FROM posts WHERE posts.author_id = ${alias}.id)` },
248
+ })
249
+ ```
250
+
251
+ Correlated subqueries splice `spec.table` as the outer alias; with neither
252
+ `table` nor `model` set, the alias is an empty string and the SQL breaks at the
253
+ database.
254
+
255
+ Source: docs/definitions/computed.mdx ("table is required for the function form")
256
+
257
+ ### [HIGH] Reading a dotted alias target as a JSON path
258
+
259
+ Wrong:
260
+
261
+ ```typescript
262
+ aliases: { tier: 'metadata.tier' }
263
+ // → whereHas('metadata') — looks for a RELATION named metadata, fails at the DB
264
+ ```
265
+
266
+ Correct:
267
+
268
+ ```typescript
269
+ computed: { tier: "metadata->>'tier'" } // JSON access needs computed SQL you write
270
+ ```
271
+
272
+ In alias targets everything before the last dot is a relation hop; there is no
273
+ JSON-path reading, and adding the path to `relations` will not make it one.
274
+
275
+ Source: docs/definitions/aliases.mdx ("A dot in the target means a relation, not a JSON path")
276
+
277
+ ### [MEDIUM] Nesting computed aliases inside AND/OR groups
278
+
279
+ Wrong:
280
+
281
+ ```
282
+ filter[OR][0][postCount][gte]=5 // treated as a normal column → dropped
283
+ ```
284
+
285
+ Correct:
286
+
287
+ ```
288
+ filter[postCount][gte]=5 // keep computed conditions top-level
289
+ ```
290
+
291
+ Computed filtering is recognized only on top-level leaf filters; inside a
292
+ group the alias falls back to the column pipeline and vanishes unless it is
293
+ also a real column.
294
+
295
+ Source: docs/definitions/computed.mdx ("How a computed filter is routed")
296
+
297
+ ### [MEDIUM] Chaining aliases expecting multi-hop resolution
298
+
299
+ Wrong:
300
+
301
+ ```typescript
302
+ resolveFieldAlias({ a: 'b', b: 'c' }, 'a') // expecting 'c' — actually returns 'b'
303
+ ```
304
+
305
+ Correct:
306
+
307
+ ```typescript
308
+ resolveFieldAlias({ a: 'c', b: 'c' }, 'a') // point aliases directly at real targets
309
+ ```
310
+
311
+ Resolution runs exactly one hop and never re-runs the target through the map,
312
+ which makes cycles structurally impossible — and multi-hop chains silently
313
+ resolve short.
314
+
315
+ Source: docs/definitions/aliases.mdx ("No cascading, no cycles")
316
+
317
+ ### [MEDIUM] Expecting date-coerced values back as Date objects
318
+
319
+ Wrong:
320
+
321
+ ```typescript
322
+ // fieldTypes: { bornOn: { kind: 'date' } } — then expecting value instanceof Date downstream
323
+ ```
324
+
325
+ Correct:
326
+
327
+ ```typescript
328
+ // the bound value stays the validated ISO string ('2026-07-15'); the driver parses it
329
+ ```
330
+
331
+ A `date` kind validates syntax but hands the string back verbatim — re-zoning
332
+ it to midnight UTC would shift days for negative-offset clients.
333
+
334
+ Source: docs/definitions/index.mdx (coercion rules)
335
+
336
+ See also: `../filter-codegen/SKILL.md` (the same spec generates the typed
337
+ client), `../filter-safety/SKILL.md` (the boundary these declarations enforce).