@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# @adonis-agora/filter
|
|
2
|
+
|
|
3
|
+
## 0.0.0-beta-20261110131309
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#135](https://github.com/DavideCarvalho/adonis-agora-filter/pull/135) [`ed96cf0`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/ed96cf067d8e4d0983cde35c1c59eab81f94bac8) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Fix `node ace add @adonis-agora/filter`: the package's main entry now exports the `configure` hook, so the provider and the commands barrel get registered in `adonisrc.ts`.
|
|
8
|
+
|
|
9
|
+
Fix `node ace make:filter`, which crashed with `SyntaxError: Unexpected identifier 'filterable'` while compiling its stub.
|
|
10
|
+
|
|
11
|
+
## 0.11.2
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#128](https://github.com/DavideCarvalho/adonis-agora-filter/pull/128) [`456d256`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/456d256e1f9001751a03193e4aa98cc4740781c0) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - perf(in-memory): read each row's sort values at most once (lazily memoized) instead of twice per comparison, memoize the reader used for to-one relation hops, and hoist per-field kind lookups out of the distinct/group-by row loops. Sorting 20k rows by a virtual field is ~2x faster and by a to-one relation path ~5x faster; results and error behaviour are unchanged.
|
|
16
|
+
|
|
17
|
+
## 0.11.1
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- [#126](https://github.com/DavideCarvalho/adonis-agora-filter/pull/126) [`e03c7b7`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/e03c7b7a833ba4e3a8a2012dfbd3efae62162262) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Faster list queries, and a `notContains` fix.
|
|
22
|
+
|
|
23
|
+
- `filterPaginate` (the query macro and `Model.filterPaginate`) reads the page before the total and skips Lucid's `COUNT(*)` when the page comes back short, since `offset + rows` already is the total. Full pages, and empty pages past the first, still count. Models with `before:paginate` / `before:fetch` hooks, and select lists that can change the row count (`distinct`, aggregates, raw expressions, `union`), keep Lucid's own `paginate`. The paginator and its meta are unchanged.
|
|
24
|
+
- The keyset seek ANDs a redundant `c0 >= v0` (`<=` for a `desc` lead) in front of its OR-of-tiers predicate, so Postgres can seek a `(c0, id)` index to the cursor instead of scanning from the start of it. Skipped when the boundary value is NULL.
|
|
25
|
+
- A to-many `$count` filter that only tests emptiness (`gt 0`, `gte 1`, `notEquals 0`, `equals 0`, `lte 0`, `lt 1`) compiles to `EXISTS` / `NOT EXISTS` instead of a correlated `COUNT(*)`. Same rows.
|
|
26
|
+
- Fix: `notContains` compiled to a not-equals test. It now is the negation of `contains` — `field IS NOT NULL AND NOT (field ILIKE '%v%')` — with the same case-insensitivity and LIKE escaping. `QueryBuilderLike` gains the `whereNot(callback)` overload this needs (Lucid, the in-memory builder and `MockQueryBuilder` implement it).
|
|
27
|
+
|
|
28
|
+
## 0.11.0
|
|
29
|
+
|
|
30
|
+
### Minor Changes
|
|
31
|
+
|
|
32
|
+
- [#105](https://github.com/DavideCarvalho/adonis-agora-filter/pull/105) [`72ff139`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/72ff139e16c9ac05f326d490a997579ac5fb3273) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Drop Node.js 20 and 22: `engines.node` is now `>=24`. Node 20 reached end-of-life on 2026-04-30, and AdonisJS 7, which these packages require as a peer, needs Node 24 or later (`@adonisjs/core` declares `engines.node >=24.0.0`). The supported lines are the ones Node still maintains that AdonisJS 7 accepts: 24 and 26, both tested in CI.
|
|
33
|
+
|
|
34
|
+
## 0.10.0
|
|
35
|
+
|
|
36
|
+
### Minor Changes
|
|
37
|
+
|
|
38
|
+
- [#67](https://github.com/DavideCarvalho/adonis-agora-filter/pull/67) [`1dd11c3`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/1dd11c3563cd015d27627808cc3e23830ed4be50) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - In-memory adapter at `@adonis-agora/filter/in-memory`: `defineCollection({ fields, primaryKey, relations })` declares a row source (field types incl. `'string[]'`-style arrays, virtual fields, to-one/to-many relations), and `collection.query(rows)` is a `QueryBuilderLike` the existing runner drives unchanged — `applyFilter`, `applyCursor`, `applyFilterFromRequest`, `applyCursorFromRequest` and `groupByCountFromRequest` all work over a plain array with the same parsing, allow-listing and coercion. Evaluation mirrors the SQL the Lucid adapter emits (ILIKE, three-valued NULL logic, NULLS LAST asc / FIRST desc, EXISTS for to-many paths). `filterInMemory` / `cursorInMemory` do it in one call; `InMemoryQueryError` is thrown for SQL-only features (`computed`, `fullText`, `vectorSimilarity`).
|
|
39
|
+
|
|
40
|
+
## 0.9.0
|
|
41
|
+
|
|
42
|
+
### Minor Changes
|
|
43
|
+
|
|
44
|
+
- [#35](https://github.com/DavideCarvalho/adonis-agora-filter/pull/35) [`b6424ec`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/b6424ec696dbf8b50928e03a619d0227f5436345) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - One class style that does everything: `BaseFilter<TQuery>` — the generic base behind `BaseModelFilter` (Lucid) and custom backends alike — plus `applyCustomFilter` (methods per key over a caller-created draft, same envelope, loud rejections) and `groupByCount` end to end (`FilterQueryBuilder.groupByCount()` + `groupByCount[field]` envelope + `groupByCountFromRequest` over Lucid `GROUP BY` or a `GroupByCountAdapter`, with scope, search and paging). Lucid gains the `select`/`count`/`groupBy`/`offset` aggregation seam (optional, non-breaking).
|
|
45
|
+
|
|
46
|
+
## 0.8.1
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- [#32](https://github.com/DavideCarvalho/adonis-agora-filter/pull/32) [`1fe94ce`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/1fe94cee8ef847c25b163b68f3eaba9eb4c1c7ac) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Declare `@adonisjs/core` as a required peer, not an optional one.
|
|
51
|
+
|
|
52
|
+
The package's main entry point reads the ambient `HttpContext` (the Lucid macros and the
|
|
53
|
+
`Filterable` mixin both resolve the request context from AsyncLocalStorage when no `ctx` is
|
|
54
|
+
passed), so importing `@adonis-agora/filter` without `@adonisjs/core` installed failed at load
|
|
55
|
+
with `ERR_MODULE_NOT_FOUND` rather than warning at install. The peer now says what the code does.
|
|
56
|
+
|
|
57
|
+
`@adonisjs/lucid` stays optional and structural — the library talks to any `QueryBuilderLike`. A
|
|
58
|
+
new test walks the barrel's import graph and fails on any value import of `@adonisjs/lucid`, which
|
|
59
|
+
is the promise that _is_ kept.
|
|
60
|
+
|
|
61
|
+
## 0.8.0
|
|
62
|
+
|
|
63
|
+
### Minor Changes
|
|
64
|
+
|
|
65
|
+
- [#29](https://github.com/DavideCarvalho/adonis-agora-filter/pull/29) [`2fd4700`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/2fd47000b87b025edf4c117ef97b1d9b731731a0) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Filter classes — the `adonis-lucid-filter` authoring shape, with the whole pipeline behind it.
|
|
66
|
+
|
|
67
|
+
A filter can now be a class:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
@inject()
|
|
71
|
+
export default class UserFilter extends BaseModelFilter {
|
|
72
|
+
declare $query: ModelQueryBuilderContract<typeof User>;
|
|
73
|
+
constructor(private tenants: TenantsService) {
|
|
74
|
+
super();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static model = User;
|
|
78
|
+
static filterable = ["name", "email", "status"];
|
|
79
|
+
static sortable = ["name", "createdAt"];
|
|
80
|
+
static searchable = ["name", "email"];
|
|
81
|
+
|
|
82
|
+
setup() {
|
|
83
|
+
this.$query.where("tenantId", this.tenants.current(this.$ctx));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fullName(value: string) {
|
|
87
|
+
this.$query.whereRaw("first_name || ' ' || last_name ilike ?", [
|
|
88
|
+
`%${value}%`,
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- **`BaseModelFilter`** — a method per request key, the builder on `this.$query`, a `setup()` that
|
|
95
|
+
runs before anything the request asked for. Writing the method is what exposes the key, so it
|
|
96
|
+
needs no allow-list entry; plain columns stay declarative in the statics.
|
|
97
|
+
- **Container-resolved** — the class is constructed through the request's IoC resolver, so
|
|
98
|
+
`@inject()` on the constructor works exactly as it does in a controller.
|
|
99
|
+
- **`Filterable` model mixin** — `static $filter = () => UserFilter`, then
|
|
100
|
+
`User.filterPaginate(ctx)` for the whole endpoint, or `const { query } = await User.filter(ctx)`
|
|
101
|
+
to keep composing and `query.filterPaginate()` to page it with what the request asked for.
|
|
102
|
+
- **The macros take a class** too, and `filterPaginate()` with no arguments pages a query a
|
|
103
|
+
previous call already filtered.
|
|
104
|
+
- **`node ace make:filter user`** scaffolds the class.
|
|
105
|
+
- Key matching follows the same conventions as `adonis-lucid-filter`: `static blacklist`,
|
|
106
|
+
`static dropId`, `static camelCase`, and a bare top-level key (`?minAge=21`) reaching a method of
|
|
107
|
+
that name. Keys the wire format owns (`sort`, `page`, `search`, …) are never dispatched.
|
|
108
|
+
|
|
109
|
+
Everything compiles to the same `FilterSpec` the declarative `defineFilter` produces, so classes
|
|
110
|
+
and specs share one runner — same allow-listing, operators, search, sort and page clamping.
|
|
111
|
+
|
|
112
|
+
Note for the class form: `applyFilterFromRequest(query, UserFilter, ctx)` and `User.filter(ctx)`
|
|
113
|
+
resolve to `{ page, size }` and `{ query, page, size }` respectively, never to the builder as the
|
|
114
|
+
promise's own value — a Lucid query builder is thenable, so a promise resolving to one would run
|
|
115
|
+
the query instead of handing it back.
|
|
116
|
+
|
|
117
|
+
- [#30](https://github.com/DavideCarvalho/adonis-agora-filter/pull/30) [`9796b85`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/9796b851b078d6d1aa977bbe5a396058d8059d55) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Decorators for the class form, and a `FilterClass` type fix.
|
|
118
|
+
|
|
119
|
+
`@filterFor('team.name')` binds a filter method to the request key(s) it answers, for keys a method
|
|
120
|
+
name could never spell and for one method answering several keys; a bound method stops answering to
|
|
121
|
+
its own name, so the public key survives a rename. A key the wire format owns (`sort`, `page`, …)
|
|
122
|
+
is refused when the class is defined.
|
|
123
|
+
|
|
124
|
+
`@filterable()`, `@sortable()` and `@searchable()` declare a model's filterable surface on the
|
|
125
|
+
columns themselves, stacked under Lucid's `@column()`; a filter picks them up through its
|
|
126
|
+
`static model`, and a static on the filter still replaces the list outright so a stricter filter can
|
|
127
|
+
narrow a shared model. `@filterable('number')` also declares the field's kind.
|
|
128
|
+
|
|
129
|
+
Both work under legacy (`experimentalDecorators`, what AdonisJS compiles) and standard TC39
|
|
130
|
+
decorators — the flavour is detected at call time.
|
|
131
|
+
|
|
132
|
+
Fixes `FilterClass`, which intersected `Record<string, unknown>`: no concrete
|
|
133
|
+
`class UserFilter extends BaseModelFilter` satisfied that, so `static $filter = () => UserFilter`
|
|
134
|
+
on a model did not typecheck. A type test over a real Lucid model now covers it.
|
|
135
|
+
|
|
136
|
+
## 0.7.1
|
|
137
|
+
|
|
138
|
+
### Patch Changes
|
|
139
|
+
|
|
140
|
+
- [#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.
|
|
141
|
+
|
|
142
|
+
## 0.7.0
|
|
143
|
+
|
|
144
|
+
### Minor Changes
|
|
145
|
+
|
|
146
|
+
- [#23](https://github.com/DavideCarvalho/adonis-agora-filter/pull/23) [`2bd4e7b`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/2bd4e7baa50d3d0b4a25c834fc9edf612f7d854a) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - `parseSpatieRequest` now parses `distinct`, matching `parseFilterRequest`.
|
|
147
|
+
|
|
148
|
+
`parseSpatieRequest` is documented as the additive counterpart to
|
|
149
|
+
`parseFilterRequest` — same filter, sort and search shapes, plus cursor
|
|
150
|
+
pagination, includes and sparse fieldsets — so a controller can swap one for the
|
|
151
|
+
other. It did not read `distinct`, even though the runner applies it. Swapping
|
|
152
|
+
the parser turned a working `?distinct=city` into a full, un-deduped result set
|
|
153
|
+
with nothing to signal that the parameter had been dropped.
|
|
154
|
+
|
|
155
|
+
Both string (`distinct=city,tier`) and repeated (`distinct[]=city&distinct[]=tier`)
|
|
156
|
+
forms now parse identically in the two parsers.
|
|
157
|
+
|
|
158
|
+
- [#23](https://github.com/DavideCarvalho/adonis-agora-filter/pull/23) [`2bd4e7b`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/2bd4e7baa50d3d0b4a25c834fc9edf612f7d854a) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Add the `configure` hook, so `node ace add @adonis-agora/filter` actually wires the package.
|
|
159
|
+
|
|
160
|
+
The install instructions have always said to run `node ace add @adonis-agora/filter`,
|
|
161
|
+
but the package shipped no `configure` hook, so the command installed the
|
|
162
|
+
dependency and wired nothing. Two things silently did not happen: the provider
|
|
163
|
+
was never added to `adonisrc.ts` (no `applyFilterFromRequest` / `filterPaginate`
|
|
164
|
+
macros on `ModelQueryBuilder`), and the commands barrel was never registered, so
|
|
165
|
+
`make:filter-client` never appeared in `node ace list`.
|
|
166
|
+
|
|
167
|
+
`node ace add @adonis-agora/filter` — or `node ace configure @adonis-agora/filter`
|
|
168
|
+
on an already-installed package — now registers both:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// adonisrc.ts
|
|
172
|
+
providers: [() => import('@adonis-agora/filter/filter_provider')],
|
|
173
|
+
commands: [() => import('@adonis-agora/filter/commands')],
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Nothing is published to `config/`: a filter policy is a per-model
|
|
177
|
+
`defineFilterSpec` call in your own code, not global configuration. Apps that
|
|
178
|
+
wired those two entries by hand need no change — the codemod is idempotent.
|
|
179
|
+
|
|
180
|
+
- [#23](https://github.com/DavideCarvalho/adonis-agora-filter/pull/23) [`2bd4e7b`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/2bd4e7baa50d3d0b4a25c834fc9edf612f7d854a) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - `parseFilterRequest` now understands the client builder's structured shape, closing a silent unfiltered-response bug.
|
|
181
|
+
|
|
182
|
+
A POST search endpoint that hands `filterQuery()…build()` straight to
|
|
183
|
+
`parseFilterRequest` — the pairing the guides describe — did not work. The
|
|
184
|
+
builder returns
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
{ filter: { where: [{ field: 'status', operator: 'in', value: [...] }] },
|
|
188
|
+
sort: [{ field: 'createdAt', direction: 'desc' }],
|
|
189
|
+
paginate: { page: 2, size: 25 } }
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
and the parser reshaped it as if `where` were a column: the entire condition list
|
|
193
|
+
became one `in` filter on a field named `where`, which the allow-list then
|
|
194
|
+
pruned, while `sort` and `paginate` were discarded for not being strings. The
|
|
195
|
+
endpoint answered with **every row, unsorted and unpaginated**, and raised
|
|
196
|
+
nothing — the failure looked like a working search with a broad result set.
|
|
197
|
+
|
|
198
|
+
The same shape reaches a plain GET too, because OR/AND groups serialize to a
|
|
199
|
+
top-level `where[0][field]=…`, so grouped queries were silently unfiltered as
|
|
200
|
+
well.
|
|
201
|
+
|
|
202
|
+
`parseFilterRequest` now recognises an already-structured condition list — under
|
|
203
|
+
`filter.where` or at the top level — and takes it as the filters, with nested
|
|
204
|
+
`AND`/`OR` groups intact. It also reads `sort` in the `[{ field, direction }]`
|
|
205
|
+
form and maps `paginate: { page, size }` onto `page`/`size`.
|
|
206
|
+
|
|
207
|
+
Existing requests are unaffected: `filter[field]=…`, `sort=-createdAt`,
|
|
208
|
+
`page`/`size` and `page[number]`/`page[size]` parse exactly as before, and a real
|
|
209
|
+
column named `where` (`filter[where]=lobby`, `filter[where][contains]=lob`,
|
|
210
|
+
`filter[where][]=a&filter[where][]=b`) is still treated as a column — only an
|
|
211
|
+
array of `{ field, operator }` records is read as a structured list, and a query
|
|
212
|
+
string cannot produce one by accident.
|
|
213
|
+
|
|
214
|
+
Note that `include` is still not consumed: eager-loading stays the caller's
|
|
215
|
+
`preload` call.
|
|
216
|
+
|
|
217
|
+
### Patch Changes
|
|
218
|
+
|
|
219
|
+
- [#23](https://github.com/DavideCarvalho/adonis-agora-filter/pull/23) [`2bd4e7b`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/2bd4e7baa50d3d0b4a25c834fc9edf612f7d854a) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Report the real version from the exported `VERSION` constant.
|
|
220
|
+
|
|
221
|
+
`VERSION` is a hand-written literal next to a "keep in sync with package.json"
|
|
222
|
+
comment, and it had not been touched since the first release: the package shipped
|
|
223
|
+
`0.2.0` through `0.6.0` while `VERSION` still answered `'0.1.0'`. Anything gating
|
|
224
|
+
on it — a feature check, a bug report, a diagnostics banner — got a wrong answer.
|
|
225
|
+
|
|
226
|
+
It now reads `0.6.0`, and a test compares it against `package.json` so the next
|
|
227
|
+
release cannot silently drift again.
|
|
228
|
+
|
|
229
|
+
- [#22](https://github.com/DavideCarvalho/adonis-agora-filter/pull/22) [`a33b2fe`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/a33b2febc48baa3dafcf6a703e4250bdebbde275) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Fix a 500 when `distinct` is given a relation path.
|
|
230
|
+
|
|
231
|
+
`distinct` was gated on the `allowed` list and nothing else. A relation path like
|
|
232
|
+
`posts.title` is filterable by design — declaring `relations: { posts: { filterable: ['title'] } }`
|
|
233
|
+
whitelists it, and `make:filter-client` enumerates it into the generated field
|
|
234
|
+
union — so `?distinct=posts.title` cleared the check and was handed to Lucid
|
|
235
|
+
verbatim:
|
|
236
|
+
|
|
237
|
+
```sql
|
|
238
|
+
select distinct "posts"."title" from "users"
|
|
239
|
+
--> ERROR: missing FROM-clause entry for table "posts"
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Lucid filters a relation with a correlated `EXISTS` subquery, so the relation is
|
|
243
|
+
never joined into the outer `FROM` and there is no alias to project a column
|
|
244
|
+
from. A `whereHas` on the same relation in the same request does not help.
|
|
245
|
+
|
|
246
|
+
**If your `distinct` works today, nothing changes.** Root-table columns —
|
|
247
|
+
including ones qualified with the root table's own name — behave exactly as
|
|
248
|
+
before, as do the alias resolution and the allow-list drop.
|
|
249
|
+
|
|
250
|
+
**If you pass a relation path** (`posts.title`) or a to-many aggregate path
|
|
251
|
+
(`posts.$count`), you now get a defined refusal instead of a database error: the
|
|
252
|
+
field is dropped from the `distinct` list (the remaining fields still apply), or
|
|
253
|
+
raises `InvalidColumnFilterError` — a 400, not a 500 — when your spec sets
|
|
254
|
+
`throwOnInvalid`. The message names the cause: it is the missing join, not the
|
|
255
|
+
allow-list, so adding the path to `filterable` will not (and should not) change
|
|
256
|
+
it. Filtering on that path keeps working; only projecting it is refused.
|
|
257
|
+
|
|
258
|
+
- [#23](https://github.com/DavideCarvalho/adonis-agora-filter/pull/23) [`2bd4e7b`](https://github.com/DavideCarvalho/adonis-agora-filter/commit/2bd4e7baa50d3d0b4a25c834fc9edf612f7d854a) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Declare a supported Node range in `engines` again, instead of one exact version.
|
|
259
|
+
|
|
260
|
+
Both packages shipped `"engines": { "node": "v26.7.0" }` — an exact version, and
|
|
261
|
+
plainly the output of `node -v`, leading `v` and all. Every consumer on any other
|
|
262
|
+
Node got an unsatisfied-engine warning on install, and anyone running with
|
|
263
|
+
`engine-strict` (or a package manager that treats it as fatal) could not install
|
|
264
|
+
at all. The pinned version was also higher than anything the project itself uses:
|
|
265
|
+
CI runs Node 22 and `.nvmrc` names Node 20.
|
|
266
|
+
|
|
267
|
+
`engines` states the floor the package actually supports, which is `>=20.6.0` —
|
|
268
|
+
the same range it declared before, and the one the rest of the Agora packages
|
|
269
|
+
use. Nothing about the code changed; this only stops a false incompatibility
|
|
270
|
+
signal.
|
|
271
|
+
|
|
272
|
+
## 0.6.0
|
|
273
|
+
|
|
274
|
+
### Minor Changes
|
|
275
|
+
|
|
276
|
+
- Parity sync from nestjs-filter: execute the server-side `distinct` projection (was a silent no-op), computed (virtual) fields for filter + sort (verbatim-string + `({alias}) => sql` forms), and native to-many aggregate fields (`$count`/`$sum`/`$avg`/`$min`/`$max`) auto-discovered from Lucid relation metadata — value stays parameterized (injection-safe), identifiers quoted.
|
|
277
|
+
|
|
278
|
+
## 0.5.0
|
|
279
|
+
|
|
280
|
+
### Minor Changes
|
|
281
|
+
|
|
282
|
+
- [#6](https://github.com/DavideCarvalho/adonis-filter/pull/6) [`e39da2c`](https://github.com/DavideCarvalho/adonis-filter/commit/e39da2c6f7e47553990f3295414b23d508894c9a) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Os macros `query.applyFilterFromRequest(spec, ctx?)` e `query.filterPaginate(spec, ctx?)` passam a aceitar `ctx` opcional: quando omitido, leem o `HttpContext` ativo do AsyncLocalStorage do Adonis (`HttpContext.getOrFail()`). Nos controllers (99% dos casos) você chama `query.applyFilterFromRequest(spec)` sem passar o ctx. Fora de uma request (job/command), passe o ctx explicitamente. O default vive só no macro (camada Adonis); a função livre `applyFilterFromRequest` continua framework-agnostic, exigindo o ctx.
|
|
283
|
+
|
|
284
|
+
## 0.4.0
|
|
285
|
+
|
|
286
|
+
### Minor Changes
|
|
287
|
+
|
|
288
|
+
- [#4](https://github.com/DavideCarvalho/adonis-filter/pull/4) [`76b2ca8`](https://github.com/DavideCarvalho/adonis-filter/commit/76b2ca82d27811b09dbd98629be6964e9aee6167) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Add chainable Lucid query-builder macros via an optional `FilterProvider`
|
|
289
|
+
|
|
290
|
+
Register `@adonis-agora/filter/filter_provider` to get the method-call form of
|
|
291
|
+
`applyFilterFromRequest` on any Lucid query builder:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
// filter + sort + search, then keep chaining:
|
|
295
|
+
const rows = await User.query()
|
|
296
|
+
.where("tenantId", tenant.id)
|
|
297
|
+
.applyFilterFromRequest(userFilter, ctx)
|
|
298
|
+
.orderBy("createdAt", "desc");
|
|
299
|
+
|
|
300
|
+
// filter + paginate in one terminal call (returns Lucid's paginator):
|
|
301
|
+
const page = await User.query().filterPaginate(userFilter, ctx);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`applyFilterFromRequest` applies the spec's server scope + allow-listed
|
|
305
|
+
filter/sort/search and returns the query for chaining (pagination resolved but
|
|
306
|
+
not applied); `filterPaginate` additionally calls `paginate(page, size)`. The
|
|
307
|
+
free functions are unchanged and work without the provider — the macros only add
|
|
308
|
+
the chainable sugar, so `@adonisjs/lucid` is an optional peer. `registerFilterMacros`
|
|
309
|
+
is also exported for manual registration.
|
|
310
|
+
|
|
311
|
+
## 0.3.1
|
|
312
|
+
|
|
313
|
+
### Patch Changes
|
|
314
|
+
|
|
315
|
+
- [`e6fb05c`](https://github.com/DavideCarvalho/adonis-filter/commit/e6fb05c0e137cd4ab3bad1f9ce5626216e670a8f) - Fix `QueryBuilderLike` rejecting every real Lucid query builder
|
|
316
|
+
|
|
317
|
+
Passing a Lucid builder to `applyFilterFromRequest` (or any adapter entry
|
|
318
|
+
point) failed to typecheck in consuming apps:
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
Argument of type 'ModelQueryBuilderContract<typeof Post, Post>' is not
|
|
322
|
+
assignable to parameter of type 'QueryBuilderLike'.
|
|
323
|
+
Types of property 'where' are incompatible.
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The message blames `where`, but `where` was fine — TS reports the first member
|
|
327
|
+
it tries. The real culprit was `whereHas`, declared here as
|
|
328
|
+
`whereHas(relation: string, ...)`. Lucid types its own as
|
|
329
|
+
`<Name extends ExtractModelRelations<Model>>(relation: Name, ...)`, a union of
|
|
330
|
+
the model's literal relation names, and `string` is not assignable to that
|
|
331
|
+
union under the contravariant parameter check — so no real builder ever
|
|
332
|
+
satisfied the interface. Runtime was always fine; this was types-only.
|
|
333
|
+
|
|
334
|
+
`relation` is now `any`, which is the only type that both accepts the `string`
|
|
335
|
+
the adapter passes and is assignable to each model's relation-name union.
|
|
336
|
+
Marking the member optional does not help: an optional member that is present
|
|
337
|
+
is still checked.
|
|
338
|
+
|
|
339
|
+
A compile-time guard against real `@adonisjs/lucid` types now covers this
|
|
340
|
+
(`test/types/lucid_compat.types.ts`, run by `pnpm typecheck`). Lucid is a
|
|
341
|
+
devDependency only — nothing under `src/` imports it, so the package stays
|
|
342
|
+
framework-free. A hand-transcribed stub of Lucid's types was tried first and
|
|
343
|
+
compiled clean while real Lucid did not, so the guard uses Lucid's own `.d.ts`.
|
|
344
|
+
|
|
345
|
+
## 0.3.0
|
|
346
|
+
|
|
347
|
+
### Minor Changes
|
|
348
|
+
|
|
349
|
+
- [`d20245c`](https://github.com/DavideCarvalho/adonis-filter/commit/d20245cc6818120098d0f9027b59284380fd9f7e) - `filterable` accepts a colocated map: field name and its kind in one place
|
|
350
|
+
|
|
351
|
+
The array form makes every non-string field appear twice — once in `filterable`, once in
|
|
352
|
+
`fieldTypes` — which is ceremony for what is usually a short list:
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
filterable: ['advisorId', 'dayOfWeek', 'isRecurring'],
|
|
356
|
+
fieldTypes: { dayOfWeek: { kind: 'number' }, isRecurring: { kind: 'boolean' } },
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
`filterable` now also accepts a map, declaring both at once:
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
filterable: { advisorId: 'string', dayOfWeek: 'number', isRecurring: 'boolean' },
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
It desugars at the `defineFilter` boundary — the keys become the allow-list, the values become
|
|
366
|
+
`fieldTypes` — so everything downstream (predicates, runner, codegen) sees exactly the spec the
|
|
367
|
+
array form produces. An explicit `fieldTypes` entry still wins per field, which is how a caller
|
|
368
|
+
adds codegen-only richness (`enumValues`/`typeRef`) on top of a bare kind.
|
|
369
|
+
|
|
370
|
+
Both existing forms (`string[]` and `'*'`) are untouched and remain the right choice when no field
|
|
371
|
+
needs a declared type — `'string'` is the no-op kind, so a spec of only string columns should keep
|
|
372
|
+
using the array.
|
|
373
|
+
|
|
374
|
+
## 0.2.0
|
|
375
|
+
|
|
376
|
+
### Minor Changes
|
|
377
|
+
|
|
378
|
+
- [`058f0c0`](https://github.com/DavideCarvalho/adonis-filter/commit/058f0c0ef3dd224277663e6a5d40c0ef58e6bbd7) - `fieldTypes` on `defineFilter`: server-side value validation, and one type declaration for both ends
|
|
379
|
+
|
|
380
|
+
A filter value arriving over a query string is always a string, and Postgres implicitly casts the
|
|
381
|
+
benign cases — `day_of_week = '3'` and `is_recurring = 'false'` both work — so the gap stayed
|
|
382
|
+
invisible. It surfaces when a client sends something uncastable: `?filter[isRecurring][equals]=xyz`
|
|
383
|
+
becomes `is_recurring = 'xyz'`, which Postgres rejects with `invalid input syntax for type boolean`.
|
|
384
|
+
That is a **500 on a public endpoint, driven entirely by user input**. The allow-list guarded which
|
|
385
|
+
FIELD could be filtered; nothing guarded the VALUE that reached the column.
|
|
386
|
+
|
|
387
|
+
`defineFilter` now accepts `fieldTypes`, and a declared field has its value coerced before it ever
|
|
388
|
+
reaches the driver. An uncoercible value is treated exactly like a disallowed field — dropped by
|
|
389
|
+
default, or a loud `InvalidColumnFilterError` (→ 400 instead of 500) under `throwOnInvalid`. The
|
|
390
|
+
existing semantics are reused rather than a second error path invented.
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
export const availabilityFilter = defineFilter({
|
|
394
|
+
filterable: ["advisorId", "dayOfWeek", "isRecurring"],
|
|
395
|
+
fieldTypes: {
|
|
396
|
+
dayOfWeek: { kind: "number" },
|
|
397
|
+
isRecurring: { kind: "boolean" },
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
The same declaration now also feeds `make:filter-client`, which previously required repeating the
|
|
403
|
+
types in the codegen manifest. Declaring a kind once drives both value coercion and the client's
|
|
404
|
+
operator narrowing; an explicit manifest `fieldTypes` still wins when the client wants richer
|
|
405
|
+
codegen-only info (`enumValues`/`typeRef`).
|
|
406
|
+
|
|
407
|
+
Details:
|
|
408
|
+
|
|
409
|
+
- Array-valued operators (`in`, `between`, ...) coerce element-wise and fail as a whole if any
|
|
410
|
+
element fails — a partially-coerced list would filter on something the client never asked for.
|
|
411
|
+
- Pattern operators (`contains`, `startsWith`, ...) are never coerced: their argument is a LIKE
|
|
412
|
+
pattern, so turning `contains: '3'` into the number `3` would destroy it.
|
|
413
|
+
- `date` values are validated but handed back verbatim, never rewritten — converting `'2026-07-15'`
|
|
414
|
+
to a `Date` would silently re-zone a date-only value and shift the day for negative-offset clients.
|
|
415
|
+
- Undeclared fields are untouched, so this is backwards compatible and opt-in.
|
|
416
|
+
- `FilterFieldKind` moved from `generate_client.ts` to `types.ts` (it is no longer codegen-only) and
|
|
417
|
+
is re-exported from its old path.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Davi Carvalho
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# `@adonis-agora/filter`
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@adonis-agora/filter) [](https://www.npmjs.com/package/@adonis-agora/filter) [](https://github.com/DavideCarvalho/adonis-agora-filter/actions/workflows/ci.yml) [](https://www.npmjs.com/package/@adonis-agora/filter)
|
|
4
|
+
|
|
5
|
+
Server-side query filtering, sorting, and pagination for AdonisJS. Parses the
|
|
6
|
+
Spatie/JSON:API query the `@adonis-agora/filter-client` builder emits, applies it to a
|
|
7
|
+
Lucid query through a field allow-list, and resolves offset or keyset (cursor)
|
|
8
|
+
pagination.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { parseFilterRequest, applyFilter } from '@adonis-agora/filter'
|
|
12
|
+
|
|
13
|
+
const input = parseFilterRequest(ctx.request.qs())
|
|
14
|
+
const query = User.query()
|
|
15
|
+
const { page, size } = applyFilter(query, input, {
|
|
16
|
+
allowed: ['name', 'email', 'age'],
|
|
17
|
+
searchable: ['name', 'email'],
|
|
18
|
+
})
|
|
19
|
+
return query.paginate(page, size)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Chainable macros (optional)
|
|
23
|
+
|
|
24
|
+
Register the `FilterProvider` to get the method-call form of
|
|
25
|
+
`applyFilterFromRequest` on any Lucid query builder — the same operation, but
|
|
26
|
+
chainable:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// adonisrc.ts → providers
|
|
30
|
+
() => import('@adonis-agora/filter/filter_provider')
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// filter + sort + search, then keep chaining:
|
|
35
|
+
const rows = await User.query()
|
|
36
|
+
.where('tenantId', tenant.id)
|
|
37
|
+
.applyFilterFromRequest(userFilter, ctx)
|
|
38
|
+
.orderBy('createdAt', 'desc')
|
|
39
|
+
|
|
40
|
+
// filter + paginate in one terminal call (returns Lucid's paginator):
|
|
41
|
+
const page = await User.query().filterPaginate(userFilter, ctx)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`applyFilterFromRequest` returns the query (pagination resolved but not applied);
|
|
45
|
+
`filterPaginate` pages it with the resolved values, returning Lucid's paginator — it
|
|
46
|
+
reads the page first and skips the `COUNT(*)` when the page comes back short. The free
|
|
47
|
+
functions work without the provider — the macros only add the chainable sugar, so
|
|
48
|
+
`@adonisjs/lucid` stays an optional peer.
|
|
49
|
+
|
|
50
|
+
Operators: equals/notEquals, contains/iContains/startsWith/endsWith, gt/gte/lt/lte,
|
|
51
|
+
between/notBetween, in/notIn/isAnyOf, isNull/isNotNull/isEmpty/isNotEmpty, plus
|
|
52
|
+
AND/OR composition. The `allowed`/`sortable`/`searchable` lists are the security
|
|
53
|
+
boundary. The Lucid binding is structural (`QueryBuilderLike`) so any Lucid query
|
|
54
|
+
builder works and the adapter is unit-testable.
|
|
55
|
+
|
|
56
|
+
### In-memory lists
|
|
57
|
+
|
|
58
|
+
`@adonis-agora/filter/in-memory` runs the same input against a plain array — an
|
|
59
|
+
in-memory `QueryBuilderLike` the runner drives exactly like a Lucid query:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { defineCollection, filterInMemory } from '@adonis-agora/filter/in-memory'
|
|
63
|
+
|
|
64
|
+
const users = defineCollection<UserRow>({
|
|
65
|
+
fields: { name: 'string', age: 'number', fullName: { type: 'string', get: (u) => u.first + ' ' + u.last } },
|
|
66
|
+
})
|
|
67
|
+
const page = filterInMemory(users, rows, input, { allowed: ['name', 'age', 'fullName'] })
|
|
68
|
+
// { data, meta: { total, perPage, currentPage, lastPage, firstPage } }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
See the [repository README](https://github.com/DavideCarvalho/adonis-filter).
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT © Davi Carvalho
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ListLoader } from '@adonisjs/core/ace';
|
|
2
|
+
import MakeFilter from './make_filter.js';
|
|
3
|
+
import MakeFilterClient from './make_filter_client.js';
|
|
4
|
+
/**
|
|
5
|
+
* The commands barrel for `@adonis-agora/filter`. An app registers it in its
|
|
6
|
+
* `adonisrc` via `rcFile.addCommand('@adonis-agora/filter/commands')`. A
|
|
7
|
+
* {@link ListLoader} exposes the `make:filter-client` command's metadata and
|
|
8
|
+
* constructors to the ace kernel: `make:filter` (scaffold a filter class) and
|
|
9
|
+
* `make:filter-client` (emit the typed front-end builders).
|
|
10
|
+
*
|
|
11
|
+
* `@adonisjs/core` is an *optional* peer of this otherwise framework-free core —
|
|
12
|
+
* only this `./commands` subpath imports it, so the main entrypoint stays
|
|
13
|
+
* dependency-free.
|
|
14
|
+
*/
|
|
15
|
+
declare const loader: ListLoader<typeof MakeFilter | typeof MakeFilterClient>;
|
|
16
|
+
export declare const getMetaData: () => Promise<import("@adonisjs/core/types/ace").CommandMetaData[]>;
|
|
17
|
+
export declare const getCommand: (metaData: import("@adonisjs/core/types/ace").CommandMetaData) => Promise<typeof MakeFilter | typeof MakeFilterClient | null>;
|
|
18
|
+
export default loader;
|
|
19
|
+
//# sourceMappingURL=main.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../commands/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,UAAU,MAAM,kBAAkB,CAAC;AAC1C,OAAO,gBAAgB,MAAM,yBAAyB,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,QAAA,MAAM,MAAM,yDAAiD,CAAC;AAE9D,eAAO,MAAM,WAAW,qEAAkC,CAAC;AAC3D,eAAO,MAAM,UAAU,+HAAiC,CAAC;eAE1C,MAAM"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ListLoader } from '@adonisjs/core/ace';
|
|
2
|
+
import MakeFilter from './make_filter.js';
|
|
3
|
+
import MakeFilterClient from './make_filter_client.js';
|
|
4
|
+
/**
|
|
5
|
+
* The commands barrel for `@adonis-agora/filter`. An app registers it in its
|
|
6
|
+
* `adonisrc` via `rcFile.addCommand('@adonis-agora/filter/commands')`. A
|
|
7
|
+
* {@link ListLoader} exposes the `make:filter-client` command's metadata and
|
|
8
|
+
* constructors to the ace kernel: `make:filter` (scaffold a filter class) and
|
|
9
|
+
* `make:filter-client` (emit the typed front-end builders).
|
|
10
|
+
*
|
|
11
|
+
* `@adonisjs/core` is an *optional* peer of this otherwise framework-free core —
|
|
12
|
+
* only this `./commands` subpath imports it, so the main entrypoint stays
|
|
13
|
+
* dependency-free.
|
|
14
|
+
*/
|
|
15
|
+
const loader = new ListLoader([MakeFilter, MakeFilterClient]);
|
|
16
|
+
export const getMetaData = loader.getMetaData.bind(loader);
|
|
17
|
+
export const getCommand = loader.getCommand.bind(loader);
|
|
18
|
+
export default loader;
|
|
19
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../commands/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,UAAU,MAAM,kBAAkB,CAAC;AAC1C,OAAO,gBAAgB,MAAM,yBAAyB,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC;AAE9D,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC3D,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAEzD,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BaseCommand } from '@adonisjs/core/ace';
|
|
2
|
+
import type { CommandOptions } from '@adonisjs/core/types/ace';
|
|
3
|
+
/**
|
|
4
|
+
* `node ace make:filter <name>` — scaffold a filter class under `app/filters/`.
|
|
5
|
+
*
|
|
6
|
+
* The generated class extends `BaseModelFilter`: allow-lists as statics for the plain columns, a
|
|
7
|
+
* `setup()` for the scope the client cannot relax, and a method per key that needs SQL of its own.
|
|
8
|
+
* It is `@inject()`ed, so a constructor dependency resolves through the container exactly as a
|
|
9
|
+
* controller's does.
|
|
10
|
+
*/
|
|
11
|
+
export default class MakeFilter extends BaseCommand {
|
|
12
|
+
static commandName: string;
|
|
13
|
+
static description: string;
|
|
14
|
+
static options: CommandOptions;
|
|
15
|
+
name: string;
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=make_filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"make_filter.d.ts","sourceRoot":"","sources":["../../commands/make_filter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/D;;;;;;;GAOG;AACH,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,WAAW;IACjD,OAAgB,WAAW,SAAiB;IAC5C,OAAgB,WAAW,SAA+B;IAC1D,OAAgB,OAAO,EAAE,cAAc,CAA+B;IAG9D,IAAI,EAAE,MAAM,CAAC;IAEN,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAMlC;CACF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { args, BaseCommand } from '@adonisjs/core/ace';
|
|
8
|
+
import { stubsRoot } from '../stubs/main.js';
|
|
9
|
+
/**
|
|
10
|
+
* `node ace make:filter <name>` — scaffold a filter class under `app/filters/`.
|
|
11
|
+
*
|
|
12
|
+
* The generated class extends `BaseModelFilter`: allow-lists as statics for the plain columns, a
|
|
13
|
+
* `setup()` for the scope the client cannot relax, and a method per key that needs SQL of its own.
|
|
14
|
+
* It is `@inject()`ed, so a constructor dependency resolves through the container exactly as a
|
|
15
|
+
* controller's does.
|
|
16
|
+
*/
|
|
17
|
+
export default class MakeFilter extends BaseCommand {
|
|
18
|
+
static commandName = 'make:filter';
|
|
19
|
+
static description = 'Create a new filter class';
|
|
20
|
+
static options = { allowUnknownFlags: true };
|
|
21
|
+
async run() {
|
|
22
|
+
const codemods = await this.createCodemods();
|
|
23
|
+
await codemods.makeUsingStub(stubsRoot, 'make/filter/main.stub', {
|
|
24
|
+
flags: this.parsed.flags,
|
|
25
|
+
entity: this.app.generators.createEntity(this.name),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
__decorate([
|
|
30
|
+
args.string({ description: 'Name of the filter (e.g. user)' })
|
|
31
|
+
], MakeFilter.prototype, "name", void 0);
|
|
32
|
+
//# sourceMappingURL=make_filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"make_filter.js","sourceRoot":"","sources":["../../commands/make_filter.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C;;;;;;;GAOG;AACH,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,WAAW;IACjD,MAAM,CAAU,WAAW,GAAG,aAAa,CAAC;IAC5C,MAAM,CAAU,WAAW,GAAG,2BAA2B,CAAC;IAC1D,MAAM,CAAU,OAAO,GAAmB,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC;IAK7D,KAAK,CAAC,GAAG;QAChB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;QAC7C,MAAM,QAAQ,CAAC,aAAa,CAAC,SAAS,EAAE,uBAAuB,EAAE;YAC/D,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK;YACxB,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;SACpD,CAAC,CAAC;IACL,CAAC;CACF;AATS;IADP,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,gCAAgC,EAAE,CAAC;wCAC1C"}
|