@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,46 @@
1
+ import { HttpContext } from '@adonisjs/core/http';
2
+ import { applyFilterFromRequest, } from './apply_from_request.js';
3
+ import { stashPagination } from './pagination_stash.js';
4
+ export function Filterable(superclass) {
5
+ class FilterableModel extends superclass {
6
+ /** The filter this model is read through. A thunk, so the filter may import the model back. */
7
+ static $filter;
8
+ /**
9
+ * Apply the model's filter to a fresh query and hand back the **query builder** plus the
10
+ * pagination the request resolved to — filters, search and sort applied, nothing executed.
11
+ * Compose whatever else the endpoint needs on the builder, then page it with
12
+ * `filterPaginate()` (no arguments) or `paginate(page, size)`.
13
+ */
14
+ static async filter(ctx, filter, options) {
15
+ // `this` is the model the static was called on (a subclass of the mixin), which is exactly
16
+ // what these helpers need — the rule's suggestion (name the class) would pin every model to
17
+ // the anonymous mixin class instead.
18
+ // biome-ignore lint/complexity/noThisInStatic: `this` is the calling model, by design.
19
+ const model = this;
20
+ const target = filter ?? model.$filter?.();
21
+ if (!target) {
22
+ throw new Error(
23
+ // biome-ignore lint/complexity/noThisInStatic: names the model the call was made on.
24
+ `${this.name} has no filter — declare \`static $filter = () => YourFilter\` on the model, or pass one to filter().`);
25
+ }
26
+ // biome-ignore lint/complexity/noThisInStatic: the calling model builds its own query.
27
+ const query = this.query();
28
+ const pagination = await applyFilterFromRequest(query, target, ctx ?? HttpContext.getOrFail(), options);
29
+ stashPagination(query, pagination);
30
+ return { query, page: pagination.page, size: pagination.size };
31
+ }
32
+ /**
33
+ * The same, then `paginate(page, size)` with the pagination the request resolved to — the
34
+ * whole list endpoint in one call.
35
+ */
36
+ static async filterPaginate(ctx, filter, options) {
37
+ // biome-ignore lint/complexity/noThisInStatic: `this` is the calling model, by design.
38
+ const model = this;
39
+ // biome-ignore lint/complexity/noThisInStatic: forwards the call to the same model.
40
+ const { query, page, size } = await model.filter.call(this, ctx, filter, options);
41
+ return query.paginate(page, size);
42
+ }
43
+ }
44
+ return FilterableModel;
45
+ }
46
+ //# sourceMappingURL=filterable_mixin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filterable_mixin.js","sourceRoot":"","sources":["../../src/filterable_mixin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAOlD,OAAO,EAGL,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAwDxD,MAAM,UAAU,UAAU,CACxB,UAAa;IAEb,MAAM,eAAgB,SAAQ,UAAU;QACtC,+FAA+F;QAC/F,MAAM,CAAC,OAAO,CAAkC;QAEhD;;;;;WAKG;QACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAEjB,GAA0B,EAC1B,MAAiC,EACjC,OAAiC;YAEjC,2FAA2F;YAC3F,4FAA4F;YAC5F,qCAAqC;YACrC,uFAAuF;YACvF,MAAM,KAAK,GAAG,IAAyC,CAAC;YACxD,MAAM,MAAM,GAAG,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;YAC3C,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK;gBACb,qFAAqF;gBACrF,GAAG,IAAI,CAAC,IAAI,uGAAuG,CACpH,CAAC;YACJ,CAAC;YAED,uFAAuF;YACvF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAkC,CAAC;YAC3D,MAAM,UAAU,GAAG,MAAM,sBAAsB,CAC7C,KAAc,EACd,MAAe,EACf,GAAG,IAAI,WAAW,CAAC,SAAS,EAAE,EAC9B,OAAO,CACR,CAAC;YACF,eAAe,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;YACnC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC;QACjE,CAAC;QAED;;;WAGG;QACH,MAAM,CAAC,KAAK,CAAC,cAAc,CAEzB,GAA0B,EAC1B,MAAiC,EACjC,OAAiC;YAEjC,uFAAuF;YACvF,MAAM,KAAK,GAAG,IAAyC,CAAC;YACxD,oFAAoF;YACpF,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;YAClF,OAAQ,KAAsC,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAEjE,CAAC;QACJ,CAAC;KACF;IAED,OAAO,eAAwD,CAAC;AAClE,CAAC"}
@@ -1,5 +1,5 @@
1
1
  /** Keep in sync with this package's `version` in package.json. */
2
- export declare const VERSION = "0.6.0";
2
+ export declare const VERSION = "0.8.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';
@@ -16,6 +16,13 @@ export type { FilterClientEntry, FilterClientManifest, FilterFieldKind, FilterFi
16
16
  export type { DefineFilterOptions, FilterSpec, RelationColumns, RelationSpec, TenantResolver, TenantScopeSpec, } from './filter_spec.js';
17
17
  export { applyCursorFromRequest, applyFilterFromRequest } from './apply_from_request.js';
18
18
  export type { ApplyCursorFromRequestOptions, ApplyFromRequestOptions, FilterRequestContext, } from './apply_from_request.js';
19
+ export { BaseModelFilter } from './base_model_filter.js';
20
+ export { filterFor, filterable, searchable, sortable } from './decorators.js';
21
+ export type { DualDecorator, StandardDecoratorContext } from './decorators.js';
22
+ export { dispatchKeys, isFilterClass, methodForKey, specFromFilterClass, } from './filter_class.js';
23
+ export type { FilterClass } from './filter_class.js';
24
+ export { Filterable } from './filterable_mixin.js';
25
+ export type { FilterableModelStatics } from './filterable_mixin.js';
19
26
  export { registerFilterMacros } from './lucid_macros.js';
20
27
  export type { MacroableQueryBuilder } from './lucid_macros.js';
21
28
  export { parseDistinct, parseFilterRequest, parseSort, toColumnFilters } from './parse_request.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
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.6.0';
2
+ export const VERSION = '0.8.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';
@@ -9,6 +9,10 @@ export { discoverAggregateSources } from './aggregate.js';
9
9
  export { defineFilter, FilterDefinitionError, specToFilterConfig, } from './filter_spec.js';
10
10
  export { filterableFieldPaths, generateFilterClient, generateFilterClients, sortableFieldPaths, } from './generate_client.js';
11
11
  export { applyCursorFromRequest, applyFilterFromRequest } from './apply_from_request.js';
12
+ export { BaseModelFilter } from './base_model_filter.js';
13
+ export { filterFor, filterable, searchable, sortable } from './decorators.js';
14
+ export { dispatchKeys, isFilterClass, methodForKey, specFromFilterClass, } from './filter_class.js';
15
+ export { Filterable } from './filterable_mixin.js';
12
16
  export { registerFilterMacros } from './lucid_macros.js';
13
17
  export { parseDistinct, parseFilterRequest, parseSort, toColumnFilters } from './parse_request.js';
14
18
  export { parseSpatieRequest } from './spatie_parser.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAQ/B,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EACL,wBAAwB,EACxB,gBAAgB,EAChB,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAe1D,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAMzF,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACnG,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,mBAAmB,EACnB,WAAW,EACX,WAAW,EACX,SAAS,EACT,qBAAqB,EACrB,yBAAyB,GAK1B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,eAAe,EACf,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,aAAa,GACd,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAQ/B,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EACL,wBAAwB,EACxB,gBAAgB,EAChB,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAe1D,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAMzF,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE9E,OAAO,EACL,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACnG,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,mBAAmB,EACnB,WAAW,EACX,WAAW,EACX,SAAS,EACT,qBAAqB,EACrB,yBAAyB,GAK1B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,eAAe,EACf,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,aAAa,GACd,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -1,5 +1,7 @@
1
1
  import { type ApplyFromRequestOptions, type FilterRequestContext } from './apply_from_request.js';
2
+ import { type FilterClass } from './filter_class.js';
2
3
  import type { FilterSpec } from './filter_spec.js';
4
+ import type { ResolvedPagination } from './runner.js';
3
5
  /**
4
6
  * The static slice of a Lucid query-builder class we register onto: Adonis'
5
7
  * `Macroable.macro(name, fn)` adds `fn` to the builder prototype so every query
@@ -45,12 +47,41 @@ declare module '@adonisjs/lucid/types/model' {
45
47
  * request scope (e.g. a job/command), where there is no ambient context.
46
48
  */
47
49
  applyFilterFromRequest(spec: FilterSpec, ctx?: FilterRequestContext, options?: ApplyFromRequestOptions): this;
50
+ /**
51
+ * The same for a filter **class**: resolved through the IoC container (so `@inject()` works),
52
+ * `setup()` first, then every request key the class owns a method for.
53
+ *
54
+ * Resolution is async, so this returns a promise — of the resolved `{ page, size }`, not of
55
+ * the builder (a Lucid builder is thenable, so awaiting a promise of one would run the query).
56
+ * The builder is the one you called it on, still yours to compose on:
57
+ *
58
+ * ```ts
59
+ * const query = User.query().whereNull('deletedAt')
60
+ * const { page, size } = await query.applyFilterFromRequest(UserFilter)
61
+ * return query.preload('team').paginate(page, size)
62
+ * ```
63
+ */
64
+ applyFilterFromRequest(filter: FilterClass, ctx?: FilterRequestContext, options?: ApplyFromRequestOptions): Promise<ResolvedPagination>;
48
65
  /**
49
66
  * Apply a {@link FilterSpec} from the request context and paginate with the
50
67
  * resolved `{ page, size }`, returning Lucid's paginator — filter + paginate
51
68
  * in one terminal call. `ctx` is optional (see {@link applyFilterFromRequest}).
52
69
  */
53
70
  filterPaginate(spec: FilterSpec, ctx?: FilterRequestContext, options?: ApplyFromRequestOptions): ReturnType<this['paginate']>;
71
+ /** The filter-class form of {@link filterPaginate} — async, for the same reason. */
72
+ filterPaginate(filter: FilterClass, ctx?: FilterRequestContext, options?: ApplyFromRequestOptions): Promise<Awaited<ReturnType<this['paginate']>>>;
73
+ /**
74
+ * Page a query that has **already** been filtered (by the model's `filter()` or by the
75
+ * `applyFilterFromRequest` macro) with the `{ page, size }` that call resolved — so a
76
+ * controller can keep composing on the builder and still page it the way the request asked.
77
+ *
78
+ * ```ts
79
+ * const query = await User.filter(ctx)
80
+ * query.whereNotNull('confirmedAt')
81
+ * return query.filterPaginate()
82
+ * ```
83
+ */
84
+ filterPaginate(): ReturnType<this['paginate']>;
54
85
  }
55
86
  }
56
87
  //# sourceMappingURL=lucid_macros.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lucid_macros.d.ts","sourceRoot":"","sources":["../../src/lucid_macros.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAE1B,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAanD;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GAAG,IAAI,CAAC;CAC7E;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,iBAAiB,EAAE,qBAAqB,GAAG,IAAI,CAoBnF;AAED,OAAO,QAAQ,6BAA6B,CAAC;IAC3C,UAAU,yBAAyB,CAAC,KAAK,SAAS,UAAU,EAAE,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC;QACxF;;;;;;;;;WASG;QACH,sBAAsB,CACpB,IAAI,EAAE,UAAU,EAChB,GAAG,CAAC,EAAE,oBAAoB,EAC1B,OAAO,CAAC,EAAE,uBAAuB,GAChC,IAAI,CAAC;QACR;;;;WAIG;QACH,cAAc,CACZ,IAAI,EAAE,UAAU,EAChB,GAAG,CAAC,EAAE,oBAAoB,EAC1B,OAAO,CAAC,EAAE,uBAAuB,GAChC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;KACjC;CACF"}
1
+ {"version":3,"file":"lucid_macros.d.ts","sourceRoot":"","sources":["../../src/lucid_macros.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAE1B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,KAAK,WAAW,EAAiB,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAYtD;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GAAG,IAAI,CAAC;CAC7E;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,iBAAiB,EAAE,qBAAqB,GAAG,IAAI,CA+CnF;AAED,OAAO,QAAQ,6BAA6B,CAAC;IAC3C,UAAU,yBAAyB,CAAC,KAAK,SAAS,UAAU,EAAE,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC;QACxF;;;;;;;;;WASG;QACH,sBAAsB,CACpB,IAAI,EAAE,UAAU,EAChB,GAAG,CAAC,EAAE,oBAAoB,EAC1B,OAAO,CAAC,EAAE,uBAAuB,GAChC,IAAI,CAAC;QACR;;;;;;;;;;;;;WAaG;QACH,sBAAsB,CACpB,MAAM,EAAE,WAAW,EACnB,GAAG,CAAC,EAAE,oBAAoB,EAC1B,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CAAC;QAC/B;;;;WAIG;QACH,cAAc,CACZ,IAAI,EAAE,UAAU,EAChB,GAAG,CAAC,EAAE,oBAAoB,EAC1B,OAAO,CAAC,EAAE,uBAAuB,GAChC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAChC,oFAAoF;QACpF,cAAc,CACZ,MAAM,EAAE,WAAW,EACnB,GAAG,CAAC,EAAE,oBAAoB,EAC1B,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QAClD;;;;;;;;;;WAUG;QACH,cAAc,IAAI,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;KAChD;CACF"}
@@ -1,5 +1,7 @@
1
1
  import { HttpContext } from '@adonisjs/core/http';
2
2
  import { applyFilterFromRequest, } from './apply_from_request.js';
3
+ import { isFilterClass } from './filter_class.js';
4
+ import { readPagination, stashPagination } from './pagination_stash.js';
3
5
  /**
4
6
  * O ctx da request para o macro: explícito, ou o `HttpContext` ativo lido do AsyncLocalStorage do
5
7
  * Adonis quando omitido. `getOrFail()` lança quando não há request em escopo (ex.: chamada dentro de
@@ -30,12 +32,34 @@ function resolveCtx(ctx) {
30
32
  * the same implementations.
31
33
  */
32
34
  export function registerFilterMacros(ModelQueryBuilder) {
33
- ModelQueryBuilder.macro('applyFilterFromRequest', function (spec, ctx, options) {
34
- applyFilterFromRequest(this, spec, resolveCtx(ctx), options);
35
+ ModelQueryBuilder.macro('applyFilterFromRequest', function (filter, ctx, options) {
36
+ // A class is resolved through the container, so this leg is async — and it resolves to the
37
+ // pagination rather than to the builder: a Lucid query builder is thenable, so a promise that
38
+ // resolved to one would execute the query instead of handing it back. You already hold the
39
+ // builder; `await query.applyFilterFromRequest(UserFilter)` gives you its `{ page, size }`.
40
+ if (isFilterClass(filter)) {
41
+ return applyFilterFromRequest(this, filter, resolveCtx(ctx), options).then((pagination) => {
42
+ stashPagination(this, pagination);
43
+ return pagination;
44
+ });
45
+ }
46
+ stashPagination(this, applyFilterFromRequest(this, filter, resolveCtx(ctx), options));
35
47
  return this;
36
48
  });
37
- ModelQueryBuilder.macro('filterPaginate', function (spec, ctx, options) {
38
- const { page, size } = applyFilterFromRequest(this, spec, resolveCtx(ctx), options);
49
+ ModelQueryBuilder.macro('filterPaginate', function (filter, ctx, options) {
50
+ // No filter argument: this query has already been through one (the model's `filter()`, or the
51
+ // `applyFilterFromRequest` macro), so page it with the pagination that call resolved.
52
+ if (filter === undefined) {
53
+ const stashed = readPagination(this);
54
+ if (!stashed) {
55
+ throw new Error('filterPaginate() was called with no filter on a query that has not been filtered. Pass the filter — filterPaginate(UserFilter) — or apply one first.');
56
+ }
57
+ return this.paginate(stashed.page, stashed.size);
58
+ }
59
+ if (isFilterClass(filter)) {
60
+ return applyFilterFromRequest(this, filter, resolveCtx(ctx), options).then(({ page, size }) => this.paginate(page, size));
61
+ }
62
+ const { page, size } = applyFilterFromRequest(this, filter, resolveCtx(ctx), options);
39
63
  return this.paginate(page, size);
40
64
  });
41
65
  }
@@ -1 +1 @@
1
- {"version":3,"file":"lucid_macros.js","sourceRoot":"","sources":["../../src/lucid_macros.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,OAAO,EAGL,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AAIjC;;;;;GAKG;AACH,SAAS,UAAU,CAAC,GAAqC;IACvD,OAAO,GAAG,IAAI,WAAW,CAAC,SAAS,EAAE,CAAC;AACxC,CAAC;AAcD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,oBAAoB,CAAC,iBAAwC;IAC3E,iBAAiB,CAAC,KAAK,CAAC,wBAAwB,EAAE,UAEhD,IAAgB,EAChB,GAA0B,EAC1B,OAAiC;QAEjC,sBAAsB,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;QAC7D,OAAO,IAAI,CAAC;IACd,CAAiD,CAAC,CAAC;IAEnD,iBAAiB,CAAC,KAAK,CAAC,gBAAgB,EAAE,UAExC,IAAgB,EAChB,GAA0B,EAC1B,OAAiC;QAEjC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,sBAAsB,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;QACpF,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACnC,CAAiD,CAAC,CAAC;AACrD,CAAC"}
1
+ {"version":3,"file":"lucid_macros.js","sourceRoot":"","sources":["../../src/lucid_macros.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,OAAO,EAGL,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAoB,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGpE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxE;;;;;GAKG;AACH,SAAS,UAAU,CAAC,GAAqC;IACvD,OAAO,GAAG,IAAI,WAAW,CAAC,SAAS,EAAE,CAAC;AACxC,CAAC;AAcD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,oBAAoB,CAAC,iBAAwC;IAC3E,iBAAiB,CAAC,KAAK,CAAC,wBAAwB,EAAE,UAEhD,MAAgC,EAChC,GAA0B,EAC1B,OAAiC;QAEjC,2FAA2F;QAC3F,8FAA8F;QAC9F,2FAA2F;QAC3F,4FAA4F;QAC5F,IAAI,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1B,OAAO,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE;gBACxF,eAAe,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;gBAClC,OAAO,UAAU,CAAC;YACpB,CAAC,CAAC,CAAC;QACL,CAAC;QACD,eAAe,CAAC,IAAI,EAAE,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QACtF,OAAO,IAAI,CAAC;IACd,CAAiD,CAAC,CAAC;IAEnD,iBAAiB,CAAC,KAAK,CAAC,gBAAgB,EAAE,UAExC,MAAiC,EACjC,GAA0B,EAC1B,OAAiC;QAEjC,8FAA8F;QAC9F,sFAAsF;QACtF,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;YACrC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CACb,sJAAsJ,CACvJ,CAAC;YACJ,CAAC;YACD,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QACnD,CAAC;QAED,IAAI,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1B,OAAO,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,CACxE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAsB,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAClE,CAAC;QACJ,CAAC;QACD,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;QACtF,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACnC,CAAiD,CAAC,CAAC;AACrD,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { ResolvedPagination } from './runner.js';
2
+ export declare function stashPagination(query: unknown, pagination: ResolvedPagination): void;
3
+ export declare function readPagination(query: unknown): ResolvedPagination | undefined;
4
+ //# sourceMappingURL=pagination_stash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination_stash.d.ts","sourceRoot":"","sources":["../../src/pagination_stash.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAWtD,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,kBAAkB,GAAG,IAAI,CAIpF;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,GAAG,SAAS,CAG7E"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Where a filtered query remembers the pagination the request resolved to.
3
+ *
4
+ * The builder is handed back to the caller so the endpoint can keep composing on it; the page and
5
+ * size it already resolved ride along on this symbol, so a later `filterPaginate()` needs no
6
+ * arguments and no second read of the request.
7
+ */
8
+ const PAGINATION = Symbol.for('@adonis-agora/filter:pagination');
9
+ export function stashPagination(query, pagination) {
10
+ if (query !== null && typeof query === 'object') {
11
+ query[PAGINATION] = pagination;
12
+ }
13
+ }
14
+ export function readPagination(query) {
15
+ if (query === null || typeof query !== 'object')
16
+ return undefined;
17
+ return query[PAGINATION];
18
+ }
19
+ //# sourceMappingURL=pagination_stash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination_stash.js","sourceRoot":"","sources":["../../src/pagination_stash.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;AAEjE,MAAM,UAAU,eAAe,CAAC,KAAc,EAAE,UAA8B;IAC5E,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/C,KAAiC,CAAC,UAAU,CAAC,GAAG,UAAU,CAAC;IAC9D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAClE,OAAQ,KAAwD,CAAC,UAAU,CAAC,CAAC;AAC/E,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** Absolute path to this package's stubs, handed to `codemods.makeUsingStub`. */
2
+ export declare const stubsRoot: string;
3
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../stubs/main.ts"],"names":[],"mappings":"AAEA,iFAAiF;AACjF,eAAO,MAAM,SAAS,QAAgD,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ /** Absolute path to this package's stubs, handed to `codemods.makeUsingStub`. */
3
+ export const stubsRoot = fileURLToPath(new URL('./', import.meta.url));
4
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../../stubs/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,iFAAiF;AACjF,MAAM,CAAC,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC"}
@@ -0,0 +1,45 @@
1
+ {{#var filterName = string(entity.name).pascalCase().removeSuffix('Filter').toString() + 'Filter'}}
2
+ {{#var modelName = string(entity.name).pascalCase().removeSuffix('Filter').toString()}}
3
+ {{#var filterFileName = string(entity.name).snakeCase().removeSuffix('filter').toString() + '_filter'}}
4
+ {{#var modelFileName = string(entity.name).snakeCase().removeSuffix('filter').toString()}}
5
+ {{{
6
+ exports({
7
+ to: app.makePath('app/filters', entity.path, filterFileName + '.ts')
8
+ })
9
+ }}}
10
+ import { BaseModelFilter } from '@adonis-agora/filter'
11
+ import type { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model'
12
+ import {{ modelName }} from '#models/{{ modelFileName }}'
13
+
14
+ export default class {{ filterName }} extends BaseModelFilter {
15
+ declare $query: ModelQueryBuilderContract<typeof {{ modelName }}>
16
+
17
+ static model = {{ modelName }}
18
+
19
+ /** Columns clients may filter on with the standard operators — these need no method. */
20
+ static filterable = ['name']
21
+ /** Columns clients may sort on. Defaults to `filterable`. */
22
+ static sortable = ['createdAt']
23
+ /** Columns the free-text `search` term scans. */
24
+ static searchable = ['name']
25
+
26
+ static defaultSort = [{ field: 'createdAt', direction: 'desc' as const }]
27
+ static defaultSize = 25
28
+ static maxSize = 100
29
+
30
+ /**
31
+ * Runs on every call, before anything the request asked for — the place for a scope the client
32
+ * cannot relax (a tenant, a soft-delete guard, an ownership check).
33
+ */
34
+ setup() {
35
+ // this.$query.whereNull('deletedAt')
36
+ }
37
+
38
+ /**
39
+ * One method per request key that needs SQL of its own. `?filter[fullName]=silva` lands here;
40
+ * writing the method IS what exposes the key, so it needs no allow-list entry.
41
+ */
42
+ // fullName(value: string) {
43
+ // this.$query.whereILike('full_name', `%${value}%`)
44
+ // }
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/filter",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
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": {
@@ -41,8 +41,10 @@
41
41
  },
42
42
  "files": [
43
43
  "dist/",
44
+ "stubs/",
44
45
  "README.md",
45
- "CHANGELOG.md"
46
+ "CHANGELOG.md",
47
+ "skills"
46
48
  ],
47
49
  "sideEffects": false,
48
50
  "peerDependencies": {
@@ -59,6 +61,7 @@
59
61
  },
60
62
  "devDependencies": {
61
63
  "@adonisjs/assembler": "8.4.0",
64
+ "@tanstack/intent": "^0.3.2",
62
65
  "@adonisjs/core": "7.4.0",
63
66
  "@adonisjs/lucid": "22.4.2",
64
67
  "@types/luxon": "3.7.4",
@@ -80,12 +83,14 @@
80
83
  "query",
81
84
  "spatie",
82
85
  "pagination",
83
- "agora"
86
+ "agora",
87
+ "tanstack-intent"
84
88
  ],
85
89
  "scripts": {
86
- "build": "tsc -p tsconfig.json",
90
+ "build": "tsc -p tsconfig.json && pnpm run copy:stubs",
87
91
  "test": "vitest run --passWithNoTests",
88
92
  "test:watch": "vitest",
89
- "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.typetest.json"
93
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.typetest.json",
94
+ "copy:stubs": "mkdir -p dist/stubs/make/filter && cp stubs/make/filter/main.stub dist/stubs/make/filter/main.stub"
90
95
  }
91
96
  }
@@ -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).