@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.
Files changed (168) hide show
  1. package/CHANGELOG.md +417 -0
  2. package/LICENSE +21 -0
  3. package/README.md +75 -0
  4. package/dist/commands/main.d.ts +19 -0
  5. package/dist/commands/main.d.ts.map +1 -0
  6. package/dist/commands/main.js +19 -0
  7. package/dist/commands/main.js.map +1 -0
  8. package/dist/commands/make_filter.d.ts +18 -0
  9. package/dist/commands/make_filter.d.ts.map +1 -0
  10. package/dist/commands/make_filter.js +32 -0
  11. package/dist/commands/make_filter.js.map +1 -0
  12. package/dist/commands/make_filter_client.d.ts +35 -0
  13. package/dist/commands/make_filter_client.d.ts.map +1 -0
  14. package/dist/commands/make_filter_client.js +85 -0
  15. package/dist/commands/make_filter_client.js.map +1 -0
  16. package/dist/configure.d.ts +15 -0
  17. package/dist/configure.d.ts.map +1 -0
  18. package/dist/configure.js +20 -0
  19. package/dist/configure.js.map +1 -0
  20. package/dist/providers/filter_provider.d.ts +28 -0
  21. package/dist/providers/filter_provider.d.ts.map +1 -0
  22. package/dist/providers/filter_provider.js +33 -0
  23. package/dist/providers/filter_provider.js.map +1 -0
  24. package/dist/src/aggregate.d.ts +70 -0
  25. package/dist/src/aggregate.d.ts.map +1 -0
  26. package/dist/src/aggregate.js +195 -0
  27. package/dist/src/aggregate.js.map +1 -0
  28. package/dist/src/aggregate_path.d.ts +25 -0
  29. package/dist/src/aggregate_path.d.ts.map +1 -0
  30. package/dist/src/aggregate_path.js +35 -0
  31. package/dist/src/aggregate_path.js.map +1 -0
  32. package/dist/src/apply_from_request.d.ts +85 -0
  33. package/dist/src/apply_from_request.d.ts.map +1 -0
  34. package/dist/src/apply_from_request.js +164 -0
  35. package/dist/src/apply_from_request.js.map +1 -0
  36. package/dist/src/base_filter.d.ts +49 -0
  37. package/dist/src/base_filter.d.ts.map +1 -0
  38. package/dist/src/base_filter.js +33 -0
  39. package/dist/src/base_filter.js.map +1 -0
  40. package/dist/src/base_model_filter.d.ts +90 -0
  41. package/dist/src/base_model_filter.d.ts.map +1 -0
  42. package/dist/src/base_model_filter.js +85 -0
  43. package/dist/src/base_model_filter.js.map +1 -0
  44. package/dist/src/coerce_value.d.ts +26 -0
  45. package/dist/src/coerce_value.d.ts.map +1 -0
  46. package/dist/src/coerce_value.js +82 -0
  47. package/dist/src/coerce_value.js.map +1 -0
  48. package/dist/src/cursor.d.ts +92 -0
  49. package/dist/src/cursor.d.ts.map +1 -0
  50. package/dist/src/cursor.js +107 -0
  51. package/dist/src/cursor.js.map +1 -0
  52. package/dist/src/custom_filter.d.ts +36 -0
  53. package/dist/src/custom_filter.d.ts.map +1 -0
  54. package/dist/src/custom_filter.js +127 -0
  55. package/dist/src/custom_filter.js.map +1 -0
  56. package/dist/src/decorator_metadata.d.ts +41 -0
  57. package/dist/src/decorator_metadata.d.ts.map +1 -0
  58. package/dist/src/decorator_metadata.js +83 -0
  59. package/dist/src/decorator_metadata.js.map +1 -0
  60. package/dist/src/decorators.d.ts +73 -0
  61. package/dist/src/decorators.d.ts.map +1 -0
  62. package/dist/src/decorators.js +118 -0
  63. package/dist/src/decorators.js.map +1 -0
  64. package/dist/src/escape-like.d.ts +10 -0
  65. package/dist/src/escape-like.d.ts.map +1 -0
  66. package/dist/src/escape-like.js +15 -0
  67. package/dist/src/escape-like.js.map +1 -0
  68. package/dist/src/field_aliases.d.ts +30 -0
  69. package/dist/src/field_aliases.d.ts.map +1 -0
  70. package/dist/src/field_aliases.js +65 -0
  71. package/dist/src/field_aliases.js.map +1 -0
  72. package/dist/src/filter_class.d.ts +46 -0
  73. package/dist/src/filter_class.d.ts.map +1 -0
  74. package/dist/src/filter_class.js +244 -0
  75. package/dist/src/filter_class.js.map +1 -0
  76. package/dist/src/filter_spec.d.ts +231 -0
  77. package/dist/src/filter_spec.d.ts.map +1 -0
  78. package/dist/src/filter_spec.js +182 -0
  79. package/dist/src/filter_spec.js.map +1 -0
  80. package/dist/src/filterable_mixin.d.ts +49 -0
  81. package/dist/src/filterable_mixin.d.ts.map +1 -0
  82. package/dist/src/filterable_mixin.js +47 -0
  83. package/dist/src/filterable_mixin.js.map +1 -0
  84. package/dist/src/generate_client.d.ts +107 -0
  85. package/dist/src/generate_client.d.ts.map +1 -0
  86. package/dist/src/generate_client.js +164 -0
  87. package/dist/src/generate_client.js.map +1 -0
  88. package/dist/src/group_by_count.d.ts +67 -0
  89. package/dist/src/group_by_count.d.ts.map +1 -0
  90. package/dist/src/group_by_count.js +71 -0
  91. package/dist/src/group_by_count.js.map +1 -0
  92. package/dist/src/in_memory.d.ts +279 -0
  93. package/dist/src/in_memory.d.ts.map +1 -0
  94. package/dist/src/in_memory.js +810 -0
  95. package/dist/src/in_memory.js.map +1 -0
  96. package/dist/src/index.d.ts +51 -0
  97. package/dist/src/index.d.ts.map +1 -0
  98. package/dist/src/index.js +33 -0
  99. package/dist/src/index.js.map +1 -0
  100. package/dist/src/lucid_adapter.d.ts +311 -0
  101. package/dist/src/lucid_adapter.d.ts.map +1 -0
  102. package/dist/src/lucid_adapter.js +469 -0
  103. package/dist/src/lucid_adapter.js.map +1 -0
  104. package/dist/src/lucid_macros.d.ts +87 -0
  105. package/dist/src/lucid_macros.d.ts.map +1 -0
  106. package/dist/src/lucid_macros.js +67 -0
  107. package/dist/src/lucid_macros.js.map +1 -0
  108. package/dist/src/normalizer.d.ts +21 -0
  109. package/dist/src/normalizer.d.ts.map +1 -0
  110. package/dist/src/normalizer.js +61 -0
  111. package/dist/src/normalizer.js.map +1 -0
  112. package/dist/src/operators.d.ts +37 -0
  113. package/dist/src/operators.d.ts.map +1 -0
  114. package/dist/src/operators.js +43 -0
  115. package/dist/src/operators.js.map +1 -0
  116. package/dist/src/paginate.d.ts +12 -0
  117. package/dist/src/paginate.d.ts.map +1 -0
  118. package/dist/src/paginate.js +88 -0
  119. package/dist/src/paginate.js.map +1 -0
  120. package/dist/src/pagination_stash.d.ts +4 -0
  121. package/dist/src/pagination_stash.d.ts.map +1 -0
  122. package/dist/src/pagination_stash.js +19 -0
  123. package/dist/src/pagination_stash.js.map +1 -0
  124. package/dist/src/parse_request.d.ts +50 -0
  125. package/dist/src/parse_request.d.ts.map +1 -0
  126. package/dist/src/parse_request.js +220 -0
  127. package/dist/src/parse_request.js.map +1 -0
  128. package/dist/src/runner.d.ts +65 -0
  129. package/dist/src/runner.d.ts.map +1 -0
  130. package/dist/src/runner.js +371 -0
  131. package/dist/src/runner.js.map +1 -0
  132. package/dist/src/source_resolver.d.ts +18 -0
  133. package/dist/src/source_resolver.d.ts.map +1 -0
  134. package/dist/src/source_resolver.js +64 -0
  135. package/dist/src/source_resolver.js.map +1 -0
  136. package/dist/src/spatie_parser.d.ts +39 -0
  137. package/dist/src/spatie_parser.d.ts.map +1 -0
  138. package/dist/src/spatie_parser.js +134 -0
  139. package/dist/src/spatie_parser.js.map +1 -0
  140. package/dist/src/testing.d.ts +73 -0
  141. package/dist/src/testing.d.ts.map +1 -0
  142. package/dist/src/testing.js +136 -0
  143. package/dist/src/testing.js.map +1 -0
  144. package/dist/src/types.d.ts +226 -0
  145. package/dist/src/types.d.ts.map +1 -0
  146. package/dist/src/types.js +2 -0
  147. package/dist/src/types.js.map +1 -0
  148. package/dist/src/validate-column-filter.d.ts +30 -0
  149. package/dist/src/validate-column-filter.d.ts.map +1 -0
  150. package/dist/src/validate-column-filter.js +148 -0
  151. package/dist/src/validate-column-filter.js.map +1 -0
  152. package/dist/src/value-shape.d.ts +18 -0
  153. package/dist/src/value-shape.d.ts.map +1 -0
  154. package/dist/src/value-shape.js +36 -0
  155. package/dist/src/value-shape.js.map +1 -0
  156. package/dist/stubs/main.d.ts +3 -0
  157. package/dist/stubs/main.d.ts.map +1 -0
  158. package/dist/stubs/main.js +4 -0
  159. package/dist/stubs/main.js.map +1 -0
  160. package/dist/stubs/make/filter/main.stub +45 -0
  161. package/package.json +102 -0
  162. package/skills/filter-basics/SKILL.md +285 -0
  163. package/skills/filter-codegen/SKILL.md +246 -0
  164. package/skills/filter-definitions/SKILL.md +340 -0
  165. package/skills/filter-querying/SKILL.md +319 -0
  166. package/skills/filter-safety/SKILL.md +304 -0
  167. package/stubs/main.ts +4 -0
  168. package/stubs/make/filter/main.stub +45 -0
@@ -0,0 +1,231 @@
1
+ import { type LucidModelLike } from './aggregate.js';
2
+ import type { FieldAliases } from './field_aliases.js';
3
+ import type { FilterFieldTypeInfo } from './generate_client.js';
4
+ import type { ColumnFilter } from './operators.js';
5
+ import type { ComputedFields, FilterConfig, FilterFieldKind, FullTextSearchConfig, SortItem, VectorSimilarityConfig } from './types.js';
6
+ /**
7
+ * Thrown when a {@link defineFilter} declaration is itself invalid (a developer
8
+ * error surfaced at wiring time, not a bad client request). Request-time
9
+ * violations (a disallowed field under `throwOnInvalid`) still surface as
10
+ * `InvalidColumnFilterError` from the runner — this is the one new typed error,
11
+ * for the declaration boundary the NestJS `@Filterable` decorator guarded.
12
+ */
13
+ export declare class FilterDefinitionError extends Error {
14
+ constructor(message: string);
15
+ }
16
+ /** Column-level allow-list for a relation: `'*'` (any column) or a bare-name list. */
17
+ export type RelationColumns = string[] | '*';
18
+ /**
19
+ * The colocated allow-list form: field name → its column kind, declaring both at once.
20
+ *
21
+ * The array form makes every non-string field appear twice (once in `filterable`, once in
22
+ * `fieldTypes`); this states each field and its type in one place, and desugars to exactly that
23
+ * pair. Fields whose kind carries no contract can still use `'string'` — it is the no-op kind.
24
+ *
25
+ * ```ts
26
+ * defineFilter({ filterable: { advisorId: 'string', dayOfWeek: 'number', isRecurring: 'boolean' } })
27
+ * ```
28
+ */
29
+ export type FilterableMap = Record<string, FilterFieldKind>;
30
+ /** `filterable` as accepted on input: the classic list/`'*'`, or the colocated map. */
31
+ export type FilterableInput = RelationColumns | FilterableMap;
32
+ /**
33
+ * The filterable/sortable declaration for one whitelisted relation, keyed by the
34
+ * relation name on the owning model. Column names are **bare** (unprefixed) — the
35
+ * request field is the dotted path `relation.column`. Nesting `relations` again
36
+ * whitelists deeper paths (`relation.child.column`), bounded by `maxDepth`.
37
+ */
38
+ export interface RelationSpec {
39
+ /** Columns of the related model clients may filter on. Defaults to `'*'`. */
40
+ filterable?: RelationColumns;
41
+ /** Columns of the related model clients may sort on. Defaults to `filterable`. */
42
+ sortable?: RelationColumns;
43
+ /** Further whitelisted relations reachable from this one (one hop deeper). */
44
+ relations?: Record<string, RelationSpec>;
45
+ /**
46
+ * Numeric child columns exposed to the to-many aggregate functions
47
+ * (`$sum`/`$avg`/`$min`/`$max`) — e.g. `['views', 'total']` makes
48
+ * `posts.$sum.views` and `posts.$max.total` filterable/sortable. `$count`
49
+ * needs no column and is synthesised for every to-many relation regardless.
50
+ *
51
+ * Because Lucid does not reflect a column's SQL type, listing a column here is
52
+ * how the developer asserts it is numeric (the aggregate allow-list). Only
53
+ * meaningful when the owning {@link DefineFilterOptions.model} is set so the
54
+ * relation's FK/pivot metadata can be discovered; ignored otherwise.
55
+ */
56
+ aggregates?: string[];
57
+ }
58
+ /**
59
+ * Reads the current tenant id from the request context (the AdonisJS
60
+ * `HttpContext`, or any object the caller passes). A `null`/`undefined` result
61
+ * means "no tenant in context" — tenant scoping is then skipped (opt-in, exactly
62
+ * like the NestJS `@TenantScoped` no-op when no tenant resolves).
63
+ */
64
+ export type TenantResolver = (ctx: unknown) => string | number | null | undefined;
65
+ /** Tenant auto-scope: constrain `column` to the tenant id resolved from ctx. */
66
+ export interface TenantScopeSpec {
67
+ /** The model column constrained to the current tenant id (e.g. `'tenantId'`). */
68
+ column: string;
69
+ /** Resolves the tenant id from the request ctx. Nullish → scope is skipped. */
70
+ resolve: TenantResolver;
71
+ }
72
+ /**
73
+ * The declarative filter definition — the idiomatic AdonisJS reimplementation of
74
+ * everything the NestJS `@Filterable`/`@Relations`/`@TenantScoped` decorators
75
+ * encoded, as a plain options object (no decorators, no metadata reflection).
76
+ */
77
+ export interface DefineFilterOptions {
78
+ /**
79
+ * Columns clients may filter on. Three forms:
80
+ * - `['a', 'b']` — a bare allow-list.
81
+ * - `'*'` — any base column (use with care).
82
+ * - `{ a: 'string', b: 'number' }` — the colocated form: allow-list AND {@link
83
+ * DefineFilterOptions.fieldTypes} in one place, so a non-string field is not written twice.
84
+ */
85
+ filterable: FilterableInput;
86
+ /** Columns clients may sort on. Defaults to {@link DefineFilterOptions.filterable}. */
87
+ sortable?: RelationColumns;
88
+ /**
89
+ * Columns the free-text `search` term scans with a portable ILIKE — the
90
+ * default search path when {@link DefineFilterOptions.fullText} is not set.
91
+ */
92
+ searchable?: string[];
93
+ /**
94
+ * Per-field column value types. Declaring a field here does two things at once:
95
+ *
96
+ * 1. **Server-side validation.** A query-string filter value is always a string, and Postgres
97
+ * silently casts the benign cases (`day_of_week = '3'` works) — so the gap stays invisible
98
+ * until a client sends something uncastable (`is_recurring = 'xyz'`), which raises
99
+ * `invalid input syntax for type boolean` at the database and surfaces as a **500 driven by
100
+ * user input**. With a declared kind the value is coerced up front, and one that can't be
101
+ * coerced is treated exactly like a disallowed field: dropped, or a loud
102
+ * `InvalidColumnFilterError` (→ 400) under {@link DefineFilterOptions.throwOnInvalid}.
103
+ * 2. **Type-aware client codegen.** `make:filter-client` reads the same declaration, so the
104
+ * emitted client narrows operators per field instead of being operator-permissive.
105
+ *
106
+ * One declaration, both ends. Undeclared fields keep the previous behaviour (no coercion), so
107
+ * adding this to an existing spec is opt-in and backwards compatible.
108
+ */
109
+ fieldTypes?: Record<string, FilterFieldTypeInfo>;
110
+ /**
111
+ * Opt-in Postgres tsvector full-text search. When set, the request `search`
112
+ * string routes through `websearch_to_tsquery`/`@@` (and optional `ts_rank`)
113
+ * instead of the ILIKE `searchable` scan. Column(s) + language + rank.
114
+ */
115
+ fullText?: FullTextSearchConfig;
116
+ /** Whitelisted relations and their nested filterable/sortable columns. */
117
+ relations?: Record<string, RelationSpec>;
118
+ /**
119
+ * The owning Lucid model — enables to-many aggregate fields
120
+ * (`$count`/`$sum`/`$avg`/`$min`/`$max`). Its relation metadata (hasMany /
121
+ * manyToMany FK + pivot columns) is introspected at build time to synthesise
122
+ * aggregate computed sources for the whitelisted {@link relations}. Optional;
123
+ * without it, aggregate fields are simply not available (the feature degrades
124
+ * gracefully) — every other feature works with or without it. Also supplies
125
+ * the default {@link table} (the model's table name) used as the correlated
126
+ * subquery's outer alias.
127
+ */
128
+ model?: LucidModelLike;
129
+ /**
130
+ * Maximum relation-path depth (number of relation hops; a base column is
131
+ * depth 0, `posts.title` is depth 1, `posts.comments.body` is depth 2).
132
+ * Defaults to the deepest declared relation nesting. An explicit smaller value
133
+ * caps paths even when a deeper relation is declared.
134
+ */
135
+ maxDepth?: number;
136
+ /** Client-alias → resolved-target field remapping (see {@link resolveFieldAlias}). */
137
+ aliases?: FieldAliases;
138
+ /**
139
+ * Opt-in pgvector embedding-similarity ordering (distinct from text `search`):
140
+ * declares the vector column (and metric / threshold / top-K) rows are ranked
141
+ * by when a request carries a query embedding. Additive — a spec without this
142
+ * is unchanged.
143
+ */
144
+ vectorSimilarity?: VectorSimilarityConfig;
145
+ /**
146
+ * Virtual/computed fields — alias → dev-declared SQL expression
147
+ * ({@link ComputedFields}). Two source forms: a verbatim string
148
+ * (`{ fullName: "first || ' ' || last" }`) or a function
149
+ * (`{ postCount: ({ alias }) => \`(SELECT COUNT(*) FROM posts WHERE posts.author_id = ${alias}.id)\` }`)
150
+ * for correlated subqueries. A declared alias becomes filterable and sortable
151
+ * as if it were a real column; the client value stays parameterized and only
152
+ * the dev expression is inlined. Function-form sources need {@link table}.
153
+ */
154
+ computed?: ComputedFields;
155
+ /**
156
+ * The root model's table name, surfaced to computed-field functions as the
157
+ * outer alias (Lucid gives the main table no generated alias, so the table
158
+ * name IS the alias). Required for function-form computed fields and to-many
159
+ * aggregate fields whose correlated subqueries reference the outer row.
160
+ */
161
+ table?: string;
162
+ /** Opt-in tenant auto-scope read from ctx. */
163
+ tenant?: TenantScopeSpec;
164
+ /**
165
+ * Server-declared filters always applied (in real column terms, AND-combined
166
+ * with the request filters). Not subject to the allow-list — they are trusted
167
+ * server policy, never client input.
168
+ */
169
+ defaultFilters?: ColumnFilter[];
170
+ /** Sort applied when the request supplies none (stable default ordering). */
171
+ defaultSort?: SortItem[];
172
+ /** Default page size when the request gives none. Default 25 (in the runner). */
173
+ defaultSize?: number;
174
+ /** Hard cap on page size. Default 100 (in the runner). */
175
+ maxSize?: number;
176
+ /** Throw `InvalidColumnFilterError` on a disallowed field instead of dropping it. */
177
+ throwOnInvalid?: boolean;
178
+ }
179
+ /**
180
+ * A resolved, reusable, frozen filter definition produced by {@link defineFilter}.
181
+ * Build it once (module scope) and hand it to {@link applyFilterFromRequest} on
182
+ * every request. The `isFilterable`/`isSortable` predicates are the allow-list
183
+ * boundary — relation-path and depth aware — that the runner enforces.
184
+ */
185
+ export interface FilterSpec {
186
+ readonly filterable: RelationColumns;
187
+ readonly sortable: RelationColumns;
188
+ readonly searchable: readonly string[];
189
+ /** Declared column value kinds — drives value coercion AND client codegen. */
190
+ readonly fieldTypes: Readonly<Record<string, FilterFieldTypeInfo>> | undefined;
191
+ readonly fullText: FullTextSearchConfig | undefined;
192
+ readonly relations: Readonly<Record<string, RelationSpec>>;
193
+ readonly maxDepth: number;
194
+ readonly aliases: FieldAliases | undefined;
195
+ readonly vectorSimilarity: VectorSimilarityConfig | undefined;
196
+ /** Declared computed/virtual fields (alias → SQL source). */
197
+ readonly computed: ComputedFields | undefined;
198
+ /** Root table name surfaced to computed-field functions as the outer alias. */
199
+ readonly table: string | undefined;
200
+ readonly tenant: TenantScopeSpec | undefined;
201
+ readonly defaultFilters: readonly ColumnFilter[];
202
+ readonly defaultSort: readonly SortItem[];
203
+ readonly defaultSize: number | undefined;
204
+ readonly maxSize: number | undefined;
205
+ readonly throwOnInvalid: boolean;
206
+ /** Is this (possibly relation-dotted, alias-resolved) field filterable? */
207
+ isFilterable(field: string): boolean;
208
+ /** Is this (possibly relation-dotted, alias-resolved) field sortable? */
209
+ isSortable(field: string): boolean;
210
+ }
211
+ /**
212
+ * Build a reusable {@link FilterSpec} from a declarative {@link DefineFilterOptions}.
213
+ *
214
+ * This is the AdonisJS-idiomatic replacement for the NestJS decorator stack:
215
+ * instead of `@Filterable`/`@Relations`/`@TenantScoped` metadata read by an
216
+ * interceptor, the definition is an explicit, framework-free config object built
217
+ * once and passed explicitly to {@link applyFilterFromRequest}. It captures the
218
+ * same feature set the decorators encoded — filterable/sortable allow-listing,
219
+ * a relation whitelist with a depth cap, field aliases, tenant scoping, and
220
+ * default filters/sort.
221
+ */
222
+ export declare function defineFilter(options: DefineFilterOptions): FilterSpec;
223
+ /**
224
+ * Project a {@link FilterSpec} onto the per-call {@link FilterConfig} the runner's
225
+ * {@link applyFilter}/{@link applyCursor} consume. The allow-lists become
226
+ * predicates (so relation-path + depth rules survive), and `defaultSort` fields
227
+ * are unioned into the sortable predicate so a server-declared default ordering
228
+ * is never dropped by the client-facing sort allow-list.
229
+ */
230
+ export declare function specToFilterConfig(spec: FilterSpec): FilterConfig;
231
+ //# sourceMappingURL=filter_spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter_spec.d.ts","sourceRoot":"","sources":["../../src/filter_spec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,182 @@
1
+ import { discoverAggregateSources } from './aggregate.js';
2
+ /**
3
+ * Thrown when a {@link defineFilter} declaration is itself invalid (a developer
4
+ * error surfaced at wiring time, not a bad client request). Request-time
5
+ * violations (a disallowed field under `throwOnInvalid`) still surface as
6
+ * `InvalidColumnFilterError` from the runner — this is the one new typed error,
7
+ * for the declaration boundary the NestJS `@Filterable` decorator guarded.
8
+ */
9
+ export class FilterDefinitionError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'FilterDefinitionError';
13
+ }
14
+ }
15
+ /** True for the colocated map form (a plain object, not an array and not `'*'`). */
16
+ function isFilterableMap(input) {
17
+ return typeof input === 'object' && input !== null && !Array.isArray(input);
18
+ }
19
+ /** Desugar `filterable` to the allow-list the rest of the lib consumes. */
20
+ function normalizeFilterable(input) {
21
+ return isFilterableMap(input) ? Object.keys(input) : input;
22
+ }
23
+ /** Extract the `fieldTypes` a colocated `filterable` map declares, or undefined for other forms. */
24
+ function fieldTypesFromFilterable(input) {
25
+ if (!isFilterableMap(input))
26
+ return undefined;
27
+ const out = {};
28
+ for (const [field, kind] of Object.entries(input))
29
+ out[field] = { kind };
30
+ return out;
31
+ }
32
+ /** Blocked path segments that could otherwise index inherited prototype members. */
33
+ const BLOCKED_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype', 'toString', 'valueOf']);
34
+ /** Deepest relation nesting declared under `relations` (0 when none). */
35
+ function declaredDepth(relations) {
36
+ if (!relations)
37
+ return 0;
38
+ let max = 0;
39
+ for (const rel of Object.values(relations)) {
40
+ max = Math.max(max, 1 + declaredDepth(rel.relations));
41
+ }
42
+ return max;
43
+ }
44
+ /** Does an allow-list admit a bare column name? `undefined` → treated as `'*'`. */
45
+ function columnAllowed(list, column) {
46
+ if (list === undefined || list === '*')
47
+ return true;
48
+ return list.includes(column);
49
+ }
50
+ /**
51
+ * Resolve a (possibly dotted) field path against the base allow-lists and the
52
+ * relation whitelist, enforcing `maxDepth`. Pure — the client-supplied `field`
53
+ * is only ever read, and each segment is guarded against prototype-pollution
54
+ * lookups before it indexes the relations map.
55
+ */
56
+ function pathAllowed(field, kind, baseFilterable, baseSortable, relations, maxDepth) {
57
+ const segments = field.split('.');
58
+ // Base column (no relation hop).
59
+ if (segments.length === 1) {
60
+ const seg = segments[0];
61
+ if (seg.length === 0 || BLOCKED_SEGMENTS.has(seg))
62
+ return false;
63
+ return columnAllowed(kind === 'filterable' ? baseFilterable : baseSortable, seg);
64
+ }
65
+ // Relation path — bounded by maxDepth (hops = segments - 1).
66
+ if (segments.length - 1 > maxDepth)
67
+ return false;
68
+ let node = relations;
69
+ for (let i = 0; i < segments.length - 1; i++) {
70
+ const seg = segments[i];
71
+ if (seg.length === 0 || BLOCKED_SEGMENTS.has(seg))
72
+ return false;
73
+ if (!node || !Object.hasOwn(node, seg))
74
+ return false;
75
+ const rel = node[seg];
76
+ // Last hop: the final segment is the leaf column on this relation.
77
+ if (i === segments.length - 2) {
78
+ const leaf = segments[segments.length - 1];
79
+ if (leaf.length === 0 || BLOCKED_SEGMENTS.has(leaf))
80
+ return false;
81
+ const list = kind === 'filterable' ? rel.filterable : (rel.sortable ?? rel.filterable);
82
+ return columnAllowed(list, leaf);
83
+ }
84
+ node = rel.relations;
85
+ }
86
+ return false;
87
+ }
88
+ /**
89
+ * Build a reusable {@link FilterSpec} from a declarative {@link DefineFilterOptions}.
90
+ *
91
+ * This is the AdonisJS-idiomatic replacement for the NestJS decorator stack:
92
+ * instead of `@Filterable`/`@Relations`/`@TenantScoped` metadata read by an
93
+ * interceptor, the definition is an explicit, framework-free config object built
94
+ * once and passed explicitly to {@link applyFilterFromRequest}. It captures the
95
+ * same feature set the decorators encoded — filterable/sortable allow-listing,
96
+ * a relation whitelist with a depth cap, field aliases, tenant scoping, and
97
+ * default filters/sort.
98
+ */
99
+ export function defineFilter(options) {
100
+ if (!options || options.filterable === undefined) {
101
+ throw new FilterDefinitionError('defineFilter requires a `filterable` allow-list.');
102
+ }
103
+ if (options.maxDepth !== undefined &&
104
+ (!Number.isInteger(options.maxDepth) || options.maxDepth < 0)) {
105
+ throw new FilterDefinitionError('`maxDepth` must be a non-negative integer.');
106
+ }
107
+ const relations = options.relations ?? {};
108
+ // The colocated map form desugars here, at the boundary: the keys become the allow-list and the
109
+ // values become `fieldTypes`. Everything downstream (the predicates, the runner, the codegen)
110
+ // keeps seeing the array form it always saw, so this is purely an authoring convenience.
111
+ const filterable = normalizeFilterable(options.filterable);
112
+ const sortable = options.sortable ?? filterable;
113
+ const declaredTypes = fieldTypesFromFilterable(options.filterable);
114
+ // An explicit `fieldTypes` entry wins per field: the map form can only express a bare kind, so
115
+ // this is how a caller adds codegen-only richness (enumValues/typeRef) on top of it.
116
+ const fieldTypes = declaredTypes || options.fieldTypes ? { ...declaredTypes, ...options.fieldTypes } : undefined;
117
+ const maxDepth = options.maxDepth ?? declaredDepth(relations);
118
+ // Root table (the correlated-subquery outer alias): explicit `table` wins,
119
+ // else the model's own table name.
120
+ const table = options.table ?? options.model?.table;
121
+ // Discover to-many aggregate computed sources from the model's relation
122
+ // metadata, then let dev-declared `computed` win on any key collision. The
123
+ // discovery is capability-gated (no model → empty map) and per-relation
124
+ // fault-tolerant, so this never throws at wiring time.
125
+ const aggregateSources = discoverAggregateSources(options.model, relations);
126
+ const computed = Object.keys(aggregateSources).length > 0 || options.computed
127
+ ? { ...aggregateSources, ...options.computed }
128
+ : undefined;
129
+ const spec = {
130
+ filterable,
131
+ sortable,
132
+ searchable: options.searchable ?? [],
133
+ fieldTypes,
134
+ fullText: options.fullText,
135
+ relations,
136
+ maxDepth,
137
+ aliases: options.aliases,
138
+ vectorSimilarity: options.vectorSimilarity,
139
+ computed,
140
+ table,
141
+ tenant: options.tenant,
142
+ defaultFilters: options.defaultFilters ?? [],
143
+ defaultSort: options.defaultSort ?? [],
144
+ defaultSize: options.defaultSize,
145
+ maxSize: options.maxSize,
146
+ throwOnInvalid: options.throwOnInvalid ?? false,
147
+ isFilterable(field) {
148
+ return pathAllowed(field, 'filterable', filterable, sortable, relations, maxDepth);
149
+ },
150
+ isSortable(field) {
151
+ return pathAllowed(field, 'sortable', filterable, sortable, relations, maxDepth);
152
+ },
153
+ };
154
+ return Object.freeze(spec);
155
+ }
156
+ /**
157
+ * Project a {@link FilterSpec} onto the per-call {@link FilterConfig} the runner's
158
+ * {@link applyFilter}/{@link applyCursor} consume. The allow-lists become
159
+ * predicates (so relation-path + depth rules survive), and `defaultSort` fields
160
+ * are unioned into the sortable predicate so a server-declared default ordering
161
+ * is never dropped by the client-facing sort allow-list.
162
+ */
163
+ export function specToFilterConfig(spec) {
164
+ const defaultSortFields = new Set(spec.defaultSort.map((s) => s.field));
165
+ const allowed = (field) => spec.isFilterable(field);
166
+ const sortable = (field) => spec.isSortable(field) || defaultSortFields.has(field);
167
+ return {
168
+ allowed,
169
+ sortable,
170
+ ...(spec.searchable.length > 0 && { searchable: [...spec.searchable] }),
171
+ ...(spec.fieldTypes && { fieldTypes: spec.fieldTypes }),
172
+ ...(spec.fullText && { fullText: spec.fullText }),
173
+ ...(spec.aliases && { aliases: spec.aliases }),
174
+ ...(spec.vectorSimilarity && { vectorSimilarity: spec.vectorSimilarity }),
175
+ ...(spec.computed && { computed: spec.computed }),
176
+ ...(spec.table !== undefined && { table: spec.table }),
177
+ ...(spec.defaultSize !== undefined && { defaultSize: spec.defaultSize }),
178
+ ...(spec.maxSize !== undefined && { maxSize: spec.maxSize }),
179
+ throwOnInvalid: spec.throwOnInvalid,
180
+ };
181
+ }
182
+ //# sourceMappingURL=filter_spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter_spec.js","sourceRoot":"","sources":["../../src/filter_spec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import type { NormalizeConstructor } from '@adonisjs/core/types/helpers';
2
+ import type { LucidModel, ModelPaginatorContract, ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
3
+ import { type ApplyFromRequestOptions, type FilterRequestContext } from './apply_from_request.js';
4
+ import type { FilterClass } from './filter_class.js';
5
+ import type { FilterSpec } from './filter_spec.js';
6
+ import type { ResolvedPagination } from './runner.js';
7
+ /**
8
+ * Opt a Lucid model into filtering — the `adonis-lucid-filter` shape, with the rest of the
9
+ * pipeline (search, sort, page bounds) included.
10
+ *
11
+ * ```ts
12
+ * export default class User extends compose(BaseModel, Filterable) {
13
+ * static $filter = () => UserFilter
14
+ * }
15
+ * ```
16
+ *
17
+ * The model then answers two calls, and the first **hands the query builder back** — nothing is
18
+ * executed until you say so:
19
+ *
20
+ * ```ts
21
+ * // keep composing, then page it with what the request asked for
22
+ * const { query } = await User.filter(ctx)
23
+ * query.whereNotNull('confirmedAt').preload('team')
24
+ * return query.filterPaginate()
25
+ *
26
+ * // or the one-liner
27
+ * return User.filterPaginate(ctx)
28
+ * ```
29
+ *
30
+ * The builder comes back inside an object rather than as the promise's own value on purpose: a
31
+ * Lucid query builder is *thenable*, so `await`ing a promise that resolves to one would run the
32
+ * query and hand you rows instead of the builder. Destructuring keeps the builder a builder.
33
+ */
34
+ /**
35
+ * What {@link Filterable} adds to a model, spelled out so the mixin's return type can be named
36
+ * (an inferred anonymous class would drag Lucid's internals into every consumer's build).
37
+ */
38
+ export interface FilteredQuery<M extends LucidModel> extends ResolvedPagination {
39
+ /** The query builder, filtered/searched/sorted — yours to keep composing on, not yet executed. */
40
+ query: ModelQueryBuilderContract<M>;
41
+ }
42
+ export interface FilterableModelStatics {
43
+ /** The filter this model is read through. A thunk, so the filter may import the model back. */
44
+ $filter?: () => FilterClass | FilterSpec;
45
+ filter<M extends LucidModel>(this: M, ctx?: FilterRequestContext, filter?: FilterClass | FilterSpec, options?: ApplyFromRequestOptions): Promise<FilteredQuery<M>>;
46
+ filterPaginate<M extends LucidModel>(this: M, ctx?: FilterRequestContext, filter?: FilterClass | FilterSpec, options?: ApplyFromRequestOptions): Promise<ModelPaginatorContract<InstanceType<M>>>;
47
+ }
48
+ export declare function Filterable<T extends NormalizeConstructor<LucidModel>>(superclass: T): T & FilterableModelStatics;
49
+ //# sourceMappingURL=filterable_mixin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filterable_mixin.d.ts","sourceRoot":"","sources":["../../src/filterable_mixin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACzE,OAAO,KAAK,EACV,UAAU,EACV,sBAAsB,EACtB,yBAAyB,EAC1B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,KAAK,uBAAuB,EAE5B,KAAK,oBAAoB,EAC1B,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH;;;GAGG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,UAAU,CAAE,SAAQ,kBAAkB;IAC7E,kGAAkG;IAClG,KAAK,EAAE,yBAAyB,CAAC,CAAC,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,sBAAsB;IACrC,+FAA+F;IAC/F,OAAO,CAAC,EAAE,MAAM,WAAW,GAAG,UAAU,CAAC;IACzC,MAAM,CAAC,CAAC,SAAS,UAAU,EACzB,IAAI,EAAE,CAAC,EACP,GAAG,CAAC,EAAE,oBAAoB,EAC1B,MAAM,CAAC,EAAE,WAAW,GAAG,UAAU,EACjC,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7B,cAAc,CAAC,CAAC,SAAS,UAAU,EACjC,IAAI,EAAE,CAAC,EACP,GAAG,CAAC,EAAE,oBAAoB,EAC1B,MAAM,CAAC,EAAE,WAAW,GAAG,UAAU,EACjC,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,sBAAsB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACrD;AAED,wBAAgB,UAAU,CAAC,CAAC,SAAS,oBAAoB,CAAC,UAAU,CAAC,EACnE,UAAU,EAAE,CAAC,GACZ,CAAC,GAAG,sBAAsB,CA6D5B"}
@@ -0,0 +1,47 @@
1
+ import { HttpContext } from '@adonisjs/core/http';
2
+ import { applyFilterFromRequest, } from './apply_from_request.js';
3
+ import { paginate } from './paginate.js';
4
+ import { stashPagination } from './pagination_stash.js';
5
+ export function Filterable(superclass) {
6
+ class FilterableModel extends superclass {
7
+ /** The filter this model is read through. A thunk, so the filter may import the model back. */
8
+ static $filter;
9
+ /**
10
+ * Apply the model's filter to a fresh query and hand back the **query builder** plus the
11
+ * pagination the request resolved to — filters, search and sort applied, nothing executed.
12
+ * Compose whatever else the endpoint needs on the builder, then page it with
13
+ * `filterPaginate()` (no arguments) or `paginate(page, size)`.
14
+ */
15
+ static async filter(ctx, filter, options) {
16
+ // `this` is the model the static was called on (a subclass of the mixin), which is exactly
17
+ // what these helpers need — the rule's suggestion (name the class) would pin every model to
18
+ // the anonymous mixin class instead.
19
+ // biome-ignore lint/complexity/noThisInStatic: `this` is the calling model, by design.
20
+ const model = this;
21
+ const target = filter ?? model.$filter?.();
22
+ if (!target) {
23
+ throw new Error(
24
+ // biome-ignore lint/complexity/noThisInStatic: names the model the call was made on.
25
+ `${this.name} has no filter — declare \`static $filter = () => YourFilter\` on the model, or pass one to filter().`);
26
+ }
27
+ // biome-ignore lint/complexity/noThisInStatic: the calling model builds its own query.
28
+ const query = this.query();
29
+ const pagination = await applyFilterFromRequest(query, target, ctx ?? HttpContext.getOrFail(), options);
30
+ stashPagination(query, pagination);
31
+ return { query, page: pagination.page, size: pagination.size };
32
+ }
33
+ /**
34
+ * The same, then `paginate(page, size)` with the pagination the request resolved to — the
35
+ * whole list endpoint in one call.
36
+ */
37
+ static async filterPaginate(ctx, filter, options) {
38
+ // biome-ignore lint/complexity/noThisInStatic: `this` is the calling model, by design.
39
+ const model = this;
40
+ // biome-ignore lint/complexity/noThisInStatic: forwards the call to the same model.
41
+ const { query, page, size } = await model.filter.call(this, ctx, filter, options);
42
+ return paginate(query, page, size);
43
+ }
44
+ }
45
+ return FilterableModel;
46
+ }
47
+ //# 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,EAEL,sBAAsB,GAEvB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,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,OAAO,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAqD,CAAC;QACzF,CAAC;KACF;IAED,OAAO,eAAwD,CAAC;AAClE,CAAC"}
@@ -0,0 +1,107 @@
1
+ import type { FilterSpec } from './filter_spec.js';
2
+ /**
3
+ * Codegen for the typed client — the AdonisJS-idiomatic port of the NestJS
4
+ * `nestjs-filter` codegen. Where the NestJS package ships a standalone
5
+ * `reflect-metadata` bin that reads decorator metadata off entity classes and
6
+ * emits `filterQuery: () => filterQueryTyped<Fields, TypeMap>()` members into a
7
+ * generated `api.ts`, the Adonis port has no decorators: the filter declaration
8
+ * is already a plain runtime {@link FilterSpec} produced by `defineFilter(...)`.
9
+ *
10
+ * So generation is a **pure function of the spec** — string in, string out, no
11
+ * AST walk, no metadata reflection, no framework. The ace command
12
+ * (`make:filter-client`) is the thin IO wrapper that loads the app's declared
13
+ * specs and writes the emitted module to disk. Keeping the generator pure keeps
14
+ * it trivially testable (snapshot/string assertions) and framework-free, exactly
15
+ * like the rest of this core.
16
+ *
17
+ * The emitted module targets `@adonis-agora/filter-client`'s `filterQueryTyped<Fields,
18
+ * FieldTypes>()` factory — the same client the NestJS codegen emitted against —
19
+ * so the two ecosystems share one browser runtime.
20
+ */
21
+ import type { FilterFieldKind } from './types.js';
22
+ export type { FilterFieldKind };
23
+ /**
24
+ * Optional per-field type information. A {@link FilterSpec} carries the
25
+ * allow-list but not column value types (Lucid models aren't reflected here), so
26
+ * the caller supplies types to unlock the client's type-aware operator narrowing.
27
+ * Without it the emitted client is still field-name-safe, just operator-permissive.
28
+ */
29
+ export interface FilterFieldTypeInfo {
30
+ /** Classified value kind. Ignored when {@link typeRef} or {@link enumValues} is set. */
31
+ kind?: FilterFieldKind;
32
+ /** Enum literal members — emitted as a union (`"A" | "B"` or `1 | 2`). Wins over {@link kind}. */
33
+ enumValues?: readonly (string | number)[];
34
+ /** A named TS type (e.g. `'Role'`) — emitted verbatim. Wins over everything. */
35
+ typeRef?: string;
36
+ /** Nullable column — appends `| null` to the emitted type. */
37
+ nullable?: boolean;
38
+ }
39
+ /** Options for {@link generateFilterClient}. */
40
+ export interface GenerateFilterClientOptions {
41
+ /**
42
+ * Base name for the emitted identifiers — e.g. `'people'` yields
43
+ * `PeopleFilterFields`, `PeopleFilterFieldTypes`, `peopleFilterMeta` and
44
+ * `peopleFilterQuery()`. Non-identifier characters are stripped.
45
+ */
46
+ name: string;
47
+ /** Per-field value types keyed by (dotted) field path. Unlocks operator narrowing. */
48
+ fieldTypes?: Record<string, FilterFieldTypeInfo>;
49
+ /** Import specifier for `filterQueryTyped`. Default `'@adonis-agora/filter-client'`. */
50
+ clientModule?: string;
51
+ /** Cap relation-path depth of the emitted field union. Default {@link FilterSpec.maxDepth}. */
52
+ maxDepth?: number;
53
+ /** Emit the "generated — do not edit" banner. Default `true`. */
54
+ banner?: boolean;
55
+ }
56
+ /** Enumerate the filterable field paths a spec admits, capped at `maxDepth` relation hops. */
57
+ export declare function filterableFieldPaths(spec: FilterSpec, maxDepth?: number): string[];
58
+ /** Enumerate the sortable field paths a spec admits, capped at `maxDepth` relation hops. */
59
+ export declare function sortableFieldPaths(spec: FilterSpec, maxDepth?: number): string[];
60
+ /**
61
+ * Generate a typed filter client module for one {@link FilterSpec} — a pure
62
+ * string transform. The emitted module exports, for a spec named `people`:
63
+ *
64
+ * - `type PeopleFilterFields` — the union of filterable field paths (the security
65
+ * boundary, as concrete string literals);
66
+ * - `interface PeopleFilterFieldTypes` — the per-field value-type map (only when
67
+ * `fieldTypes` is supplied), which drives the client's operator/value narrowing;
68
+ * - `const peopleFilterMeta` — runtime metadata (filterable/sortable/searchable
69
+ * fields, whitelisted relations, per-field kinds, and the default sort / page
70
+ * size / max size + cursor keyset the runner paginates by);
71
+ * - `function peopleFilterQuery()` — a `filterQueryTyped<Fields, FieldTypes>()`
72
+ * factory returning a type-safe builder scoped to this spec.
73
+ */
74
+ export declare function generateFilterClient(spec: FilterSpec, options: GenerateFilterClientOptions): string;
75
+ /**
76
+ * One entry in a {@link FilterClientManifest}: a declared spec plus the optional
77
+ * type info / codegen overrides the emitter needs. This is what an app exports
78
+ * from `config/filter.ts` (as `filters`) for the `make:filter-client` command to
79
+ * consume — the specs are already `defineFilter(...)` results, so no reflection.
80
+ */
81
+ export interface FilterClientEntry {
82
+ /** The `defineFilter(...)` result to generate a client for. */
83
+ spec: FilterSpec;
84
+ /** Per-field value types (see {@link GenerateFilterClientOptions.fieldTypes}). */
85
+ fieldTypes?: Record<string, FilterFieldTypeInfo>;
86
+ /** Override the client import specifier for this entry. */
87
+ clientModule?: string;
88
+ /** Override the relation-path depth cap for this entry. */
89
+ maxDepth?: number;
90
+ }
91
+ /** A map of client name → declared spec, keyed by the emitted client's base name. */
92
+ export type FilterClientManifest = Record<string, FilterClientEntry>;
93
+ /** A single generated client: its name, its AdonisJS-conventional file name, and its code. */
94
+ export interface GeneratedFilterClient {
95
+ name: string;
96
+ /** `snake_case`d file name, e.g. `blog_post_filter_client.ts`. */
97
+ filename: string;
98
+ code: string;
99
+ }
100
+ /**
101
+ * Expand a whole {@link FilterClientManifest} into per-spec generated modules —
102
+ * the pure core the `make:filter-client` ace command writes to disk. Kept pure
103
+ * (manifest in, modules out) so the command stays a thin IO wrapper and the
104
+ * expansion is itself testable without a running app.
105
+ */
106
+ export declare function generateFilterClients(manifest: FilterClientManifest): GeneratedFilterClient[];
107
+ //# sourceMappingURL=generate_client.d.ts.map