@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,85 @@
|
|
|
1
|
+
import type { ResolvedCursor } from './cursor.js';
|
|
2
|
+
import { type FilterClass } from './filter_class.js';
|
|
3
|
+
import { type FilterSpec } from './filter_spec.js';
|
|
4
|
+
import { type QueryBuilderLike } from './lucid_adapter.js';
|
|
5
|
+
import { type ResolvedPagination } from './runner.js';
|
|
6
|
+
import type { FilterInput } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* The structural slice of an AdonisJS `HttpContext` these helpers read. Only
|
|
9
|
+
* `request.qs()` (the decoded query string) is used to source the raw input;
|
|
10
|
+
* the whole ctx is also handed to a spec's tenant resolver. Declared
|
|
11
|
+
* structurally so the package never hard-imports `@adonisjs/core` — a real
|
|
12
|
+
* `HttpContext` satisfies it, and tests can pass a plain object.
|
|
13
|
+
*/
|
|
14
|
+
export interface FilterRequestContext {
|
|
15
|
+
request?: {
|
|
16
|
+
qs?: () => Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
[key: string]: any;
|
|
19
|
+
}
|
|
20
|
+
/** Options shared by the request-driven helpers. */
|
|
21
|
+
export interface ApplyFromRequestOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Pre-parsed input to use instead of reading `ctx.request.qs()`. Useful for
|
|
24
|
+
* non-HTTP callers and tests; when omitted the query string is parsed via
|
|
25
|
+
* {@link parseFilterRequest}.
|
|
26
|
+
*/
|
|
27
|
+
input?: FilterInput;
|
|
28
|
+
/**
|
|
29
|
+
* Query embedding for pgvector embedding-similarity ranking (distinct from the
|
|
30
|
+
* text `search`). The idiomatic path: the controller computes it from an
|
|
31
|
+
* embedding service and passes it here (rather than shipping a large float
|
|
32
|
+
* array through the query string). Ignored unless the spec declares a
|
|
33
|
+
* `vectorSimilarity` column; merged over any `input.vectorSimilarity`.
|
|
34
|
+
*/
|
|
35
|
+
vectorSimilarity?: readonly number[];
|
|
36
|
+
}
|
|
37
|
+
/** Options for {@link applyCursorFromRequest}. */
|
|
38
|
+
export interface ApplyCursorFromRequestOptions extends ApplyFromRequestOptions {
|
|
39
|
+
/** Primary-key column appended to the keyset as a stable tiebreaker. Default `'id'`. */
|
|
40
|
+
primaryKey?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Apply the server-side, non-client-tamperable scope to the query BEFORE the
|
|
44
|
+
* allow-listed request filters: the tenant constraint (when the spec declares
|
|
45
|
+
* one and a tenant id resolves from ctx) and any `defaultFilters`. These bypass
|
|
46
|
+
* the allow-list on purpose — they are trusted server policy, mirroring how the
|
|
47
|
+
* NestJS runner applied `@TenantScoped` via the adapter's auto-field path.
|
|
48
|
+
*/
|
|
49
|
+
export declare function applyServerScope(query: QueryBuilderLike, spec: FilterSpec, ctx: FilterRequestContext | undefined): void;
|
|
50
|
+
/**
|
|
51
|
+
* Apply a {@link FilterSpec} to a Lucid query from a request context, returning
|
|
52
|
+
* the resolved offset pagination — the single explicit call an AdonisJS
|
|
53
|
+
* controller makes (the idiomatic replacement for the NestJS
|
|
54
|
+
* `ApplyFilterInterceptor` + `@ApplyFilter` param decorator).
|
|
55
|
+
*
|
|
56
|
+
* It resolves the request input from `ctx.request.qs()`, injects the server
|
|
57
|
+
* scope (tenant + default filters) that is never exposed to the allow-list, and
|
|
58
|
+
* delegates the allow-listed filter/sort/search + pagination resolution to
|
|
59
|
+
* {@link applyFilter}.
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* const spec = defineFilter({ filterable: ['name', 'age'], tenant: { column: 'tenantId', resolve: (ctx) => ctx.auth.user.tenantId } })
|
|
63
|
+
*
|
|
64
|
+
* // in a controller:
|
|
65
|
+
* const query = User.query()
|
|
66
|
+
* const { page, size } = applyFilterFromRequest(query, spec, ctx)
|
|
67
|
+
* return query.paginate(page, size)
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare function applyFilterFromRequest(query: QueryBuilderLike, spec: FilterSpec, ctx: FilterRequestContext | undefined, options?: ApplyFromRequestOptions): ResolvedPagination;
|
|
71
|
+
/**
|
|
72
|
+
* The class form: the filter is resolved through the IoC container (so an `@inject()`ed
|
|
73
|
+
* constructor works), its `setup()` runs before anything the request asked for, and every request
|
|
74
|
+
* key the class owns a method for is dispatched to that method instead of the declarative path.
|
|
75
|
+
* Resolution is async, so this overload returns a promise.
|
|
76
|
+
*/
|
|
77
|
+
export declare function applyFilterFromRequest(query: QueryBuilderLike, filter: FilterClass, ctx: FilterRequestContext | undefined, options?: ApplyFromRequestOptions): Promise<ResolvedPagination>;
|
|
78
|
+
/**
|
|
79
|
+
* The keyset (cursor) counterpart of {@link applyFilterFromRequest}. Sources the
|
|
80
|
+
* request filters and the `after`/`before`/`first`/`last` cursor params from ctx,
|
|
81
|
+
* injects the same server scope, and delegates to {@link applyCursor}. Feed the
|
|
82
|
+
* fetched rows and the returned {@link ResolvedCursor} to `buildCursorPage`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function applyCursorFromRequest(query: QueryBuilderLike, spec: FilterSpec, ctx: FilterRequestContext | undefined, options?: ApplyCursorFromRequestOptions): ResolvedCursor;
|
|
85
|
+
//# sourceMappingURL=apply_from_request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply_from_request.d.ts","sourceRoot":"","sources":["../../src/apply_from_request.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAgB,cAAc,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EACL,KAAK,WAAW,EAIjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,UAAU,EAAsB,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAsB,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG/E,OAAO,EAA+C,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACnG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;IAEjD,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,oDAAoD;AACpD,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,kDAAkD;AAClD,MAAM,WAAW,6BAA8B,SAAQ,uBAAuB;IAC5E,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AASD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,oBAAoB,GAAG,SAAS,GACpC,IAAI,CAeN;AAUD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,oBAAoB,GAAG,SAAS,EACrC,OAAO,CAAC,EAAE,uBAAuB,GAChC,kBAAkB,CAAC;AACtB;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,oBAAoB,GAAG,SAAS,EACrC,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CAAC;AAyI/B;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,oBAAoB,GAAG,SAAS,EACrC,OAAO,GAAE,6BAAkC,GAC1C,cAAc,CAahB"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { isFilterClass, methodForKey, specFromFilterClass, } from './filter_class.js';
|
|
2
|
+
import { specToFilterConfig } from './filter_spec.js';
|
|
3
|
+
import { applyColumnFilters } from './lucid_adapter.js';
|
|
4
|
+
import { parseFilterRequest } from './parse_request.js';
|
|
5
|
+
import { applyCursor, applyFilter } from './runner.js';
|
|
6
|
+
/** Read the decoded query string off the ctx (empty object when absent). */
|
|
7
|
+
function rawQs(ctx) {
|
|
8
|
+
const qs = ctx?.request?.qs;
|
|
9
|
+
if (typeof qs === 'function')
|
|
10
|
+
return qs.call(ctx?.request) ?? {};
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Apply the server-side, non-client-tamperable scope to the query BEFORE the
|
|
15
|
+
* allow-listed request filters: the tenant constraint (when the spec declares
|
|
16
|
+
* one and a tenant id resolves from ctx) and any `defaultFilters`. These bypass
|
|
17
|
+
* the allow-list on purpose — they are trusted server policy, mirroring how the
|
|
18
|
+
* NestJS runner applied `@TenantScoped` via the adapter's auto-field path.
|
|
19
|
+
*/
|
|
20
|
+
export function applyServerScope(query, spec, ctx) {
|
|
21
|
+
const serverFilters = [];
|
|
22
|
+
if (spec.tenant) {
|
|
23
|
+
const tenantId = spec.tenant.resolve(ctx);
|
|
24
|
+
if (tenantId !== undefined && tenantId !== null) {
|
|
25
|
+
serverFilters.push({ field: spec.tenant.column, operator: 'equals', value: tenantId });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (spec.defaultFilters.length > 0) {
|
|
29
|
+
serverFilters.push(...spec.defaultFilters);
|
|
30
|
+
}
|
|
31
|
+
if (serverFilters.length > 0) {
|
|
32
|
+
applyColumnFilters(query, serverFilters);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Fall back to the spec's `defaultSort` when the request supplied no sort. */
|
|
36
|
+
function withDefaultSort(input, spec) {
|
|
37
|
+
if ((!input.sort || input.sort.length === 0) && spec.defaultSort.length > 0) {
|
|
38
|
+
return { ...input, sort: [...spec.defaultSort] };
|
|
39
|
+
}
|
|
40
|
+
return input;
|
|
41
|
+
}
|
|
42
|
+
export function applyFilterFromRequest(query, specOrClass, ctx, options = {}) {
|
|
43
|
+
if (isFilterClass(specOrClass)) {
|
|
44
|
+
return applyFilterClass(query, specOrClass, ctx, options);
|
|
45
|
+
}
|
|
46
|
+
return applySpec(query, specOrClass, ctx, options);
|
|
47
|
+
}
|
|
48
|
+
function applySpec(query, spec, ctx, options) {
|
|
49
|
+
const parsed = options.input ?? parseFilterRequest(rawQs(ctx));
|
|
50
|
+
const withVector = options.vectorSimilarity !== undefined
|
|
51
|
+
? { ...parsed, vectorSimilarity: options.vectorSimilarity }
|
|
52
|
+
: parsed;
|
|
53
|
+
applyServerScope(query, spec, ctx);
|
|
54
|
+
return applyFilter(query, withDefaultSort(withVector, spec), specToFilterConfig(spec));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a filter class through the request's IoC container when there is one — the same
|
|
58
|
+
* resolver a controller is constructed with, so `@inject()` on the filter behaves identically —
|
|
59
|
+
* and fall back to plain construction outside an AdonisJS request (a test, a script).
|
|
60
|
+
*/
|
|
61
|
+
async function resolveFilter(cls, ctx) {
|
|
62
|
+
const resolver = ctx?.containerResolver;
|
|
63
|
+
if (resolver && typeof resolver.make === 'function') {
|
|
64
|
+
return (await resolver.make(cls));
|
|
65
|
+
}
|
|
66
|
+
return new cls();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Run a filter class against the query: bind the per-request state onto the instance, run
|
|
70
|
+
* `setup()`, apply everything the class declared statically through the ordinary spec path, then
|
|
71
|
+
* hand each key the class owns to its own method.
|
|
72
|
+
*
|
|
73
|
+
* The split matters: a key with a method is removed from the declarative input **before** the
|
|
74
|
+
* runner sees it, so `fullName` never becomes `where "fullName" = ?` on a column that may not
|
|
75
|
+
* exist — the method is the only thing that touches it.
|
|
76
|
+
*/
|
|
77
|
+
async function applyFilterClass(query, cls, ctx, options) {
|
|
78
|
+
const raw = rawQs(ctx);
|
|
79
|
+
const parsed = options.input ?? parseFilterRequest(raw);
|
|
80
|
+
const instance = await resolveFilter(cls, ctx);
|
|
81
|
+
const owned = [];
|
|
82
|
+
const rest = [];
|
|
83
|
+
for (const condition of parsed.filters ?? []) {
|
|
84
|
+
const method = methodForKey(cls, condition.field);
|
|
85
|
+
if (method === undefined) {
|
|
86
|
+
rest.push(condition);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
owned.push({ method, value: condition.value, operator: condition.operator });
|
|
90
|
+
}
|
|
91
|
+
// A bare top-level key (`?minAge=21`) reaches a method of the same name too — the shape
|
|
92
|
+
// `adonis-lucid-filter` dispatches on — as long as the wire format does not own the key.
|
|
93
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
94
|
+
const method = methodForKey(cls, key);
|
|
95
|
+
if (method !== undefined && !owned.some((entry) => entry.method === method)) {
|
|
96
|
+
owned.push({ method, value, operator: 'equals' });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
Object.assign(instance, {
|
|
100
|
+
$query: query,
|
|
101
|
+
$input: raw,
|
|
102
|
+
$parsed: parsed,
|
|
103
|
+
$ctx: ctx,
|
|
104
|
+
});
|
|
105
|
+
await instance.setup?.();
|
|
106
|
+
const pagination = applySpec(query, specFromFilterClass(cls), ctx, {
|
|
107
|
+
...options,
|
|
108
|
+
input: { ...parsed, filters: rest },
|
|
109
|
+
});
|
|
110
|
+
for (const { method, value, operator } of owned) {
|
|
111
|
+
const fn = instance[method];
|
|
112
|
+
if (typeof fn === 'function') {
|
|
113
|
+
await fn.call(instance, value, operator);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return pagination;
|
|
117
|
+
}
|
|
118
|
+
/** Parse cursor (keyset) params from a decoded query string (Spatie `page[...]` shapes). */
|
|
119
|
+
function parseCursorParams(qs) {
|
|
120
|
+
const out = {};
|
|
121
|
+
const page = qs.page != null && typeof qs.page === 'object' && !Array.isArray(qs.page)
|
|
122
|
+
? qs.page
|
|
123
|
+
: undefined;
|
|
124
|
+
const after = qs.after ?? page?.after;
|
|
125
|
+
const before = qs.before ?? page?.before;
|
|
126
|
+
if (typeof after === 'string' && after.length > 0)
|
|
127
|
+
out.after = after;
|
|
128
|
+
else if (typeof before === 'string' && before.length > 0)
|
|
129
|
+
out.before = before;
|
|
130
|
+
const first = qs.first ?? page?.first;
|
|
131
|
+
const last = qs.last ?? page?.last;
|
|
132
|
+
const toInt = (v) => {
|
|
133
|
+
const n = typeof v === 'number' ? v : typeof v === 'string' && v.trim() !== '' ? Number(v) : Number.NaN;
|
|
134
|
+
return Number.isFinite(n) ? n : undefined;
|
|
135
|
+
};
|
|
136
|
+
const f = toInt(first);
|
|
137
|
+
const l = toInt(last);
|
|
138
|
+
if (f !== undefined)
|
|
139
|
+
out.first = f;
|
|
140
|
+
if (l !== undefined)
|
|
141
|
+
out.last = l;
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The keyset (cursor) counterpart of {@link applyFilterFromRequest}. Sources the
|
|
146
|
+
* request filters and the `after`/`before`/`first`/`last` cursor params from ctx,
|
|
147
|
+
* injects the same server scope, and delegates to {@link applyCursor}. Feed the
|
|
148
|
+
* fetched rows and the returned {@link ResolvedCursor} to `buildCursorPage`.
|
|
149
|
+
*/
|
|
150
|
+
export function applyCursorFromRequest(query, spec, ctx, options = {}) {
|
|
151
|
+
const qs = rawQs(ctx);
|
|
152
|
+
const parsed = options.input ?? parseFilterRequest(qs);
|
|
153
|
+
const full = {
|
|
154
|
+
...withDefaultSort(parsed, spec),
|
|
155
|
+
...parseCursorParams(qs),
|
|
156
|
+
};
|
|
157
|
+
applyServerScope(query, spec, ctx);
|
|
158
|
+
const config = {
|
|
159
|
+
...specToFilterConfig(spec),
|
|
160
|
+
...(options.primaryKey !== undefined && { primaryKey: options.primaryKey }),
|
|
161
|
+
};
|
|
162
|
+
return applyCursor(query, full, config);
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=apply_from_request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply_from_request.js","sourceRoot":"","sources":["../../src/apply_from_request.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { FilterInput } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The class-authoring form of a filter over ANY backend — the generic base behind
|
|
4
|
+
* {@link import('./base_model_filter.js').BaseModelFilter} (Lucid) and custom backends
|
|
5
|
+
* (in-memory stores, engine clients, predicate bags) alike: **one style that does everything**.
|
|
6
|
+
*
|
|
7
|
+
* A method's name IS the key it owns, `setup()` runs on every call before anything the request
|
|
8
|
+
* asked for, and the class is resolved through the IoC container so an `@inject()`ed constructor
|
|
9
|
+
* works — exactly the shape AdonisJS developers know from `adonis-lucid-filter`. What differs per
|
|
10
|
+
* backend is only what `$query` IS: a Lucid builder for models, a draft the methods narrow for
|
|
11
|
+
* anything else.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* export class RunFilter extends BaseFilter<RunQueryDraft> {
|
|
15
|
+
* declare $query: RunQueryDraft;
|
|
16
|
+
*
|
|
17
|
+
* status(value: unknown, operator: string) {
|
|
18
|
+
* const values = (Array.isArray(value) ? value : [value]).map(String);
|
|
19
|
+
* this.$query.narrow(values.length === 1 ? { status: values[0] } : { statuses: values });
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* const draft = new RunQueryDraft();
|
|
24
|
+
* await applyCustomFilter(draft, RunFilter, ctx);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare abstract class BaseFilter<TQuery> {
|
|
28
|
+
/**
|
|
29
|
+
* The query being filtered — the one the caller created. Methods mutate it directly; nothing
|
|
30
|
+
* here constructs or executes a query.
|
|
31
|
+
*/
|
|
32
|
+
$query: TQuery;
|
|
33
|
+
/** The raw decoded request input the dispatch reads (query string, or the body on a POST). */
|
|
34
|
+
$input: Readonly<Record<string, unknown>>;
|
|
35
|
+
/** The parsed input — filters, sort, search, pagination — after the wire format is read. */
|
|
36
|
+
$parsed: FilterInput;
|
|
37
|
+
/** The request context handed to the call (a real `HttpContext` in an AdonisJS app). */
|
|
38
|
+
$ctx: unknown;
|
|
39
|
+
/**
|
|
40
|
+
* Runs on every filter call, before anything the request asked for — the place for defaults
|
|
41
|
+
* and for constraints the client must not be able to relax.
|
|
42
|
+
*/
|
|
43
|
+
setup?(): void | Promise<void>;
|
|
44
|
+
/** The whole raw input, one key of it, or a fallback when the key is absent. */
|
|
45
|
+
input(): Readonly<Record<string, unknown>>;
|
|
46
|
+
input(key: string): unknown;
|
|
47
|
+
input(key: string, fallback: unknown): unknown;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=base_filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base_filter.d.ts","sourceRoot":"","sources":["../../src/base_filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,8BAAsB,UAAU,CAAC,MAAM;IACrC;;;OAGG;IACK,MAAM,EAAE,MAAM,CAAC;IAEvB,8FAA8F;IACtF,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAElD,4FAA4F;IACpF,OAAO,EAAE,WAAW,CAAC;IAE7B,wFAAwF;IAChF,IAAI,EAAE,OAAO,CAAC;IAEtB;;;OAGG;IACH,KAAK,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/B,gFAAgF;IAChF,KAAK,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3C,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC;CAKhD"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The class-authoring form of a filter over ANY backend — the generic base behind
|
|
3
|
+
* {@link import('./base_model_filter.js').BaseModelFilter} (Lucid) and custom backends
|
|
4
|
+
* (in-memory stores, engine clients, predicate bags) alike: **one style that does everything**.
|
|
5
|
+
*
|
|
6
|
+
* A method's name IS the key it owns, `setup()` runs on every call before anything the request
|
|
7
|
+
* asked for, and the class is resolved through the IoC container so an `@inject()`ed constructor
|
|
8
|
+
* works — exactly the shape AdonisJS developers know from `adonis-lucid-filter`. What differs per
|
|
9
|
+
* backend is only what `$query` IS: a Lucid builder for models, a draft the methods narrow for
|
|
10
|
+
* anything else.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* export class RunFilter extends BaseFilter<RunQueryDraft> {
|
|
14
|
+
* declare $query: RunQueryDraft;
|
|
15
|
+
*
|
|
16
|
+
* status(value: unknown, operator: string) {
|
|
17
|
+
* const values = (Array.isArray(value) ? value : [value]).map(String);
|
|
18
|
+
* this.$query.narrow(values.length === 1 ? { status: values[0] } : { statuses: values });
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* const draft = new RunQueryDraft();
|
|
23
|
+
* await applyCustomFilter(draft, RunFilter, ctx);
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export class BaseFilter {
|
|
27
|
+
input(key, fallback) {
|
|
28
|
+
if (key === undefined)
|
|
29
|
+
return this.$input;
|
|
30
|
+
return Object.hasOwn(this.$input, key) ? this.$input[key] : fallback;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=base_filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base_filter.js","sourceRoot":"","sources":["../../src/base_filter.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,OAAgB,UAAU;IA0B9B,KAAK,CAAC,GAAY,EAAE,QAAkB;QACpC,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,MAAM,CAAC;QAC1C,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IACvE,CAAC;CACF"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { LucidModelLike } from './aggregate.js';
|
|
2
|
+
import { BaseFilter } from './base_filter.js';
|
|
3
|
+
import type { FieldAliases } from './field_aliases.js';
|
|
4
|
+
import type { FilterableInput, RelationColumns, RelationSpec, TenantScopeSpec } from './filter_spec.js';
|
|
5
|
+
import type { FilterFieldTypeInfo } from './generate_client.js';
|
|
6
|
+
import type { QueryBuilderLike } from './lucid_adapter.js';
|
|
7
|
+
import type { ColumnFilter } from './operators.js';
|
|
8
|
+
import type { ComputedFields, FullTextSearchConfig, SortItem, VectorSimilarityConfig } from './types.js';
|
|
9
|
+
/**
|
|
10
|
+
* The class-authoring form of a filter — the counterpart of `defineFilter`, in the shape
|
|
11
|
+
* AdonisJS developers already know from `adonis-lucid-filter`: **a method per request key**,
|
|
12
|
+
* with the query builder on `this.$query`.
|
|
13
|
+
*
|
|
14
|
+
* A method's name IS the key it owns. `?filter[fullName]=silva` calls `fullName('silva')`, and
|
|
15
|
+
* because you wrote the method, the key needs no allow-list entry — writing the code is the
|
|
16
|
+
* decision to expose it. Plain columns that need no custom SQL stay declarative: list them in
|
|
17
|
+
* `static filterable` and the runner applies the operators, search, sort and pagination for you.
|
|
18
|
+
*
|
|
19
|
+
* The class is resolved through the IoC container, so an `@inject()`ed constructor works: a
|
|
20
|
+
* filter can depend on a service exactly like a controller does.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* @inject()
|
|
24
|
+
* export default class UserFilter extends BaseModelFilter {
|
|
25
|
+
* declare $query: ModelQueryBuilderContract<typeof User>
|
|
26
|
+
*
|
|
27
|
+
* constructor(private tenants: TenantsService) { super() }
|
|
28
|
+
*
|
|
29
|
+
* static model = User
|
|
30
|
+
* static filterable = ['name', 'email', 'status']
|
|
31
|
+
* static sortable = ['name', 'createdAt']
|
|
32
|
+
* static searchable = ['name', 'email']
|
|
33
|
+
*
|
|
34
|
+
* // always applied, before anything the request asked for
|
|
35
|
+
* setup() {
|
|
36
|
+
* this.$query.where('tenantId', this.tenants.current(this.$ctx))
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* // the one key this class owns itself
|
|
40
|
+
* fullName(value: string) {
|
|
41
|
+
* this.$query.whereILike('full_name', `%${value}%`)
|
|
42
|
+
* }
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare abstract class BaseModelFilter<Q extends QueryBuilderLike = QueryBuilderLike> extends BaseFilter<Q> {
|
|
47
|
+
/** The owning Lucid model. Unlocks to-many aggregates and supplies the root table name. */
|
|
48
|
+
static model?: LucidModelLike;
|
|
49
|
+
/** Columns clients may filter on with the standard operators. `'*'` allows any base column. */
|
|
50
|
+
static filterable?: FilterableInput;
|
|
51
|
+
/** Columns clients may sort on. Defaults to `filterable`. */
|
|
52
|
+
static sortable?: RelationColumns;
|
|
53
|
+
/** Columns the free-text `search` term scans with a portable ILIKE. */
|
|
54
|
+
static searchable?: string[];
|
|
55
|
+
/** Whitelisted relations and the columns reachable through them. */
|
|
56
|
+
static relations?: Record<string, RelationSpec>;
|
|
57
|
+
/** Client-alias → target-field remapping. */
|
|
58
|
+
static aliases?: FieldAliases;
|
|
59
|
+
/** Virtual fields — alias → SQL expression. */
|
|
60
|
+
static computed?: ComputedFields;
|
|
61
|
+
/** Per-field value kinds, driving coercion and typed client codegen. */
|
|
62
|
+
static fieldTypes?: Record<string, FilterFieldTypeInfo>;
|
|
63
|
+
/** Opt-in Postgres tsvector search for the `search` term. */
|
|
64
|
+
static fullText?: FullTextSearchConfig;
|
|
65
|
+
/** Opt-in pgvector embedding-similarity ordering. */
|
|
66
|
+
static vectorSimilarity?: VectorSimilarityConfig;
|
|
67
|
+
/** Auto-scope a column to the tenant resolved from ctx (the declarative form of `setup`). */
|
|
68
|
+
static tenant?: TenantScopeSpec;
|
|
69
|
+
/** Server-declared filters always AND-combined with the request. */
|
|
70
|
+
static defaultFilters?: ColumnFilter[];
|
|
71
|
+
/** Sort applied when the request carries none. */
|
|
72
|
+
static defaultSort?: SortItem[];
|
|
73
|
+
/** Page size when the request asks for none. Default 25. */
|
|
74
|
+
static defaultSize?: number;
|
|
75
|
+
/** Hard cap on page size. Default 100. */
|
|
76
|
+
static maxSize?: number;
|
|
77
|
+
/** Max relation-path hops. Defaults to the deepest declared relation nesting. */
|
|
78
|
+
static maxDepth?: number;
|
|
79
|
+
/** Root table name — defaults to `model.table`. */
|
|
80
|
+
static table?: string;
|
|
81
|
+
/** Throw `InvalidColumnFilterError` on a disallowed field instead of dropping it. */
|
|
82
|
+
static throwOnInvalid?: boolean;
|
|
83
|
+
/** Method names that must never be dispatched from request input, whatever the key says. */
|
|
84
|
+
static blacklist?: string[];
|
|
85
|
+
/** Strip a trailing `Id` from an input key before looking for a method (`companyId` → `company`). */
|
|
86
|
+
static dropId?: boolean;
|
|
87
|
+
/** Match `snake_case` input keys against camelCase methods (`first_name` → `firstName`). Default true. */
|
|
88
|
+
static camelCase?: boolean;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=base_model_filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base_model_filter.d.ts","sourceRoot":"","sources":["../../src/base_model_filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EACV,cAAc,EACd,oBAAoB,EACpB,QAAQ,EACR,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,8BAAsB,eAAe,CACnC,CAAC,SAAS,gBAAgB,GAAG,gBAAgB,CAC7C,SAAQ,UAAU,CAAC,CAAC,CAAC;IAGrB,2FAA2F;IAC3F,MAAM,CAAC,KAAK,CAAC,EAAE,cAAc,CAAC;IAC9B,+FAA+F;IAC/F,MAAM,CAAC,UAAU,CAAC,EAAE,eAAe,CAAC;IACpC,6DAA6D;IAC7D,MAAM,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;IAClC,uEAAuE;IACvE,MAAM,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,oEAAoE;IACpE,MAAM,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAChD,6CAA6C;IAC7C,MAAM,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC;IAC9B,+CAA+C;IAC/C,MAAM,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;IACjC,wEAAwE;IACxE,MAAM,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IACxD,6DAA6D;IAC7D,MAAM,CAAC,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IACvC,qDAAqD;IACrD,MAAM,CAAC,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;IACjD,6FAA6F;IAC7F,MAAM,CAAC,MAAM,CAAC,EAAE,eAAe,CAAC;IAChC,oEAAoE;IACpE,MAAM,CAAC,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;IACvC,kDAAkD;IAClD,MAAM,CAAC,WAAW,CAAC,EAAE,QAAQ,EAAE,CAAC;IAChC,4DAA4D;IAC5D,MAAM,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC5B,0CAA0C;IAC1C,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IACxB,iFAAiF;IACjF,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IACzB,mDAAmD;IACnD,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACtB,qFAAqF;IACrF,MAAM,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAIhC,4FAA4F;IAC5F,MAAM,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,qGAAqG;IACrG,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IACxB,0GAA0G;IAC1G,MAAM,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC5B"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { BaseFilter } from './base_filter.js';
|
|
2
|
+
/**
|
|
3
|
+
* The class-authoring form of a filter — the counterpart of `defineFilter`, in the shape
|
|
4
|
+
* AdonisJS developers already know from `adonis-lucid-filter`: **a method per request key**,
|
|
5
|
+
* with the query builder on `this.$query`.
|
|
6
|
+
*
|
|
7
|
+
* A method's name IS the key it owns. `?filter[fullName]=silva` calls `fullName('silva')`, and
|
|
8
|
+
* because you wrote the method, the key needs no allow-list entry — writing the code is the
|
|
9
|
+
* decision to expose it. Plain columns that need no custom SQL stay declarative: list them in
|
|
10
|
+
* `static filterable` and the runner applies the operators, search, sort and pagination for you.
|
|
11
|
+
*
|
|
12
|
+
* The class is resolved through the IoC container, so an `@inject()`ed constructor works: a
|
|
13
|
+
* filter can depend on a service exactly like a controller does.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* @inject()
|
|
17
|
+
* export default class UserFilter extends BaseModelFilter {
|
|
18
|
+
* declare $query: ModelQueryBuilderContract<typeof User>
|
|
19
|
+
*
|
|
20
|
+
* constructor(private tenants: TenantsService) { super() }
|
|
21
|
+
*
|
|
22
|
+
* static model = User
|
|
23
|
+
* static filterable = ['name', 'email', 'status']
|
|
24
|
+
* static sortable = ['name', 'createdAt']
|
|
25
|
+
* static searchable = ['name', 'email']
|
|
26
|
+
*
|
|
27
|
+
* // always applied, before anything the request asked for
|
|
28
|
+
* setup() {
|
|
29
|
+
* this.$query.where('tenantId', this.tenants.current(this.$ctx))
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* // the one key this class owns itself
|
|
33
|
+
* fullName(value: string) {
|
|
34
|
+
* this.$query.whereILike('full_name', `%${value}%`)
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class BaseModelFilter extends BaseFilter {
|
|
40
|
+
// ── the declarative half: everything a plain column needs, without writing a method ──────────
|
|
41
|
+
/** The owning Lucid model. Unlocks to-many aggregates and supplies the root table name. */
|
|
42
|
+
static model;
|
|
43
|
+
/** Columns clients may filter on with the standard operators. `'*'` allows any base column. */
|
|
44
|
+
static filterable;
|
|
45
|
+
/** Columns clients may sort on. Defaults to `filterable`. */
|
|
46
|
+
static sortable;
|
|
47
|
+
/** Columns the free-text `search` term scans with a portable ILIKE. */
|
|
48
|
+
static searchable;
|
|
49
|
+
/** Whitelisted relations and the columns reachable through them. */
|
|
50
|
+
static relations;
|
|
51
|
+
/** Client-alias → target-field remapping. */
|
|
52
|
+
static aliases;
|
|
53
|
+
/** Virtual fields — alias → SQL expression. */
|
|
54
|
+
static computed;
|
|
55
|
+
/** Per-field value kinds, driving coercion and typed client codegen. */
|
|
56
|
+
static fieldTypes;
|
|
57
|
+
/** Opt-in Postgres tsvector search for the `search` term. */
|
|
58
|
+
static fullText;
|
|
59
|
+
/** Opt-in pgvector embedding-similarity ordering. */
|
|
60
|
+
static vectorSimilarity;
|
|
61
|
+
/** Auto-scope a column to the tenant resolved from ctx (the declarative form of `setup`). */
|
|
62
|
+
static tenant;
|
|
63
|
+
/** Server-declared filters always AND-combined with the request. */
|
|
64
|
+
static defaultFilters;
|
|
65
|
+
/** Sort applied when the request carries none. */
|
|
66
|
+
static defaultSort;
|
|
67
|
+
/** Page size when the request asks for none. Default 25. */
|
|
68
|
+
static defaultSize;
|
|
69
|
+
/** Hard cap on page size. Default 100. */
|
|
70
|
+
static maxSize;
|
|
71
|
+
/** Max relation-path hops. Defaults to the deepest declared relation nesting. */
|
|
72
|
+
static maxDepth;
|
|
73
|
+
/** Root table name — defaults to `model.table`. */
|
|
74
|
+
static table;
|
|
75
|
+
/** Throw `InvalidColumnFilterError` on a disallowed field instead of dropping it. */
|
|
76
|
+
static throwOnInvalid;
|
|
77
|
+
// ── dispatch controls (the `adonis-lucid-filter` knobs) ──────────────────────────────────────
|
|
78
|
+
/** Method names that must never be dispatched from request input, whatever the key says. */
|
|
79
|
+
static blacklist;
|
|
80
|
+
/** Strip a trailing `Id` from an input key before looking for a method (`companyId` → `company`). */
|
|
81
|
+
static dropId;
|
|
82
|
+
/** Match `snake_case` input keys against camelCase methods (`first_name` → `firstName`). Default true. */
|
|
83
|
+
static camelCase;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=base_model_filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base_model_filter.js","sourceRoot":"","sources":["../../src/base_model_filter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAkB9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,OAAgB,eAEpB,SAAQ,UAAa;IACrB,gGAAgG;IAEhG,2FAA2F;IAC3F,MAAM,CAAC,KAAK,CAAkB;IAC9B,+FAA+F;IAC/F,MAAM,CAAC,UAAU,CAAmB;IACpC,6DAA6D;IAC7D,MAAM,CAAC,QAAQ,CAAmB;IAClC,uEAAuE;IACvE,MAAM,CAAC,UAAU,CAAY;IAC7B,oEAAoE;IACpE,MAAM,CAAC,SAAS,CAAgC;IAChD,6CAA6C;IAC7C,MAAM,CAAC,OAAO,CAAgB;IAC9B,+CAA+C;IAC/C,MAAM,CAAC,QAAQ,CAAkB;IACjC,wEAAwE;IACxE,MAAM,CAAC,UAAU,CAAuC;IACxD,6DAA6D;IAC7D,MAAM,CAAC,QAAQ,CAAwB;IACvC,qDAAqD;IACrD,MAAM,CAAC,gBAAgB,CAA0B;IACjD,6FAA6F;IAC7F,MAAM,CAAC,MAAM,CAAmB;IAChC,oEAAoE;IACpE,MAAM,CAAC,cAAc,CAAkB;IACvC,kDAAkD;IAClD,MAAM,CAAC,WAAW,CAAc;IAChC,4DAA4D;IAC5D,MAAM,CAAC,WAAW,CAAU;IAC5B,0CAA0C;IAC1C,MAAM,CAAC,OAAO,CAAU;IACxB,iFAAiF;IACjF,MAAM,CAAC,QAAQ,CAAU;IACzB,mDAAmD;IACnD,MAAM,CAAC,KAAK,CAAU;IACtB,qFAAqF;IACrF,MAAM,CAAC,cAAc,CAAW;IAEhC,gGAAgG;IAEhG,4FAA4F;IAC5F,MAAM,CAAC,SAAS,CAAY;IAC5B,qGAAqG;IACrG,MAAM,CAAC,MAAM,CAAW;IACxB,0GAA0G;IAC1G,MAAM,CAAC,SAAS,CAAW;CAC5B"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FilterFieldKind } from './types.js';
|
|
2
|
+
/** Outcome of coercing one filter value against its declared {@link FilterFieldKind}. */
|
|
3
|
+
export type CoerceResult = {
|
|
4
|
+
ok: true;
|
|
5
|
+
value: unknown;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
reason: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Coerce a raw filter value to its declared column kind, or reject it.
|
|
12
|
+
*
|
|
13
|
+
* Why this exists: a filter value arriving over a query string is ALWAYS a string
|
|
14
|
+
* (`?filter[dayOfWeek][equals]=3` yields `'3'`). Postgres papers over the benign cases with an
|
|
15
|
+
* implicit cast — `day_of_week = '3'` and `is_recurring = 'false'` both work — so the gap is
|
|
16
|
+
* invisible until a client sends something that ISN'T castable: `is_recurring = 'xyz'` raises
|
|
17
|
+
* `invalid input syntax for type boolean` at the database, which surfaces as a 500 on a public
|
|
18
|
+
* endpoint from pure user input. The allow-list guards which FIELD may be filtered; this guards
|
|
19
|
+
* what VALUE may reach the column.
|
|
20
|
+
*
|
|
21
|
+
* Rejection is not an exception here: the caller maps it onto the spec's existing `throwOnInvalid`
|
|
22
|
+
* semantics, so a bad value behaves exactly like a disallowed field — dropped by default, or a
|
|
23
|
+
* loud `InvalidColumnFilterError` (→ 400) when the spec asks for it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function coerceFilterValue(value: unknown, kind: FilterFieldKind): CoerceResult;
|
|
26
|
+
//# sourceMappingURL=coerce_value.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coerce_value.d.ts","sourceRoot":"","sources":["../../src/coerce_value.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,yFAAyF;AACzF,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAExF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,GAAG,YAAY,CAgErF"}
|