@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
|
@@ -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
|