@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,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A recording stand-in for a Lucid query builder, published under
|
|
3
|
+
* `@adonis-agora/filter/testing` so consumers can unit-test their own filter
|
|
4
|
+
* definitions without a database. It satisfies {@link QueryBuilderLike}, so any
|
|
5
|
+
* of the adapter/runner helpers (or {@link applyFilterFromRequest}) can be
|
|
6
|
+
* driven against it and the resulting translation asserted.
|
|
7
|
+
*
|
|
8
|
+
* Nested `where`/`orWhere` closures run against child recorders, and a dotted
|
|
9
|
+
* relation-path filter's `whereHas` records the relation name plus runs its
|
|
10
|
+
* nested callback against a child. {@link flatten} collects every leaf call
|
|
11
|
+
* (across all groups and relation subqueries) so tests can assert a translation
|
|
12
|
+
* appeared without caring about the exact grouping nesting.
|
|
13
|
+
*/
|
|
14
|
+
export class MockQueryBuilder {
|
|
15
|
+
calls = [];
|
|
16
|
+
children = [];
|
|
17
|
+
record(method, ...args) {
|
|
18
|
+
this.calls.push({ method, args });
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
group(method, cb) {
|
|
22
|
+
const child = new MockQueryBuilder();
|
|
23
|
+
this.children.push(child);
|
|
24
|
+
this.calls.push({ method, args: ['<group>'] });
|
|
25
|
+
cb(child);
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural overloads for the mock.
|
|
29
|
+
where(a, b, c) {
|
|
30
|
+
if (typeof a === 'function')
|
|
31
|
+
return this.group('where', a);
|
|
32
|
+
return c !== undefined ? this.record('where', a, b, c) : this.record('where', a, b);
|
|
33
|
+
}
|
|
34
|
+
orWhere(cb) {
|
|
35
|
+
return this.group('orWhere', cb);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Record a relation subquery: the `whereHas` call is captured with the
|
|
39
|
+
* relation name (so `find('whereHas')?.args[0]` is the relation), and the
|
|
40
|
+
* nested callback runs against a child recorder whose calls surface via
|
|
41
|
+
* {@link flatten}.
|
|
42
|
+
*/
|
|
43
|
+
whereHas(relation, cb) {
|
|
44
|
+
const child = new MockQueryBuilder();
|
|
45
|
+
this.children.push(child);
|
|
46
|
+
this.calls.push({ method: 'whereHas', args: [relation] });
|
|
47
|
+
cb(child);
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
whereNot(column, value) {
|
|
51
|
+
return this.record('whereNot', column, value);
|
|
52
|
+
}
|
|
53
|
+
whereIn(column, values) {
|
|
54
|
+
return this.record('whereIn', column, values);
|
|
55
|
+
}
|
|
56
|
+
whereNotIn(column, values) {
|
|
57
|
+
return this.record('whereNotIn', column, values);
|
|
58
|
+
}
|
|
59
|
+
whereNull(column) {
|
|
60
|
+
return this.record('whereNull', column);
|
|
61
|
+
}
|
|
62
|
+
whereNotNull(column) {
|
|
63
|
+
return this.record('whereNotNull', column);
|
|
64
|
+
}
|
|
65
|
+
whereBetween(column, range) {
|
|
66
|
+
return this.record('whereBetween', column, range);
|
|
67
|
+
}
|
|
68
|
+
whereNotBetween(column, range) {
|
|
69
|
+
return this.record('whereNotBetween', column, range);
|
|
70
|
+
}
|
|
71
|
+
whereILike(column, value) {
|
|
72
|
+
return this.record('whereILike', column, value);
|
|
73
|
+
}
|
|
74
|
+
orWhereILike(column, value) {
|
|
75
|
+
return this.record('orWhereILike', column, value);
|
|
76
|
+
}
|
|
77
|
+
orderBy(column, direction) {
|
|
78
|
+
return this.record('orderBy', column, direction);
|
|
79
|
+
}
|
|
80
|
+
/** Record a raw predicate: `args` are `[sql, bindings]` (bindings default `[]`). */
|
|
81
|
+
whereRaw(sql, bindings = []) {
|
|
82
|
+
return this.record('whereRaw', sql, bindings);
|
|
83
|
+
}
|
|
84
|
+
/** Record a raw ordering: `args` are `[sql, bindings]` (bindings default `[]`). */
|
|
85
|
+
orderByRaw(sql, bindings = []) {
|
|
86
|
+
return this.record('orderByRaw', sql, bindings);
|
|
87
|
+
}
|
|
88
|
+
limit(count) {
|
|
89
|
+
return this.record('limit', count);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Every recorded call across this builder and all nested groups and relation
|
|
93
|
+
* subqueries (excludes the anonymous group markers, but keeps `whereHas` since
|
|
94
|
+
* its recorded arg is the relation name, not a marker).
|
|
95
|
+
*/
|
|
96
|
+
flatten() {
|
|
97
|
+
const own = this.calls.filter((c) => c.args[0] !== '<group>');
|
|
98
|
+
return [...own, ...this.children.flatMap((c) => c.flatten())];
|
|
99
|
+
}
|
|
100
|
+
/** Find the first flattened call matching `method`. */
|
|
101
|
+
find(method) {
|
|
102
|
+
return this.flatten().find((c) => c.method === method);
|
|
103
|
+
}
|
|
104
|
+
/** Every flattened call matching `method` (handy for asserting relation subqueries). */
|
|
105
|
+
findAll(method) {
|
|
106
|
+
return this.flatten().filter((c) => c.method === method);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Factory for a fresh {@link MockQueryBuilder} — the recording Lucid stand-in. */
|
|
110
|
+
export function makeMockQueryBuilder() {
|
|
111
|
+
return new MockQueryBuilder();
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/testing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { FieldAliases } from './field_aliases.js';
|
|
2
|
+
import type { FullTextSearchOptions, VectorDistanceMetric } from './lucid_adapter.js';
|
|
3
|
+
import type { ColumnFilter } from './operators.js';
|
|
4
|
+
/** A sort directive: field + direction. */
|
|
5
|
+
export interface SortItem {
|
|
6
|
+
field: string;
|
|
7
|
+
direction: 'asc' | 'desc';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Where a request's structured input is read from, for {@link resolveInputFromRequest}:
|
|
11
|
+
*
|
|
12
|
+
* - `'auto'` — query on reads (GET/HEAD), query+body (body wins) on writes.
|
|
13
|
+
* - `'query'` / `'body'` — always that container.
|
|
14
|
+
* - a dot-path (e.g. `'body.filters'`) — the nested object at that path.
|
|
15
|
+
* - a function — a custom extractor receiving the raw request.
|
|
16
|
+
*/
|
|
17
|
+
export type InputSource = 'auto' | 'query' | 'body' | (string & {}) | ((req: unknown) => Record<string, unknown> | undefined);
|
|
18
|
+
/**
|
|
19
|
+
* How incoming field-name keys are normalized before matching against the
|
|
20
|
+
* allow-list — a built-in case transform or a custom mapping function.
|
|
21
|
+
*/
|
|
22
|
+
export type InputNormalizer = 'camelCase' | 'snakeCase' | ((key: string) => string);
|
|
23
|
+
/**
|
|
24
|
+
* The parsed, structured input a {@link applyFilter} call consumes — produced by
|
|
25
|
+
* {@link parseFilterRequest} from a request query string, or built directly.
|
|
26
|
+
*/
|
|
27
|
+
export interface FilterInput {
|
|
28
|
+
/** Column filter conditions (with optional AND/OR composition). */
|
|
29
|
+
filters?: ColumnFilter[];
|
|
30
|
+
/** Sort directives, applied in order. */
|
|
31
|
+
sort?: SortItem[];
|
|
32
|
+
/**
|
|
33
|
+
* Free-text search term. Routed through Postgres tsvector full-text search
|
|
34
|
+
* when the policy declares {@link FilterConfig.fullText}, otherwise a portable
|
|
35
|
+
* ILIKE scan across {@link FilterConfig.searchable}.
|
|
36
|
+
*/
|
|
37
|
+
search?: string;
|
|
38
|
+
/** 1-based page number for offset pagination. */
|
|
39
|
+
page?: number;
|
|
40
|
+
/** Page size for offset pagination. */
|
|
41
|
+
size?: number;
|
|
42
|
+
/**
|
|
43
|
+
* A query embedding to rank rows by pgvector *similarity* (distinct from the
|
|
44
|
+
* text `search` above). Applied only when the policy declares a similarity
|
|
45
|
+
* column (see {@link FilterConfig.vectorSimilarity}) — ignored otherwise, so
|
|
46
|
+
* non-similarity requests are unchanged. Typically the controller computes
|
|
47
|
+
* this from an embedding service, not the query string.
|
|
48
|
+
*/
|
|
49
|
+
vectorSimilarity?: readonly number[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Declares a Postgres tsvector full-text search on a policy — the primary
|
|
53
|
+
* `search` path when set. The request's `search` string is matched against the
|
|
54
|
+
* configured document via `websearch_to_tsquery`, optionally ranked by
|
|
55
|
+
* `ts_rank`. Omitting `column`-related fields is a compile error; a policy
|
|
56
|
+
* without this config falls back to the ILIKE {@link FilterConfig.searchable}
|
|
57
|
+
* scan. Additive — carries the same shape as {@link FullTextSearchOptions}
|
|
58
|
+
* minus the per-request `query`.
|
|
59
|
+
*/
|
|
60
|
+
export type FullTextSearchConfig = Omit<FullTextSearchOptions, 'query'>;
|
|
61
|
+
/**
|
|
62
|
+
* Declares a pgvector **embedding similarity** ordering on a policy — distinct
|
|
63
|
+
* from full-text `search`. When a request also carries a query embedding
|
|
64
|
+
* ({@link FilterInput.vectorSimilarity}), the runner ranks rows by ascending
|
|
65
|
+
* distance to it. Additive — a policy without this behaves exactly as before.
|
|
66
|
+
*/
|
|
67
|
+
export interface VectorSimilarityConfig {
|
|
68
|
+
/** The pgvector column ranked against the query embedding (e.g. `'embedding'`). */
|
|
69
|
+
column: string;
|
|
70
|
+
/** Distance metric → pgvector operator. Default `'cosine'` (`<=>`). */
|
|
71
|
+
metric?: VectorDistanceMetric;
|
|
72
|
+
/** Optional max-distance filter — drop rows farther than this from the embedding. */
|
|
73
|
+
threshold?: number;
|
|
74
|
+
/** Optional top-K truncation — keep only the K nearest rows. */
|
|
75
|
+
topK?: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* An allow-list of field names — the security boundary for filter/sort. One of:
|
|
79
|
+
*
|
|
80
|
+
* - `'*'` — allow any field (use with care);
|
|
81
|
+
* - `string[]` — allow exactly these field names;
|
|
82
|
+
* - a predicate `(field) => boolean` — allow fields for which it returns true.
|
|
83
|
+
* The predicate form lets a policy express rules a flat list can't (e.g.
|
|
84
|
+
* relation-path whitelisting with a depth cap). It is evaluated against the
|
|
85
|
+
* already alias-resolved target field, never the client-facing alias key.
|
|
86
|
+
*/
|
|
87
|
+
export type AllowList = '*' | string[] | ((field: string) => boolean);
|
|
88
|
+
/**
|
|
89
|
+
* Per-call filter policy. The allow-lists are the security boundary: only fields
|
|
90
|
+
* named here can be filtered/sorted/searched, so client input can never probe
|
|
91
|
+
* arbitrary columns.
|
|
92
|
+
*/
|
|
93
|
+
export interface FilterConfig {
|
|
94
|
+
/** Columns clients may filter on. `'*'` allows any (use with care). */
|
|
95
|
+
allowed: AllowList;
|
|
96
|
+
/** Columns clients may sort on. Defaults to {@link FilterConfig.allowed}. */
|
|
97
|
+
sortable?: AllowList;
|
|
98
|
+
/**
|
|
99
|
+
* Columns the free-text `search` term scans with a portable ILIKE — the
|
|
100
|
+
* default search path when {@link FilterConfig.fullText} is not set.
|
|
101
|
+
*/
|
|
102
|
+
searchable?: string[];
|
|
103
|
+
/**
|
|
104
|
+
* Opt-in Postgres tsvector full-text search. When set, the request `search`
|
|
105
|
+
* string routes through `websearch_to_tsquery`/`@@` (and optional `ts_rank`)
|
|
106
|
+
* instead of the ILIKE {@link FilterConfig.searchable} scan. Omitted → ILIKE.
|
|
107
|
+
*/
|
|
108
|
+
fullText?: FullTextSearchConfig;
|
|
109
|
+
/** Default page size when none is given. Default 25. */
|
|
110
|
+
defaultSize?: number;
|
|
111
|
+
/** Hard cap on page size. Default 100. */
|
|
112
|
+
maxSize?: number;
|
|
113
|
+
/** Throw `InvalidColumnFilterError` on a disallowed/invalid field instead of dropping it. Default false (drop). */
|
|
114
|
+
throwOnInvalid?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Declarative field-name remapping applied to filter and sort fields BEFORE
|
|
117
|
+
* allow-listing — an alias key resolves to its target column, which is what
|
|
118
|
+
* the allow-list, validation and query builder then see. Aliases do not
|
|
119
|
+
* cascade. See {@link resolveFieldAlias}.
|
|
120
|
+
*/
|
|
121
|
+
aliases?: FieldAliases;
|
|
122
|
+
/**
|
|
123
|
+
* Opt-in pgvector embedding-similarity ordering (distinct from text `search`).
|
|
124
|
+
* When set and the request carries a query embedding
|
|
125
|
+
* ({@link FilterInput.vectorSimilarity}), rows are ranked nearest-first by
|
|
126
|
+
* distance to it. Omitted → no similarity ordering (default).
|
|
127
|
+
*/
|
|
128
|
+
vectorSimilarity?: VectorSimilarityConfig;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACtF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,2CAA2C;AAC3C,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,OAAO,GACP,MAAM,GAEN,CAAC,MAAM,GAAG,EAAE,CAAC,GACb,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC;AAE5D;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,WAAW,GAAG,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC;AAEpF;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,yCAAyC;IACzC,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC;IAClB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC;AAExE;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,SAAS,GAAG,GAAG,GAAG,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC;AAEtE;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,OAAO,EAAE,SAAS,CAAC;IACnB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mHAAmH;IACnH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;CAC3C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ColumnFilter, type FilterOperator } from './operators.js';
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes an operator string to its canonical {@link FilterOperator},
|
|
4
|
+
* resolving SQL-symbol aliases (`=` -> `equals`, `!=` -> `notEquals`, ...).
|
|
5
|
+
* Canonical operators (and unknown strings) pass through unchanged.
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeOperator(operator: string): FilterOperator;
|
|
8
|
+
/**
|
|
9
|
+
* Maximum allowed nesting depth for AND/OR filter composition.
|
|
10
|
+
* Prevents stack overflow from maliciously deep payloads (DoS).
|
|
11
|
+
*/
|
|
12
|
+
export declare const MAX_FILTER_DEPTH = 10;
|
|
13
|
+
export declare class InvalidColumnFilterError extends Error {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Validates a ColumnFilter at runtime:
|
|
18
|
+
* - field must be a non-empty string with no SQL-unsafe characters
|
|
19
|
+
* - operator must be a known FilterOperator
|
|
20
|
+
* - value is required for non-unary operators
|
|
21
|
+
* - 'between' requires a 2-element array
|
|
22
|
+
* - 'in'/'isAnyOf' require arrays
|
|
23
|
+
* - AND/OR arrays are validated recursively
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateColumnFilter(filter: ColumnFilter, depth?: number): void;
|
|
26
|
+
/**
|
|
27
|
+
* Validates an array of ColumnFilter objects.
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateColumnFilters(filters: ColumnFilter[]): void;
|
|
30
|
+
//# sourceMappingURL=validate-column-filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-column-filter.d.ts","sourceRoot":"","sources":["../../src/validate-column-filter.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EAEjB,KAAK,cAAc,EAEpB,MAAM,gBAAgB,CAAC;AAIxB;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAElE;AAyBD;;;GAGG;AACH,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC,qBAAa,wBAAyB,SAAQ,KAAK;gBACrC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,SAAI,GAAG,IAAI,CAsG1E;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,IAAI,CAOnE"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { FILTER_OPERATORS, OPERATOR_ALIASES, } from './operators.js';
|
|
2
|
+
const operatorSet = new Set(FILTER_OPERATORS);
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes an operator string to its canonical {@link FilterOperator},
|
|
5
|
+
* resolving SQL-symbol aliases (`=` -> `equals`, `!=` -> `notEquals`, ...).
|
|
6
|
+
* Canonical operators (and unknown strings) pass through unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export function normalizeOperator(operator) {
|
|
9
|
+
return (OPERATOR_ALIASES[operator] ?? operator);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Operators that require no value (unary operators).
|
|
13
|
+
*/
|
|
14
|
+
const UNARY_OPERATORS = new Set([
|
|
15
|
+
'isEmpty',
|
|
16
|
+
'isNotEmpty',
|
|
17
|
+
'isNull',
|
|
18
|
+
'isNotNull',
|
|
19
|
+
'exists',
|
|
20
|
+
'notExists',
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* Operators that require an array value.
|
|
24
|
+
*/
|
|
25
|
+
const ARRAY_OPERATORS = new Set([
|
|
26
|
+
'in',
|
|
27
|
+
'notIn',
|
|
28
|
+
'isAnyOf',
|
|
29
|
+
'between',
|
|
30
|
+
'notBetween',
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* Maximum allowed nesting depth for AND/OR filter composition.
|
|
34
|
+
* Prevents stack overflow from maliciously deep payloads (DoS).
|
|
35
|
+
*/
|
|
36
|
+
export const MAX_FILTER_DEPTH = 10;
|
|
37
|
+
export class InvalidColumnFilterError extends Error {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'InvalidColumnFilterError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validates a ColumnFilter at runtime:
|
|
45
|
+
* - field must be a non-empty string with no SQL-unsafe characters
|
|
46
|
+
* - operator must be a known FilterOperator
|
|
47
|
+
* - value is required for non-unary operators
|
|
48
|
+
* - 'between' requires a 2-element array
|
|
49
|
+
* - 'in'/'isAnyOf' require arrays
|
|
50
|
+
* - AND/OR arrays are validated recursively
|
|
51
|
+
*/
|
|
52
|
+
export function validateColumnFilter(filter, depth = 0) {
|
|
53
|
+
if (depth > MAX_FILTER_DEPTH) {
|
|
54
|
+
throw new InvalidColumnFilterError(`Filter nesting exceeds maximum depth (${MAX_FILTER_DEPTH}).`);
|
|
55
|
+
}
|
|
56
|
+
if (!filter || typeof filter !== 'object') {
|
|
57
|
+
throw new InvalidColumnFilterError('Column filter must be a non-null object.');
|
|
58
|
+
}
|
|
59
|
+
// Pure group node: `{ AND: [...] }` / `{ OR: [...] }` with no column of its
|
|
60
|
+
// own composes child filters and has no field/operator/value to validate.
|
|
61
|
+
// The client builder emits these (sometimes with an empty `field`), so treat
|
|
62
|
+
// an empty field as absent. Validate only the nested arrays, then stop.
|
|
63
|
+
const isGroupNode = (filter.AND !== undefined || filter.OR !== undefined) &&
|
|
64
|
+
(filter.field === undefined || filter.field === '');
|
|
65
|
+
if (isGroupNode) {
|
|
66
|
+
for (const key of ['AND', 'OR']) {
|
|
67
|
+
const group = filter[key];
|
|
68
|
+
if (group !== undefined) {
|
|
69
|
+
if (!Array.isArray(group)) {
|
|
70
|
+
throw new InvalidColumnFilterError(`"${key}" must be an array of ColumnFilter.`);
|
|
71
|
+
}
|
|
72
|
+
for (const sub of group) {
|
|
73
|
+
validateColumnFilter(sub, depth + 1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Validate field
|
|
80
|
+
if (typeof filter.field !== 'string' || filter.field.length === 0) {
|
|
81
|
+
throw new InvalidColumnFilterError('Column filter "field" must be a non-empty string.');
|
|
82
|
+
}
|
|
83
|
+
// Reject field names that contain SQL-unsafe characters
|
|
84
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(filter.field)) {
|
|
85
|
+
throw new InvalidColumnFilterError(`Column filter field "${filter.field}" contains invalid characters. Only letters, digits, underscores, and dots are allowed.`);
|
|
86
|
+
}
|
|
87
|
+
// Validate operator, accepting SQL-symbol aliases (`=`, `!=`, `<`, ...).
|
|
88
|
+
const op = normalizeOperator(filter.operator);
|
|
89
|
+
if (!operatorSet.has(op)) {
|
|
90
|
+
throw new InvalidColumnFilterError(`Unknown filter operator "${filter.operator}". ` +
|
|
91
|
+
`Valid operators: ${FILTER_OPERATORS.join(', ')}.`);
|
|
92
|
+
}
|
|
93
|
+
// Persist the canonical form so downstream query builders never see aliases.
|
|
94
|
+
filter.operator = op;
|
|
95
|
+
// Reject null for non-unary operators (point to isNull instead)
|
|
96
|
+
if (!UNARY_OPERATORS.has(op) && filter.value === null) {
|
|
97
|
+
throw new InvalidColumnFilterError(`Operator "${op}" received null. Use "isNull" for NULL checks.`);
|
|
98
|
+
}
|
|
99
|
+
// Validate value
|
|
100
|
+
if (UNARY_OPERATORS.has(op)) {
|
|
101
|
+
// Unary operators don't need a value — ignore any provided value
|
|
102
|
+
}
|
|
103
|
+
else if (op === 'between' || op === 'notBetween') {
|
|
104
|
+
if (!Array.isArray(filter.value) || filter.value.length !== 2) {
|
|
105
|
+
throw new InvalidColumnFilterError(`Operator "${op}" requires a value that is a 2-element array, got: ${JSON.stringify(filter.value)}.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (ARRAY_OPERATORS.has(op)) {
|
|
109
|
+
if (!Array.isArray(filter.value)) {
|
|
110
|
+
throw new InvalidColumnFilterError(`Operator "${op}" requires an array value, got: ${typeof filter.value}.`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
if (filter.value === undefined) {
|
|
115
|
+
throw new InvalidColumnFilterError(`Operator "${op}" requires a value.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Validate nested AND
|
|
119
|
+
if (filter.AND !== undefined) {
|
|
120
|
+
if (!Array.isArray(filter.AND)) {
|
|
121
|
+
throw new InvalidColumnFilterError('"AND" must be an array of ColumnFilter.');
|
|
122
|
+
}
|
|
123
|
+
for (const sub of filter.AND) {
|
|
124
|
+
validateColumnFilter(sub, depth + 1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Validate nested OR
|
|
128
|
+
if (filter.OR !== undefined) {
|
|
129
|
+
if (!Array.isArray(filter.OR)) {
|
|
130
|
+
throw new InvalidColumnFilterError('"OR" must be an array of ColumnFilter.');
|
|
131
|
+
}
|
|
132
|
+
for (const sub of filter.OR) {
|
|
133
|
+
validateColumnFilter(sub, depth + 1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Validates an array of ColumnFilter objects.
|
|
139
|
+
*/
|
|
140
|
+
export function validateColumnFilters(filters) {
|
|
141
|
+
if (!Array.isArray(filters)) {
|
|
142
|
+
throw new InvalidColumnFilterError('Column filters must be an array.');
|
|
143
|
+
}
|
|
144
|
+
for (const filter of filters) {
|
|
145
|
+
validateColumnFilter(filter);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=validate-column-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-column-filter.js","sourceRoot":"","sources":["../../src/validate-column-filter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ColumnFilter } from './operators.js';
|
|
2
|
+
/**
|
|
3
|
+
* True when `value` is a non-empty plain object whose keys are ALL filter operators — i.e. an
|
|
4
|
+
* operator map like `{ gt: 5, lt: 10 }` rather than a scalar equality value. Arrays and `null` are
|
|
5
|
+
* not operator objects. Single source of the shape classifier every adapter's auto-field path uses.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isOperatorObject(value: unknown): value is Record<string, unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a raw auto-field value to canonical {@link ColumnFilter}[]:
|
|
10
|
+
* - an array → a single `in` filter,
|
|
11
|
+
* - an {@link isOperatorObject operator object} → one filter per operator,
|
|
12
|
+
* - any other (scalar) value → a single `equals` filter.
|
|
13
|
+
*
|
|
14
|
+
* Lets an adapter's auto-field/relation/computed surfaces drive the same `ColumnFilter` path the
|
|
15
|
+
* structured-filter input already uses, instead of re-implementing the scalar/array/object ladder.
|
|
16
|
+
*/
|
|
17
|
+
export declare function valueToColumnFilters(field: string, value: unknown): ColumnFilter[];
|
|
18
|
+
//# sourceMappingURL=value-shape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"value-shape.d.ts","sourceRoot":"","sources":["../../src/value-shape.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAyC,MAAM,gBAAgB,CAAC;AAK1F;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIjF;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,YAAY,EAAE,CAUlF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { FILTER_OPERATORS } from './operators.js';
|
|
2
|
+
/** Every canonical operator name, for the operator-object shape check. */
|
|
3
|
+
const OPERATOR_SET = new Set(FILTER_OPERATORS);
|
|
4
|
+
/**
|
|
5
|
+
* True when `value` is a non-empty plain object whose keys are ALL filter operators — i.e. an
|
|
6
|
+
* operator map like `{ gt: 5, lt: 10 }` rather than a scalar equality value. Arrays and `null` are
|
|
7
|
+
* not operator objects. Single source of the shape classifier every adapter's auto-field path uses.
|
|
8
|
+
*/
|
|
9
|
+
export function isOperatorObject(value) {
|
|
10
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value))
|
|
11
|
+
return false;
|
|
12
|
+
const keys = Object.keys(value);
|
|
13
|
+
return keys.length > 0 && keys.every((k) => OPERATOR_SET.has(k));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Normalize a raw auto-field value to canonical {@link ColumnFilter}[]:
|
|
17
|
+
* - an array → a single `in` filter,
|
|
18
|
+
* - an {@link isOperatorObject operator object} → one filter per operator,
|
|
19
|
+
* - any other (scalar) value → a single `equals` filter.
|
|
20
|
+
*
|
|
21
|
+
* Lets an adapter's auto-field/relation/computed surfaces drive the same `ColumnFilter` path the
|
|
22
|
+
* structured-filter input already uses, instead of re-implementing the scalar/array/object ladder.
|
|
23
|
+
*/
|
|
24
|
+
export function valueToColumnFilters(field, value) {
|
|
25
|
+
if (Array.isArray(value))
|
|
26
|
+
return [{ field, operator: 'in', value }];
|
|
27
|
+
if (isOperatorObject(value)) {
|
|
28
|
+
return Object.entries(value).map(([operator, opVal]) => ({
|
|
29
|
+
field,
|
|
30
|
+
operator: operator,
|
|
31
|
+
value: opVal,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
return [{ field, operator: 'equals', value }];
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=value-shape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"value-shape.js","sourceRoot":"","sources":["../../src/value-shape.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,gBAAgB,EAAuB,MAAM,gBAAgB,CAAC;AAE1F,0EAA0E;AAC1E,MAAM,YAAY,GAAwB,IAAI,GAAG,CAAS,gBAAgB,CAAC,CAAC;AAE5E;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,IAAI,KAAK,IAAI,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACrF,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AACnE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAa,EAAE,KAAc;IAChE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACpE,IAAI,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;YACvD,KAAK;YACL,QAAQ,EAAE,QAA0B;YACpC,KAAK,EAAE,KAAK;SACb,CAAC,CAAC,CAAC;IACN,CAAC;IACD,OAAO,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;AAChD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adonis-agora/filter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Server-side query filtering/sorting/pagination for AdonisJS — Spatie-style input, a Lucid adapter, and field allow-listing. Part of the Agora ecosystem.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/DavideCarvalho/adonis-filter.git",
|
|
9
|
+
"directory": "packages/core"
|
|
10
|
+
},
|
|
11
|
+
"author": "Davi Carvalho <davi@goflip.ai>",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/src/index.js",
|
|
14
|
+
"types": "./dist/src/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/src/index.d.ts",
|
|
18
|
+
"import": "./dist/src/index.js",
|
|
19
|
+
"default": "./dist/src/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./testing": {
|
|
22
|
+
"types": "./dist/src/testing.d.ts",
|
|
23
|
+
"import": "./dist/src/testing.js",
|
|
24
|
+
"default": "./dist/src/testing.js"
|
|
25
|
+
},
|
|
26
|
+
"./commands": {
|
|
27
|
+
"types": "./dist/commands/main.d.ts",
|
|
28
|
+
"import": "./dist/commands/main.js",
|
|
29
|
+
"default": "./dist/commands/main.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist/",
|
|
34
|
+
"README.md",
|
|
35
|
+
"CHANGELOG.md"
|
|
36
|
+
],
|
|
37
|
+
"sideEffects": false,
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@adonisjs/core": "^7.3.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"@adonisjs/core": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@adonisjs/core": "^7.3.0",
|
|
48
|
+
"@types/node": "^20.0.0",
|
|
49
|
+
"typescript": "^5.4.0",
|
|
50
|
+
"vitest": "^3.0.0"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=20.6.0"
|
|
54
|
+
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"adonisjs",
|
|
57
|
+
"adonis",
|
|
58
|
+
"filter",
|
|
59
|
+
"lucid",
|
|
60
|
+
"query",
|
|
61
|
+
"spatie",
|
|
62
|
+
"pagination",
|
|
63
|
+
"agora"
|
|
64
|
+
],
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "tsc -p tsconfig.json",
|
|
67
|
+
"test": "vitest run --passWithNoTests",
|
|
68
|
+
"test:watch": "vitest",
|
|
69
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
70
|
+
}
|
|
71
|
+
}
|