@adonis-agora/filter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/commands/main.d.ts +17 -0
- package/dist/commands/main.d.ts.map +1 -0
- package/dist/commands/main.js +17 -0
- package/dist/commands/main.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/src/apply_from_request.d.ts +69 -0
- package/dist/src/apply_from_request.d.ts.map +1 -0
- package/dist/src/apply_from_request.js +115 -0
- package/dist/src/apply_from_request.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/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 +28 -0
- package/dist/src/field_aliases.d.ts.map +1 -0
- package/dist/src/field_aliases.js +61 -0
- package/dist/src/field_aliases.js.map +1 -0
- package/dist/src/filter_spec.d.ts +146 -0
- package/dist/src/filter_spec.d.ts.map +1 -0
- package/dist/src/filter_spec.js +140 -0
- package/dist/src/filter_spec.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 +154 -0
- package/dist/src/generate_client.js.map +1 -0
- package/dist/src/index.d.ts +28 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +18 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lucid_adapter.d.ts +213 -0
- package/dist/src/lucid_adapter.d.ts.map +1 -0
- package/dist/src/lucid_adapter.js +303 -0
- package/dist/src/lucid_adapter.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/parse_request.d.ts +22 -0
- package/dist/src/parse_request.d.ts.map +1 -0
- package/dist/src/parse_request.js +107 -0
- package/dist/src/parse_request.js.map +1 -0
- package/dist/src/runner.d.ts +46 -0
- package/dist/src/runner.d.ts.map +1 -0
- package/dist/src/runner.js +190 -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 +38 -0
- package/dist/src/spatie_parser.d.ts.map +1 -0
- package/dist/src/spatie_parser.js +130 -0
- package/dist/src/spatie_parser.js.map +1 -0
- package/dist/src/testing.d.ts +62 -0
- package/dist/src/testing.d.ts.map +1 -0
- package/dist/src/testing.js +113 -0
- package/dist/src/testing.js.map +1 -0
- package/dist/src/types.d.ts +130 -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/package.json +71 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapes special characters in a LIKE pattern value.
|
|
3
|
+
*
|
|
4
|
+
* @param value - The raw string to escape.
|
|
5
|
+
* @param dialect - The SQL dialect to target.
|
|
6
|
+
* - `'standard'` (default): escapes `%`, `_`, and `\` with a backslash.
|
|
7
|
+
* - `'mssql'`: escapes `%`, `_`, and `[` using bracket notation (`[$&]`).
|
|
8
|
+
*/
|
|
9
|
+
export declare function escapeLike(value: string, dialect?: 'standard' | 'mssql'): string;
|
|
10
|
+
//# sourceMappingURL=escape-like.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escape-like.d.ts","sourceRoot":"","sources":["../../src/escape-like.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,UAAU,GAAG,OAAoB,GAAG,MAAM,CAK5F"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapes special characters in a LIKE pattern value.
|
|
3
|
+
*
|
|
4
|
+
* @param value - The raw string to escape.
|
|
5
|
+
* @param dialect - The SQL dialect to target.
|
|
6
|
+
* - `'standard'` (default): escapes `%`, `_`, and `\` with a backslash.
|
|
7
|
+
* - `'mssql'`: escapes `%`, `_`, and `[` using bracket notation (`[$&]`).
|
|
8
|
+
*/
|
|
9
|
+
export function escapeLike(value, dialect = 'standard') {
|
|
10
|
+
if (dialect === 'mssql') {
|
|
11
|
+
return value.replace(/[%_\[]/g, '[$&]');
|
|
12
|
+
}
|
|
13
|
+
return value.replace(/[%_\\]/g, '\\$&');
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=escape-like.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escape-like.js","sourceRoot":"","sources":["../../src/escape-like.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,UAAgC,UAAU;IAClF,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;QACxB,OAAO,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AAC1C,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ColumnFilter } from './operators.js';
|
|
2
|
+
import type { SortItem } from './types.js';
|
|
3
|
+
/** A declarative field-name remapping: client-facing alias → resolved target column. */
|
|
4
|
+
export type FieldAliases = Record<string, string>;
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a client-supplied field name to its declared `aliases` target, or
|
|
7
|
+
* returns `field` unchanged when no alias applies.
|
|
8
|
+
*
|
|
9
|
+
* This is the single choke point for alias resolution — call it wherever a
|
|
10
|
+
* client-supplied field name is about to be resolved against the entity (column
|
|
11
|
+
* filters and sort). It must run FIRST, before any validation (allow-listing,
|
|
12
|
+
* field-charset checks) — those all evaluate the resolved target, never the
|
|
13
|
+
* alias key.
|
|
14
|
+
*
|
|
15
|
+
* Aliases do not cascade: the returned target is never re-run through the alias
|
|
16
|
+
* map, even when it happens to also be a declared alias key itself — this is
|
|
17
|
+
* what makes alias cycles structurally impossible.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveFieldAlias(aliases: FieldAliases | undefined, field: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Returns a copy of a {@link ColumnFilter} with its own field and every nested
|
|
22
|
+
* AND/OR child field resolved through the alias map. Pure — the input tree is
|
|
23
|
+
* left untouched.
|
|
24
|
+
*/
|
|
25
|
+
export declare function remapFilterAliases(filter: ColumnFilter, aliases: FieldAliases): ColumnFilter;
|
|
26
|
+
/** Resolves every sort directive's field through the alias map (returns a new array). */
|
|
27
|
+
export declare function remapSortAliases(sorts: SortItem[], aliases: FieldAliases): SortItem[];
|
|
28
|
+
//# sourceMappingURL=field_aliases.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field_aliases.d.ts","sourceRoot":"","sources":["../../src/field_aliases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAkB3C,wFAAwF;AACxF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAElD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK1F;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,GAAG,YAAY,CAS5F;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,YAAY,GAAG,QAAQ,EAAE,CAKrF"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field names that would resolve to inherited `Object.prototype` members
|
|
3
|
+
* (`__proto__`, `constructor`, `prototype`, `toString`, `valueOf`) if looked up
|
|
4
|
+
* on a plain-object `aliases` map without an own-property check. A
|
|
5
|
+
* client-supplied field name is never trusted as an alias-map lookup key without
|
|
6
|
+
* this guard, even though the `Object.hasOwn` check below already excludes
|
|
7
|
+
* inherited properties; kept as explicit defense-in-depth.
|
|
8
|
+
*/
|
|
9
|
+
const BLOCKED_ALIAS_KEYS = new Set([
|
|
10
|
+
'__proto__',
|
|
11
|
+
'constructor',
|
|
12
|
+
'prototype',
|
|
13
|
+
'toString',
|
|
14
|
+
'valueOf',
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Resolves a client-supplied field name to its declared `aliases` target, or
|
|
18
|
+
* returns `field` unchanged when no alias applies.
|
|
19
|
+
*
|
|
20
|
+
* This is the single choke point for alias resolution — call it wherever a
|
|
21
|
+
* client-supplied field name is about to be resolved against the entity (column
|
|
22
|
+
* filters and sort). It must run FIRST, before any validation (allow-listing,
|
|
23
|
+
* field-charset checks) — those all evaluate the resolved target, never the
|
|
24
|
+
* alias key.
|
|
25
|
+
*
|
|
26
|
+
* Aliases do not cascade: the returned target is never re-run through the alias
|
|
27
|
+
* map, even when it happens to also be a declared alias key itself — this is
|
|
28
|
+
* what makes alias cycles structurally impossible.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveFieldAlias(aliases, field) {
|
|
31
|
+
if (!aliases || BLOCKED_ALIAS_KEYS.has(field) || !Object.hasOwn(aliases, field)) {
|
|
32
|
+
return field;
|
|
33
|
+
}
|
|
34
|
+
return aliases[field] ?? field;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns a copy of a {@link ColumnFilter} with its own field and every nested
|
|
38
|
+
* AND/OR child field resolved through the alias map. Pure — the input tree is
|
|
39
|
+
* left untouched.
|
|
40
|
+
*/
|
|
41
|
+
export function remapFilterAliases(filter, aliases) {
|
|
42
|
+
const next = {
|
|
43
|
+
field: resolveFieldAlias(aliases, filter.field),
|
|
44
|
+
operator: filter.operator,
|
|
45
|
+
};
|
|
46
|
+
if (filter.value !== undefined)
|
|
47
|
+
next.value = filter.value;
|
|
48
|
+
if (filter.AND)
|
|
49
|
+
next.AND = filter.AND.map((f) => remapFilterAliases(f, aliases));
|
|
50
|
+
if (filter.OR)
|
|
51
|
+
next.OR = filter.OR.map((f) => remapFilterAliases(f, aliases));
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
/** Resolves every sort directive's field through the alias map (returns a new array). */
|
|
55
|
+
export function remapSortAliases(sorts, aliases) {
|
|
56
|
+
return sorts.map((sort) => ({
|
|
57
|
+
field: resolveFieldAlias(aliases, sort.field),
|
|
58
|
+
direction: sort.direction,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=field_aliases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field_aliases.js","sourceRoot":"","sources":["../../src/field_aliases.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC;IACjC,WAAW;IACX,aAAa;IACb,WAAW;IACX,UAAU;IACV,SAAS;CACV,CAAC,CAAC;AAKH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAiC,EAAE,KAAa;IAChF,IAAI,CAAC,OAAO,IAAI,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC;QAChF,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC;AACjC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAoB,EAAE,OAAqB;IAC5E,MAAM,IAAI,GAAiB;QACzB,KAAK,EAAE,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC;QAC/C,QAAQ,EAAE,MAAM,CAAC,QAAQ;KAC1B,CAAC;IACF,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;QAAE,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC1D,IAAI,MAAM,CAAC,GAAG;QAAE,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IACjF,IAAI,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IAC9E,OAAO,IAAI,CAAC;AACd,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,gBAAgB,CAAC,KAAiB,EAAE,OAAqB;IACvE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC1B,KAAK,EAAE,iBAAiB,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC;QAC7C,SAAS,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAC,CAAC,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { FieldAliases } from './field_aliases.js';
|
|
2
|
+
import type { ColumnFilter } from './operators.js';
|
|
3
|
+
import type { FilterConfig, FullTextSearchConfig, SortItem, VectorSimilarityConfig } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Thrown when a {@link defineFilter} declaration is itself invalid (a developer
|
|
6
|
+
* error surfaced at wiring time, not a bad client request). Request-time
|
|
7
|
+
* violations (a disallowed field under `throwOnInvalid`) still surface as
|
|
8
|
+
* `InvalidColumnFilterError` from the runner — this is the one new typed error,
|
|
9
|
+
* for the declaration boundary the NestJS `@Filterable` decorator guarded.
|
|
10
|
+
*/
|
|
11
|
+
export declare class FilterDefinitionError extends Error {
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
14
|
+
/** Column-level allow-list for a relation: `'*'` (any column) or a bare-name list. */
|
|
15
|
+
export type RelationColumns = string[] | '*';
|
|
16
|
+
/**
|
|
17
|
+
* The filterable/sortable declaration for one whitelisted relation, keyed by the
|
|
18
|
+
* relation name on the owning model. Column names are **bare** (unprefixed) — the
|
|
19
|
+
* request field is the dotted path `relation.column`. Nesting `relations` again
|
|
20
|
+
* whitelists deeper paths (`relation.child.column`), bounded by `maxDepth`.
|
|
21
|
+
*/
|
|
22
|
+
export interface RelationSpec {
|
|
23
|
+
/** Columns of the related model clients may filter on. Defaults to `'*'`. */
|
|
24
|
+
filterable?: RelationColumns;
|
|
25
|
+
/** Columns of the related model clients may sort on. Defaults to `filterable`. */
|
|
26
|
+
sortable?: RelationColumns;
|
|
27
|
+
/** Further whitelisted relations reachable from this one (one hop deeper). */
|
|
28
|
+
relations?: Record<string, RelationSpec>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reads the current tenant id from the request context (the AdonisJS
|
|
32
|
+
* `HttpContext`, or any object the caller passes). A `null`/`undefined` result
|
|
33
|
+
* means "no tenant in context" — tenant scoping is then skipped (opt-in, exactly
|
|
34
|
+
* like the NestJS `@TenantScoped` no-op when no tenant resolves).
|
|
35
|
+
*/
|
|
36
|
+
export type TenantResolver = (ctx: unknown) => string | number | null | undefined;
|
|
37
|
+
/** Tenant auto-scope: constrain `column` to the tenant id resolved from ctx. */
|
|
38
|
+
export interface TenantScopeSpec {
|
|
39
|
+
/** The model column constrained to the current tenant id (e.g. `'tenantId'`). */
|
|
40
|
+
column: string;
|
|
41
|
+
/** Resolves the tenant id from the request ctx. Nullish → scope is skipped. */
|
|
42
|
+
resolve: TenantResolver;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The declarative filter definition — the idiomatic AdonisJS reimplementation of
|
|
46
|
+
* everything the NestJS `@Filterable`/`@Relations`/`@TenantScoped` decorators
|
|
47
|
+
* encoded, as a plain options object (no decorators, no metadata reflection).
|
|
48
|
+
*/
|
|
49
|
+
export interface DefineFilterOptions {
|
|
50
|
+
/** Columns clients may filter on. `'*'` allows any base column. */
|
|
51
|
+
filterable: RelationColumns;
|
|
52
|
+
/** Columns clients may sort on. Defaults to {@link DefineFilterOptions.filterable}. */
|
|
53
|
+
sortable?: RelationColumns;
|
|
54
|
+
/**
|
|
55
|
+
* Columns the free-text `search` term scans with a portable ILIKE — the
|
|
56
|
+
* default search path when {@link DefineFilterOptions.fullText} is not set.
|
|
57
|
+
*/
|
|
58
|
+
searchable?: string[];
|
|
59
|
+
/**
|
|
60
|
+
* Opt-in Postgres tsvector full-text search. When set, the request `search`
|
|
61
|
+
* string routes through `websearch_to_tsquery`/`@@` (and optional `ts_rank`)
|
|
62
|
+
* instead of the ILIKE `searchable` scan. Column(s) + language + rank.
|
|
63
|
+
*/
|
|
64
|
+
fullText?: FullTextSearchConfig;
|
|
65
|
+
/** Whitelisted relations and their nested filterable/sortable columns. */
|
|
66
|
+
relations?: Record<string, RelationSpec>;
|
|
67
|
+
/**
|
|
68
|
+
* Maximum relation-path depth (number of relation hops; a base column is
|
|
69
|
+
* depth 0, `posts.title` is depth 1, `posts.comments.body` is depth 2).
|
|
70
|
+
* Defaults to the deepest declared relation nesting. An explicit smaller value
|
|
71
|
+
* caps paths even when a deeper relation is declared.
|
|
72
|
+
*/
|
|
73
|
+
maxDepth?: number;
|
|
74
|
+
/** Client-alias → resolved-target field remapping (see {@link resolveFieldAlias}). */
|
|
75
|
+
aliases?: FieldAliases;
|
|
76
|
+
/**
|
|
77
|
+
* Opt-in pgvector embedding-similarity ordering (distinct from text `search`):
|
|
78
|
+
* declares the vector column (and metric / threshold / top-K) rows are ranked
|
|
79
|
+
* by when a request carries a query embedding. Additive — a spec without this
|
|
80
|
+
* is unchanged.
|
|
81
|
+
*/
|
|
82
|
+
vectorSimilarity?: VectorSimilarityConfig;
|
|
83
|
+
/** Opt-in tenant auto-scope read from ctx. */
|
|
84
|
+
tenant?: TenantScopeSpec;
|
|
85
|
+
/**
|
|
86
|
+
* Server-declared filters always applied (in real column terms, AND-combined
|
|
87
|
+
* with the request filters). Not subject to the allow-list — they are trusted
|
|
88
|
+
* server policy, never client input.
|
|
89
|
+
*/
|
|
90
|
+
defaultFilters?: ColumnFilter[];
|
|
91
|
+
/** Sort applied when the request supplies none (stable default ordering). */
|
|
92
|
+
defaultSort?: SortItem[];
|
|
93
|
+
/** Default page size when the request gives none. Default 25 (in the runner). */
|
|
94
|
+
defaultSize?: number;
|
|
95
|
+
/** Hard cap on page size. Default 100 (in the runner). */
|
|
96
|
+
maxSize?: number;
|
|
97
|
+
/** Throw `InvalidColumnFilterError` on a disallowed field instead of dropping it. */
|
|
98
|
+
throwOnInvalid?: boolean;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A resolved, reusable, frozen filter definition produced by {@link defineFilter}.
|
|
102
|
+
* Build it once (module scope) and hand it to {@link applyFilterFromRequest} on
|
|
103
|
+
* every request. The `isFilterable`/`isSortable` predicates are the allow-list
|
|
104
|
+
* boundary — relation-path and depth aware — that the runner enforces.
|
|
105
|
+
*/
|
|
106
|
+
export interface FilterSpec {
|
|
107
|
+
readonly filterable: RelationColumns;
|
|
108
|
+
readonly sortable: RelationColumns;
|
|
109
|
+
readonly searchable: readonly string[];
|
|
110
|
+
readonly fullText: FullTextSearchConfig | undefined;
|
|
111
|
+
readonly relations: Readonly<Record<string, RelationSpec>>;
|
|
112
|
+
readonly maxDepth: number;
|
|
113
|
+
readonly aliases: FieldAliases | undefined;
|
|
114
|
+
readonly vectorSimilarity: VectorSimilarityConfig | undefined;
|
|
115
|
+
readonly tenant: TenantScopeSpec | undefined;
|
|
116
|
+
readonly defaultFilters: readonly ColumnFilter[];
|
|
117
|
+
readonly defaultSort: readonly SortItem[];
|
|
118
|
+
readonly defaultSize: number | undefined;
|
|
119
|
+
readonly maxSize: number | undefined;
|
|
120
|
+
readonly throwOnInvalid: boolean;
|
|
121
|
+
/** Is this (possibly relation-dotted, alias-resolved) field filterable? */
|
|
122
|
+
isFilterable(field: string): boolean;
|
|
123
|
+
/** Is this (possibly relation-dotted, alias-resolved) field sortable? */
|
|
124
|
+
isSortable(field: string): boolean;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Build a reusable {@link FilterSpec} from a declarative {@link DefineFilterOptions}.
|
|
128
|
+
*
|
|
129
|
+
* This is the AdonisJS-idiomatic replacement for the NestJS decorator stack:
|
|
130
|
+
* instead of `@Filterable`/`@Relations`/`@TenantScoped` metadata read by an
|
|
131
|
+
* interceptor, the definition is an explicit, framework-free config object built
|
|
132
|
+
* once and passed explicitly to {@link applyFilterFromRequest}. It captures the
|
|
133
|
+
* same feature set the decorators encoded — filterable/sortable allow-listing,
|
|
134
|
+
* a relation whitelist with a depth cap, field aliases, tenant scoping, and
|
|
135
|
+
* default filters/sort.
|
|
136
|
+
*/
|
|
137
|
+
export declare function defineFilter(options: DefineFilterOptions): FilterSpec;
|
|
138
|
+
/**
|
|
139
|
+
* Project a {@link FilterSpec} onto the per-call {@link FilterConfig} the runner's
|
|
140
|
+
* {@link applyFilter}/{@link applyCursor} consume. The allow-lists become
|
|
141
|
+
* predicates (so relation-path + depth rules survive), and `defaultSort` fields
|
|
142
|
+
* are unioned into the sortable predicate so a server-declared default ordering
|
|
143
|
+
* is never dropped by the client-facing sort allow-list.
|
|
144
|
+
*/
|
|
145
|
+
export declare function specToFilterConfig(spec: FilterSpec): FilterConfig;
|
|
146
|
+
//# 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,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when a {@link defineFilter} declaration is itself invalid (a developer
|
|
3
|
+
* error surfaced at wiring time, not a bad client request). Request-time
|
|
4
|
+
* violations (a disallowed field under `throwOnInvalid`) still surface as
|
|
5
|
+
* `InvalidColumnFilterError` from the runner — this is the one new typed error,
|
|
6
|
+
* for the declaration boundary the NestJS `@Filterable` decorator guarded.
|
|
7
|
+
*/
|
|
8
|
+
export class FilterDefinitionError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'FilterDefinitionError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Blocked path segments that could otherwise index inherited prototype members. */
|
|
15
|
+
const BLOCKED_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype', 'toString', 'valueOf']);
|
|
16
|
+
/** Deepest relation nesting declared under `relations` (0 when none). */
|
|
17
|
+
function declaredDepth(relations) {
|
|
18
|
+
if (!relations)
|
|
19
|
+
return 0;
|
|
20
|
+
let max = 0;
|
|
21
|
+
for (const rel of Object.values(relations)) {
|
|
22
|
+
max = Math.max(max, 1 + declaredDepth(rel.relations));
|
|
23
|
+
}
|
|
24
|
+
return max;
|
|
25
|
+
}
|
|
26
|
+
/** Does an allow-list admit a bare column name? `undefined` → treated as `'*'`. */
|
|
27
|
+
function columnAllowed(list, column) {
|
|
28
|
+
if (list === undefined || list === '*')
|
|
29
|
+
return true;
|
|
30
|
+
return list.includes(column);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a (possibly dotted) field path against the base allow-lists and the
|
|
34
|
+
* relation whitelist, enforcing `maxDepth`. Pure — the client-supplied `field`
|
|
35
|
+
* is only ever read, and each segment is guarded against prototype-pollution
|
|
36
|
+
* lookups before it indexes the relations map.
|
|
37
|
+
*/
|
|
38
|
+
function pathAllowed(field, kind, baseFilterable, baseSortable, relations, maxDepth) {
|
|
39
|
+
const segments = field.split('.');
|
|
40
|
+
// Base column (no relation hop).
|
|
41
|
+
if (segments.length === 1) {
|
|
42
|
+
const seg = segments[0];
|
|
43
|
+
if (seg.length === 0 || BLOCKED_SEGMENTS.has(seg))
|
|
44
|
+
return false;
|
|
45
|
+
return columnAllowed(kind === 'filterable' ? baseFilterable : baseSortable, seg);
|
|
46
|
+
}
|
|
47
|
+
// Relation path — bounded by maxDepth (hops = segments - 1).
|
|
48
|
+
if (segments.length - 1 > maxDepth)
|
|
49
|
+
return false;
|
|
50
|
+
let node = relations;
|
|
51
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
52
|
+
const seg = segments[i];
|
|
53
|
+
if (seg.length === 0 || BLOCKED_SEGMENTS.has(seg))
|
|
54
|
+
return false;
|
|
55
|
+
if (!node || !Object.hasOwn(node, seg))
|
|
56
|
+
return false;
|
|
57
|
+
const rel = node[seg];
|
|
58
|
+
// Last hop: the final segment is the leaf column on this relation.
|
|
59
|
+
if (i === segments.length - 2) {
|
|
60
|
+
const leaf = segments[segments.length - 1];
|
|
61
|
+
if (leaf.length === 0 || BLOCKED_SEGMENTS.has(leaf))
|
|
62
|
+
return false;
|
|
63
|
+
const list = kind === 'filterable' ? rel.filterable : (rel.sortable ?? rel.filterable);
|
|
64
|
+
return columnAllowed(list, leaf);
|
|
65
|
+
}
|
|
66
|
+
node = rel.relations;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build a reusable {@link FilterSpec} from a declarative {@link DefineFilterOptions}.
|
|
72
|
+
*
|
|
73
|
+
* This is the AdonisJS-idiomatic replacement for the NestJS decorator stack:
|
|
74
|
+
* instead of `@Filterable`/`@Relations`/`@TenantScoped` metadata read by an
|
|
75
|
+
* interceptor, the definition is an explicit, framework-free config object built
|
|
76
|
+
* once and passed explicitly to {@link applyFilterFromRequest}. It captures the
|
|
77
|
+
* same feature set the decorators encoded — filterable/sortable allow-listing,
|
|
78
|
+
* a relation whitelist with a depth cap, field aliases, tenant scoping, and
|
|
79
|
+
* default filters/sort.
|
|
80
|
+
*/
|
|
81
|
+
export function defineFilter(options) {
|
|
82
|
+
if (!options || options.filterable === undefined) {
|
|
83
|
+
throw new FilterDefinitionError('defineFilter requires a `filterable` allow-list.');
|
|
84
|
+
}
|
|
85
|
+
if (options.maxDepth !== undefined &&
|
|
86
|
+
(!Number.isInteger(options.maxDepth) || options.maxDepth < 0)) {
|
|
87
|
+
throw new FilterDefinitionError('`maxDepth` must be a non-negative integer.');
|
|
88
|
+
}
|
|
89
|
+
const relations = options.relations ?? {};
|
|
90
|
+
const filterable = options.filterable;
|
|
91
|
+
const sortable = options.sortable ?? options.filterable;
|
|
92
|
+
const maxDepth = options.maxDepth ?? declaredDepth(relations);
|
|
93
|
+
const spec = {
|
|
94
|
+
filterable,
|
|
95
|
+
sortable,
|
|
96
|
+
searchable: options.searchable ?? [],
|
|
97
|
+
fullText: options.fullText,
|
|
98
|
+
relations,
|
|
99
|
+
maxDepth,
|
|
100
|
+
aliases: options.aliases,
|
|
101
|
+
vectorSimilarity: options.vectorSimilarity,
|
|
102
|
+
tenant: options.tenant,
|
|
103
|
+
defaultFilters: options.defaultFilters ?? [],
|
|
104
|
+
defaultSort: options.defaultSort ?? [],
|
|
105
|
+
defaultSize: options.defaultSize,
|
|
106
|
+
maxSize: options.maxSize,
|
|
107
|
+
throwOnInvalid: options.throwOnInvalid ?? false,
|
|
108
|
+
isFilterable(field) {
|
|
109
|
+
return pathAllowed(field, 'filterable', filterable, sortable, relations, maxDepth);
|
|
110
|
+
},
|
|
111
|
+
isSortable(field) {
|
|
112
|
+
return pathAllowed(field, 'sortable', filterable, sortable, relations, maxDepth);
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
return Object.freeze(spec);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Project a {@link FilterSpec} onto the per-call {@link FilterConfig} the runner's
|
|
119
|
+
* {@link applyFilter}/{@link applyCursor} consume. The allow-lists become
|
|
120
|
+
* predicates (so relation-path + depth rules survive), and `defaultSort` fields
|
|
121
|
+
* are unioned into the sortable predicate so a server-declared default ordering
|
|
122
|
+
* is never dropped by the client-facing sort allow-list.
|
|
123
|
+
*/
|
|
124
|
+
export function specToFilterConfig(spec) {
|
|
125
|
+
const defaultSortFields = new Set(spec.defaultSort.map((s) => s.field));
|
|
126
|
+
const allowed = (field) => spec.isFilterable(field);
|
|
127
|
+
const sortable = (field) => spec.isSortable(field) || defaultSortFields.has(field);
|
|
128
|
+
return {
|
|
129
|
+
allowed,
|
|
130
|
+
sortable,
|
|
131
|
+
...(spec.searchable.length > 0 && { searchable: [...spec.searchable] }),
|
|
132
|
+
...(spec.fullText && { fullText: spec.fullText }),
|
|
133
|
+
...(spec.aliases && { aliases: spec.aliases }),
|
|
134
|
+
...(spec.vectorSimilarity && { vectorSimilarity: spec.vectorSimilarity }),
|
|
135
|
+
...(spec.defaultSize !== undefined && { defaultSize: spec.defaultSize }),
|
|
136
|
+
...(spec.maxSize !== undefined && { maxSize: spec.maxSize }),
|
|
137
|
+
throwOnInvalid: spec.throwOnInvalid,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=filter_spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter_spec.js","sourceRoot":"","sources":["../../src/filter_spec.ts"],"names":[],"mappings":"AAUA;;;;;;GAMG;AACH,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAwHD,oFAAoF;AACpF,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC;AAEnG,yEAAyE;AACzE,SAAS,aAAa,CAAC,SAAmD;IACxE,IAAI,CAAC,SAAS;QAAE,OAAO,CAAC,CAAC;IACzB,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3C,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,mFAAmF;AACnF,SAAS,aAAa,CAAC,IAAiC,EAAE,MAAc;IACtE,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IACpD,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAClB,KAAa,EACb,IAA+B,EAC/B,cAA+B,EAC/B,YAA6B,EAC7B,SAAuC,EACvC,QAAgB;IAEhB,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAElC,iCAAiC;IACjC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAE,CAAC;QACzB,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QAChE,OAAO,aAAa,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;IACnF,CAAC;IAED,6DAA6D;IAC7D,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,GAAG,QAAQ;QAAE,OAAO,KAAK,CAAC;IAEjD,IAAI,IAAI,GAA6C,SAAS,CAAC;IAC/D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7C,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAE,CAAC;QACzB,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QAChE,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACrD,MAAM,GAAG,GAAiB,IAAI,CAAC,GAAG,CAAE,CAAC;QAErC,mEAAmE;QACnE,IAAI,CAAC,KAAK,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;YAC5C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,OAAO,KAAK,CAAC;YAClE,MAAM,IAAI,GACR,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;YAC5E,OAAO,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACnC,CAAC;QACD,IAAI,GAAG,GAAG,CAAC,SAAS,CAAC;IACvB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,OAA4B;IACvD,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACjD,MAAM,IAAI,qBAAqB,CAAC,kDAAkD,CAAC,CAAC;IACtF,CAAC;IACD,IACE,OAAO,CAAC,QAAQ,KAAK,SAAS;QAC9B,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC,EAC7D,CAAC;QACD,MAAM,IAAI,qBAAqB,CAAC,4CAA4C,CAAC,CAAC;IAChF,CAAC;IAED,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC;IAC1C,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IACtC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,UAAU,CAAC;IACxD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,aAAa,CAAC,SAAS,CAAC,CAAC;IAE9D,MAAM,IAAI,GAAe;QACvB,UAAU;QACV,QAAQ;QACR,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,EAAE;QACpC,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,SAAS;QACT,QAAQ;QACR,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,gBAAgB,EAAE,OAAO,CAAC,gBAAgB;QAC1C,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,EAAE;QAC5C,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,EAAE;QACtC,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,KAAK;QAC/C,YAAY,CAAC,KAAa;YACxB,OAAO,WAAW,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;QACrF,CAAC;QACD,UAAU,CAAC,KAAa;YACtB,OAAO,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;QACnF,CAAC;KACF,CAAC;IAEF,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAgB;IACjD,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IACxE,MAAM,OAAO,GAAc,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IAC/D,MAAM,QAAQ,GAAc,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAE9F,OAAO;QACL,OAAO;QACP,QAAQ;QACR,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACvE,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACjD,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;QAC9C,GAAG,CAAC,IAAI,CAAC,gBAAgB,IAAI,EAAE,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACzE,GAAG,CAAC,IAAI,CAAC,WAAW,KAAK,SAAS,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC;QACxE,GAAG,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;QAC5D,cAAc,EAAE,IAAI,CAAC,cAAc;KACpC,CAAC;AACJ,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
|
+
/** A field's classified value kind — mirrors `@adonis-agora/filter-client`'s `FieldTypeKind`. */
|
|
22
|
+
export type FilterFieldKind = 'string' | 'number' | 'boolean' | 'date' | 'json' | 'unknown';
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate_client.d.ts","sourceRoot":"","sources":["../../src/generate_client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,kBAAkB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;GAkBG;AAEH,iGAAiG;AACjG,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAE5F;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,wFAAwF;IACxF,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,kGAAkG;IAClG,UAAU,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC1C,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,gDAAgD;AAChD,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IACjD,wFAAwF;IACxF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,8FAA8F;AAC9F,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,SAAgB,GAAG,MAAM,EAAE,CAEzF;AAED,4FAA4F;AAC5F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,SAAgB,GAAG,MAAM,EAAE,CAEvF;AAqFD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,2BAA2B,GACnC,MAAM,CAiFR;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,IAAI,EAAE,UAAU,CAAC;IACjB,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IACjD,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qFAAqF;AACrF,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AAErE,8FAA8F;AAC9F,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,qBAAqB,EAAE,CAW7F"}
|