@adonis-agora/filter 0.7.0 → 0.8.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/commands/main.d.ts +5 -3
  3. package/dist/commands/main.d.ts.map +1 -1
  4. package/dist/commands/main.js +4 -2
  5. package/dist/commands/main.js.map +1 -1
  6. package/dist/commands/make_filter.d.ts +18 -0
  7. package/dist/commands/make_filter.d.ts.map +1 -0
  8. package/dist/commands/make_filter.js +32 -0
  9. package/dist/commands/make_filter.js.map +1 -0
  10. package/dist/src/apply_from_request.d.ts +8 -0
  11. package/dist/src/apply_from_request.d.ts.map +1 -1
  12. package/dist/src/apply_from_request.js +70 -21
  13. package/dist/src/apply_from_request.js.map +1 -1
  14. package/dist/src/base_model_filter.d.ts +109 -0
  15. package/dist/src/base_model_filter.d.ts.map +1 -0
  16. package/dist/src/base_model_filter.js +89 -0
  17. package/dist/src/base_model_filter.js.map +1 -0
  18. package/dist/src/decorator_metadata.d.ts +41 -0
  19. package/dist/src/decorator_metadata.d.ts.map +1 -0
  20. package/dist/src/decorator_metadata.js +83 -0
  21. package/dist/src/decorator_metadata.js.map +1 -0
  22. package/dist/src/decorators.d.ts +73 -0
  23. package/dist/src/decorators.d.ts.map +1 -0
  24. package/dist/src/decorators.js +118 -0
  25. package/dist/src/decorators.js.map +1 -0
  26. package/dist/src/filter_class.d.ts +46 -0
  27. package/dist/src/filter_class.d.ts.map +1 -0
  28. package/dist/src/filter_class.js +238 -0
  29. package/dist/src/filter_class.js.map +1 -0
  30. package/dist/src/filterable_mixin.d.ts +49 -0
  31. package/dist/src/filterable_mixin.d.ts.map +1 -0
  32. package/dist/src/filterable_mixin.js +46 -0
  33. package/dist/src/filterable_mixin.js.map +1 -0
  34. package/dist/src/index.d.ts +8 -1
  35. package/dist/src/index.d.ts.map +1 -1
  36. package/dist/src/index.js +5 -1
  37. package/dist/src/index.js.map +1 -1
  38. package/dist/src/lucid_macros.d.ts +31 -0
  39. package/dist/src/lucid_macros.d.ts.map +1 -1
  40. package/dist/src/lucid_macros.js +28 -4
  41. package/dist/src/lucid_macros.js.map +1 -1
  42. package/dist/src/pagination_stash.d.ts +4 -0
  43. package/dist/src/pagination_stash.d.ts.map +1 -0
  44. package/dist/src/pagination_stash.js +19 -0
  45. package/dist/src/pagination_stash.js.map +1 -0
  46. package/dist/stubs/main.d.ts +3 -0
  47. package/dist/stubs/main.d.ts.map +1 -0
  48. package/dist/stubs/main.js +4 -0
  49. package/dist/stubs/main.js.map +1 -0
  50. package/dist/stubs/make/filter/main.stub +45 -0
  51. package/package.json +10 -5
  52. package/skills/filter-basics/SKILL.md +248 -0
  53. package/skills/filter-codegen/SKILL.md +246 -0
  54. package/skills/filter-definitions/SKILL.md +337 -0
  55. package/skills/filter-querying/SKILL.md +319 -0
  56. package/skills/filter-safety/SKILL.md +304 -0
  57. package/stubs/main.ts +4 -0
  58. package/stubs/make/filter/main.stub +45 -0
@@ -0,0 +1,83 @@
1
+ /** The key the record is filed under inside a standard decorator's metadata bag. */
2
+ const NAMESPACE = '@adonis-agora/filter';
3
+ /** Legacy channel: prototype (or model prototype) → record. */
4
+ const LEGACY = new WeakMap();
5
+ /**
6
+ * The symbols a standard decorator's metadata bag may be stamped on: the real `Symbol.metadata`
7
+ * when the runtime (or a polyfill) has it, and the registered fallback TypeScript's emit uses
8
+ * otherwise. Resolved on every read rather than captured once, so a polyfill installed after this
9
+ * module loads is still seen.
10
+ */
11
+ function metadataSymbols() {
12
+ const native = Symbol.metadata;
13
+ const fallback = Symbol.for('Symbol.metadata');
14
+ return native && native !== fallback ? [native, fallback] : [fallback];
15
+ }
16
+ /** The record a legacy decorator writes into, created on first use. */
17
+ export function legacyRecord(prototype) {
18
+ const existing = LEGACY.get(prototype);
19
+ if (existing)
20
+ return existing;
21
+ const record = {};
22
+ LEGACY.set(prototype, record);
23
+ return record;
24
+ }
25
+ /**
26
+ * The record a standard decorator writes into. The bag a subclass receives *inherits* from its
27
+ * base's, so the record is defined as an **own** property — writing through the inherited one
28
+ * would leak a subclass's bindings back onto the parent.
29
+ */
30
+ export function standardRecord(metadata) {
31
+ if (!Object.hasOwn(metadata, NAMESPACE)) {
32
+ Object.defineProperty(metadata, NAMESPACE, {
33
+ value: {},
34
+ enumerable: false,
35
+ writable: true,
36
+ configurable: true,
37
+ });
38
+ }
39
+ return metadata[NAMESPACE];
40
+ }
41
+ /** Every record that applies to a class, base first — so a subclass's declaration wins. */
42
+ function recordsFor(cls) {
43
+ const found = [];
44
+ const prototype = cls.prototype;
45
+ let proto = prototype ?? null;
46
+ while (proto !== null && proto !== Object.prototype) {
47
+ const record = LEGACY.get(proto);
48
+ if (record)
49
+ found.push(record);
50
+ proto = Object.getPrototypeOf(proto);
51
+ }
52
+ for (const symbol of metadataSymbols()) {
53
+ let bag = cls[symbol] ?? null;
54
+ while (bag !== null) {
55
+ if (Object.hasOwn(bag, NAMESPACE)) {
56
+ found.push(bag[NAMESPACE]);
57
+ }
58
+ bag = Object.getPrototypeOf(bag);
59
+ }
60
+ }
61
+ return found.reverse();
62
+ }
63
+ /**
64
+ * The merged declaration for a class — its own decorators layered over everything it inherits.
65
+ *
66
+ * Not memoized: decorators run at class-definition time, but a model's columns and a filter's
67
+ * methods are read once per compiled spec, which is itself cached by the caller.
68
+ */
69
+ export function readDecorators(cls) {
70
+ const merged = {};
71
+ for (const record of recordsFor(cls)) {
72
+ if (record.filterFor)
73
+ merged.filterFor = { ...merged.filterFor, ...record.filterFor };
74
+ if (record.filterable)
75
+ merged.filterable = { ...merged.filterable, ...record.filterable };
76
+ if (record.sortable)
77
+ merged.sortable = [...(merged.sortable ?? []), ...record.sortable];
78
+ if (record.searchable)
79
+ merged.searchable = [...(merged.searchable ?? []), ...record.searchable];
80
+ }
81
+ return merged;
82
+ }
83
+ //# sourceMappingURL=decorator_metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorator_metadata.js","sourceRoot":"","sources":["../../src/decorator_metadata.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import type { FilterFieldKind } from './types.js';
2
+ /**
3
+ * A decorator that works under both flavours — the legacy `(prototype, key, descriptor)` form
4
+ * AdonisJS compiles today (`experimentalDecorators`, which Lucid's `@column` requires) and the
5
+ * TC39 standard `(value, context)` form TypeScript 5 emits when that flag is off. Which one runs
6
+ * is decided at call time, from the shape of the second argument.
7
+ */
8
+ export interface DualDecorator {
9
+ (target: object, propertyKey: string | symbol, descriptor?: PropertyDescriptor): void;
10
+ (value: unknown, context: StandardDecoratorContext): void;
11
+ }
12
+ /** The slice of a TC39 decorator context this library reads. */
13
+ export interface StandardDecoratorContext {
14
+ kind: string;
15
+ name: string | symbol;
16
+ metadata?: object;
17
+ }
18
+ /**
19
+ * Bind a filter method to the request key(s) it answers, instead of deriving the key from the
20
+ * method's name.
21
+ *
22
+ * Worth reaching for exactly twice: when the key cannot be a method name (`team.name`), and when
23
+ * one method should answer several keys. It also pins the public contract — with a binding, the
24
+ * key survives renaming the method.
25
+ *
26
+ * ```ts
27
+ * export default class UserFilter extends BaseModelFilter {
28
+ * @filterFor('team.name')
29
+ * byTeamName(value: string) {
30
+ * this.$query.whereHas('team', (team) => team.whereILike('name', `%${value}%`))
31
+ * }
32
+ *
33
+ * @filterFor('q', 'query')
34
+ * freeText(value: string) { … }
35
+ * }
36
+ * ```
37
+ *
38
+ * A bound method answers **only** its declared keys: `?filter[byTeamName]=…` stops working, which
39
+ * is the point — the method name becomes an implementation detail.
40
+ */
41
+ export declare function filterFor(...keys: string[]): DualDecorator;
42
+ /**
43
+ * Mark a model column as filterable — the allow-list, declared where the column is, stacked under
44
+ * Lucid's own `@column()`.
45
+ *
46
+ * ```ts
47
+ * export default class User extends compose(BaseModel, Filterable) {
48
+ * @column()
49
+ * @filterable()
50
+ * declare status: string
51
+ *
52
+ * @column()
53
+ * @filterable('number')
54
+ * declare age: number
55
+ *
56
+ * @column({ serializeAs: null })
57
+ * declare passwordHash: string // undecorated — unreachable, as it should be
58
+ * }
59
+ * ```
60
+ *
61
+ * A filter class picks these up through its `static model`. Anything the filter declares itself
62
+ * still wins outright, so a stricter filter over a shared model can narrow the surface:
63
+ * `static filterable = ['status']` exposes that column and no other.
64
+ *
65
+ * The optional kind is the same one the colocated `filterable` map takes: it drives value
66
+ * coercion and the generated client's types.
67
+ */
68
+ export declare function filterable(kind?: FilterFieldKind): DualDecorator;
69
+ /** Mark a model column as sortable — `?sort=-createdAt` reaches it, nothing else does. */
70
+ export declare function sortable(): DualDecorator;
71
+ /** Mark a model column as scanned by the free-text `?search=` term. */
72
+ export declare function searchable(): DualDecorator;
73
+ //# sourceMappingURL=decorators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../../src/decorators.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,UAAU,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACtF,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,wBAAwB,GAAG,IAAI,CAAC;CAC3D;AAED,gEAAgE;AAChE,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAwCD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,SAAS,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAmB1D;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,UAAU,CAAC,IAAI,CAAC,EAAE,eAAe,GAAG,aAAa,CAKhE;AAED,0FAA0F;AAC1F,wBAAgB,QAAQ,IAAI,aAAa,CAKxC;AAED,uEAAuE;AACvE,wBAAgB,UAAU,IAAI,aAAa,CAK1C"}
@@ -0,0 +1,118 @@
1
+ import { legacyRecord, standardRecord } from './decorator_metadata.js';
2
+ import { isReservedKey } from './filter_class.js';
3
+ import { FilterDefinitionError } from './filter_spec.js';
4
+ /** Standard decorators are handed a context object; legacy ones a property key. */
5
+ function isStandardContext(value) {
6
+ return typeof value === 'object' && value !== null && 'kind' in value;
7
+ }
8
+ /**
9
+ * Resolve where a decorator should write, whichever flavour invoked it: the declared member's
10
+ * name, and the record for the class that declared it.
11
+ */
12
+ function target(what, expectedKinds, first, second) {
13
+ if (isStandardContext(second)) {
14
+ if (!expectedKinds.includes(second.kind)) {
15
+ throw new FilterDefinitionError(`${what} goes on ${expectedKinds.join(' or ')}, not on a ${second.kind}.`);
16
+ }
17
+ if (!second.metadata) {
18
+ throw new FilterDefinitionError(`${what} needs decorator metadata, which this runtime does not provide. Polyfill \`Symbol.metadata\` (\`Symbol.metadata ??= Symbol('Symbol.metadata')\`), or compile with \`experimentalDecorators\` as AdonisJS does.`);
19
+ }
20
+ return { name: String(second.name), record: standardRecord(second.metadata) };
21
+ }
22
+ if (typeof first === 'function') {
23
+ throw new FilterDefinitionError(`${what} goes on an instance member, not a static one.`);
24
+ }
25
+ if (typeof first !== 'object' || first === null) {
26
+ throw new FilterDefinitionError(`${what} was applied to something that is not a class member.`);
27
+ }
28
+ return { name: String(second), record: legacyRecord(first) };
29
+ }
30
+ /**
31
+ * Bind a filter method to the request key(s) it answers, instead of deriving the key from the
32
+ * method's name.
33
+ *
34
+ * Worth reaching for exactly twice: when the key cannot be a method name (`team.name`), and when
35
+ * one method should answer several keys. It also pins the public contract — with a binding, the
36
+ * key survives renaming the method.
37
+ *
38
+ * ```ts
39
+ * export default class UserFilter extends BaseModelFilter {
40
+ * @filterFor('team.name')
41
+ * byTeamName(value: string) {
42
+ * this.$query.whereHas('team', (team) => team.whereILike('name', `%${value}%`))
43
+ * }
44
+ *
45
+ * @filterFor('q', 'query')
46
+ * freeText(value: string) { … }
47
+ * }
48
+ * ```
49
+ *
50
+ * A bound method answers **only** its declared keys: `?filter[byTeamName]=…` stops working, which
51
+ * is the point — the method name becomes an implementation detail.
52
+ */
53
+ export function filterFor(...keys) {
54
+ if (keys.length === 0) {
55
+ throw new FilterDefinitionError('@filterFor() needs at least one request key.');
56
+ }
57
+ for (const key of keys) {
58
+ if (typeof key !== 'string' || key.length === 0) {
59
+ throw new FilterDefinitionError('@filterFor() keys must be non-empty strings.');
60
+ }
61
+ if (isReservedKey(key)) {
62
+ throw new FilterDefinitionError(`@filterFor('${key}') — '${key}' is a key the wire format itself owns (page, sort, search, …), so it never reaches a method. Bind another key.`);
63
+ }
64
+ }
65
+ return ((first, second) => {
66
+ const { name, record } = target('@filterFor()', ['method'], first, second);
67
+ record.filterFor = { ...record.filterFor, [name]: [...keys] };
68
+ });
69
+ }
70
+ /** The member kinds a column decorator may sit on. */
71
+ const COLUMN_KINDS = ['field', 'accessor', 'getter', 'setter'];
72
+ /**
73
+ * Mark a model column as filterable — the allow-list, declared where the column is, stacked under
74
+ * Lucid's own `@column()`.
75
+ *
76
+ * ```ts
77
+ * export default class User extends compose(BaseModel, Filterable) {
78
+ * @column()
79
+ * @filterable()
80
+ * declare status: string
81
+ *
82
+ * @column()
83
+ * @filterable('number')
84
+ * declare age: number
85
+ *
86
+ * @column({ serializeAs: null })
87
+ * declare passwordHash: string // undecorated — unreachable, as it should be
88
+ * }
89
+ * ```
90
+ *
91
+ * A filter class picks these up through its `static model`. Anything the filter declares itself
92
+ * still wins outright, so a stricter filter over a shared model can narrow the surface:
93
+ * `static filterable = ['status']` exposes that column and no other.
94
+ *
95
+ * The optional kind is the same one the colocated `filterable` map takes: it drives value
96
+ * coercion and the generated client's types.
97
+ */
98
+ export function filterable(kind) {
99
+ return ((first, second) => {
100
+ const { name, record } = target('@filterable()', COLUMN_KINDS, first, second);
101
+ record.filterable = { ...record.filterable, [name]: kind ?? null };
102
+ });
103
+ }
104
+ /** Mark a model column as sortable — `?sort=-createdAt` reaches it, nothing else does. */
105
+ export function sortable() {
106
+ return ((first, second) => {
107
+ const { name, record } = target('@sortable()', COLUMN_KINDS, first, second);
108
+ record.sortable = [...(record.sortable ?? []), name];
109
+ });
110
+ }
111
+ /** Mark a model column as scanned by the free-text `?search=` term. */
112
+ export function searchable() {
113
+ return ((first, second) => {
114
+ const { name, record } = target('@searchable()', COLUMN_KINDS, first, second);
115
+ record.searchable = [...(record.searchable ?? []), name];
116
+ });
117
+ }
118
+ //# sourceMappingURL=decorators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorators.js","sourceRoot":"","sources":["../../src/decorators.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import { BaseModelFilter } from './base_model_filter.js';
2
+ import { type FilterSpec } from './filter_spec.js';
3
+ /**
4
+ * A filter class as the container hands it back: constructible, extending {@link BaseModelFilter}
5
+ * — and carrying that base's static declarations (`filterable`, `sortable`, `blacklist`, …).
6
+ *
7
+ * Spelled as an intersection with `typeof BaseModelFilter` rather than an index signature: a
8
+ * concrete `class UserFilter extends BaseModelFilter` has no index signature, so
9
+ * `Record<string, unknown>` here would make every real filter class *unassignable* to this type —
10
+ * `static $filter = () => UserFilter` on a model would not compile.
11
+ */
12
+ export type FilterClass = new (...args: any[]) => BaseModelFilter;
13
+ /**
14
+ * Is this a filter class (as opposed to a `defineFilter` spec)? Checked structurally — a class
15
+ * that extends {@link BaseModelFilter} — so the two forms can share one entry point.
16
+ */
17
+ export declare function isFilterClass(value: unknown): value is FilterClass;
18
+ /**
19
+ * The request keys a class owns with a method of its own, walking the prototype chain up to (but
20
+ * not including) {@link BaseModelFilter} — so a shared abstract filter's methods are inherited
21
+ * like any other. `setup`, the constructor, `$`-prefixed members and anything on the class's
22
+ * `blacklist` are excluded.
23
+ *
24
+ * Memoized per class: the shape of a class does not change between requests.
25
+ */
26
+ export declare function dispatchKeys(cls: FilterClass): ReadonlySet<string>;
27
+ /**
28
+ * Compile a filter class's static declarations into the {@link FilterSpec} the runner consumes —
29
+ * the same object `defineFilter` produces, so a class and a spec take the identical code path
30
+ * through allow-listing, search, sort and pagination.
31
+ *
32
+ * A class that declares no `filterable` is method-only: nothing reaches SQL except through the
33
+ * methods it wrote, which is the tightest allow-list there is.
34
+ *
35
+ * Memoized per class — the declaration is static, so it is compiled once per process.
36
+ */
37
+ export declare function specFromFilterClass(cls: FilterClass): FilterSpec;
38
+ /**
39
+ * Find the method a request key maps to, honouring the class's `camelCase` (a `snake_case` key
40
+ * matches a camelCase method — on by default) and `dropId` (`companyId` matches `company`) knobs.
41
+ * Returns the method name, or `undefined` when the class does not own the key.
42
+ */
43
+ export declare function methodForKey(cls: FilterClass, key: string): string | undefined;
44
+ /** Is this key one the wire format owns (so it is never dispatched to a method)? */
45
+ export declare function isReservedKey(key: string): boolean;
46
+ //# sourceMappingURL=filter_class.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter_class.d.ts","sourceRoot":"","sources":["../../src/filter_class.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,kBAAkB,CAAC;AAGjE;;;;;;;;GAQG;AAEH,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,eAAe,CAAC;AAkDlE;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAQlE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,WAAW,GAAG,WAAW,CAAC,MAAM,CAAC,CAmBlE;AAkCD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,WAAW,GAAG,UAAU,CA8BhE;AAoDD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAuB9E;AAcD,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAElD"}
@@ -0,0 +1,238 @@
1
+ import { BaseModelFilter } from './base_model_filter.js';
2
+ import { readDecorators } from './decorator_metadata.js';
3
+ import { defineFilter } from './filter_spec.js';
4
+ /**
5
+ * A filter class's static side. Read through a cast rather than declared on {@link FilterClass}
6
+ * itself: an index signature there would make every concrete `class UserFilter extends
7
+ * BaseModelFilter` *unassignable* to the type — `static $filter = () => UserFilter` on a model
8
+ * would stop compiling.
9
+ */
10
+ function statics(cls) {
11
+ return cls;
12
+ }
13
+ /** Request keys the wire format owns — never dispatched to a method of the same name. */
14
+ const RESERVED_KEYS = new Set([
15
+ 'filter',
16
+ 'filters',
17
+ 'sort',
18
+ 'search',
19
+ 'page',
20
+ 'size',
21
+ 'distinct',
22
+ 'include',
23
+ 'select',
24
+ 'after',
25
+ 'before',
26
+ 'first',
27
+ 'last',
28
+ 'limit',
29
+ 'offset',
30
+ ]);
31
+ /** Names on the class that are machinery, not request keys. */
32
+ const RESERVED_METHODS = new Set(['constructor', 'setup', 'input']);
33
+ /**
34
+ * Is this a filter class (as opposed to a `defineFilter` spec)? Checked structurally — a class
35
+ * that extends {@link BaseModelFilter} — so the two forms can share one entry point.
36
+ */
37
+ export function isFilterClass(value) {
38
+ if (typeof value !== 'function')
39
+ return false;
40
+ let proto = Object.getPrototypeOf(value);
41
+ while (proto !== null && proto !== Function.prototype) {
42
+ if (proto === BaseModelFilter)
43
+ return true;
44
+ proto = Object.getPrototypeOf(proto);
45
+ }
46
+ return false;
47
+ }
48
+ /**
49
+ * The request keys a class owns with a method of its own, walking the prototype chain up to (but
50
+ * not including) {@link BaseModelFilter} — so a shared abstract filter's methods are inherited
51
+ * like any other. `setup`, the constructor, `$`-prefixed members and anything on the class's
52
+ * `blacklist` are excluded.
53
+ *
54
+ * Memoized per class: the shape of a class does not change between requests.
55
+ */
56
+ export function dispatchKeys(cls) {
57
+ const cached = KEY_CACHE.get(cls);
58
+ if (cached)
59
+ return cached;
60
+ const blacklist = new Set(statics(cls).blacklist ?? []);
61
+ const bound = explicitBindings(cls).methods;
62
+ const keys = new Set();
63
+ let proto = cls.prototype;
64
+ while (proto !== null && proto !== BaseModelFilter.prototype && proto !== Object.prototype) {
65
+ for (const name of Object.getOwnPropertyNames(proto)) {
66
+ if (RESERVED_METHODS.has(name) || blacklist.has(name) || bound.has(name))
67
+ continue;
68
+ if (name.startsWith('$') || name.startsWith('_'))
69
+ continue;
70
+ const descriptor = Object.getOwnPropertyDescriptor(proto, name);
71
+ if (typeof descriptor?.value === 'function')
72
+ keys.add(name);
73
+ }
74
+ proto = Object.getPrototypeOf(proto);
75
+ }
76
+ KEY_CACHE.set(cls, keys);
77
+ return keys;
78
+ }
79
+ /**
80
+ * The `@filterFor` bindings a class declares: request key → method, and the set of methods that
81
+ * are bound (and so no longer answer to their own name).
82
+ *
83
+ * Memoized per class alongside the dispatch keys — decorators run at class-definition time, long
84
+ * before the first request reaches the class.
85
+ */
86
+ function explicitBindings(cls) {
87
+ const cached = BINDING_CACHE.get(cls);
88
+ if (cached)
89
+ return cached;
90
+ const keys = new Map();
91
+ const methods = new Set();
92
+ const blacklist = new Set(statics(cls).blacklist ?? []);
93
+ for (const [method, bound] of Object.entries(readDecorators(cls).filterFor ?? {})) {
94
+ if (blacklist.has(method))
95
+ continue;
96
+ methods.add(method);
97
+ for (const key of bound)
98
+ keys.set(key, method);
99
+ }
100
+ const bindings = { keys, methods };
101
+ BINDING_CACHE.set(cls, bindings);
102
+ return bindings;
103
+ }
104
+ const BINDING_CACHE = new WeakMap();
105
+ const KEY_CACHE = new WeakMap();
106
+ const SPEC_CACHE = new WeakMap();
107
+ /**
108
+ * Compile a filter class's static declarations into the {@link FilterSpec} the runner consumes —
109
+ * the same object `defineFilter` produces, so a class and a spec take the identical code path
110
+ * through allow-listing, search, sort and pagination.
111
+ *
112
+ * A class that declares no `filterable` is method-only: nothing reaches SQL except through the
113
+ * methods it wrote, which is the tightest allow-list there is.
114
+ *
115
+ * Memoized per class — the declaration is static, so it is compiled once per process.
116
+ */
117
+ export function specFromFilterClass(cls) {
118
+ const cached = SPEC_CACHE.get(cls);
119
+ if (cached)
120
+ return cached;
121
+ const declared = declaredByModel(cls);
122
+ const spec = defineFilter({
123
+ ...declared,
124
+ filterable: statics(cls).filterable ?? declared.filterable ?? [],
125
+ ...pick(cls, [
126
+ 'model',
127
+ 'sortable',
128
+ 'searchable',
129
+ 'relations',
130
+ 'aliases',
131
+ 'computed',
132
+ 'fieldTypes',
133
+ 'fullText',
134
+ 'vectorSimilarity',
135
+ 'tenant',
136
+ 'defaultFilters',
137
+ 'defaultSort',
138
+ 'defaultSize',
139
+ 'maxSize',
140
+ 'maxDepth',
141
+ 'table',
142
+ 'throwOnInvalid',
143
+ ]),
144
+ });
145
+ SPEC_CACHE.set(cls, spec);
146
+ return spec;
147
+ }
148
+ /**
149
+ * What the model itself declares through `@filterable` / `@sortable` / `@searchable` on its
150
+ * columns, read off the filter's `static model`.
151
+ *
152
+ * These are **fallbacks**: a static on the filter class replaces the corresponding list outright,
153
+ * so a stricter filter over a shared model can narrow what the model opens up. Only `fieldTypes`
154
+ * merges per field — it is metadata about a column, not a decision about who may reach it.
155
+ */
156
+ function declaredByModel(cls) {
157
+ const model = statics(cls).model;
158
+ if (!model)
159
+ return {};
160
+ const declared = readDecorators(model);
161
+ const out = {};
162
+ if (declared.filterable) {
163
+ out.filterable = Object.keys(declared.filterable);
164
+ const fieldTypes = {};
165
+ for (const [column, kind] of Object.entries(declared.filterable)) {
166
+ if (kind !== null)
167
+ fieldTypes[column] = { kind };
168
+ }
169
+ const merged = { ...fieldTypes, ...statics(cls).fieldTypes };
170
+ if (Object.keys(merged).length > 0)
171
+ out.fieldTypes = merged;
172
+ }
173
+ if (declared.sortable)
174
+ out.sortable = [...new Set(declared.sortable)];
175
+ if (declared.searchable)
176
+ out.searchable = [...new Set(declared.searchable)];
177
+ return out;
178
+ }
179
+ function pick(cls, names) {
180
+ const out = {};
181
+ const statics = cls;
182
+ for (const name of names) {
183
+ const value = statics[name];
184
+ if (value !== undefined)
185
+ out[name] = value;
186
+ }
187
+ return out;
188
+ }
189
+ /**
190
+ * Find the method a request key maps to, honouring the class's `camelCase` (a `snake_case` key
191
+ * matches a camelCase method — on by default) and `dropId` (`companyId` matches `company`) knobs.
192
+ * Returns the method name, or `undefined` when the class does not own the key.
193
+ */
194
+ export function methodForKey(cls, key) {
195
+ if (typeof key !== 'string' || key.length === 0 || RESERVED_KEYS.has(key))
196
+ return undefined;
197
+ const bound = explicitBindings(cls).keys.get(key);
198
+ if (bound !== undefined)
199
+ return bound;
200
+ const keys = dispatchKeys(cls);
201
+ if (keys.has(key))
202
+ return key;
203
+ const knobs = statics(cls);
204
+ if (knobs.camelCase !== false) {
205
+ const camel = toCamelCase(key);
206
+ if (keys.has(camel))
207
+ return camel;
208
+ if (knobs.dropId === true) {
209
+ const dropped = stripId(camel);
210
+ if (dropped.length > 0 && keys.has(dropped))
211
+ return dropped;
212
+ }
213
+ }
214
+ if (knobs.dropId === true) {
215
+ const dropped = stripId(key);
216
+ if (dropped.length > 0 && keys.has(dropped))
217
+ return dropped;
218
+ }
219
+ return undefined;
220
+ }
221
+ function toCamelCase(key) {
222
+ const result = key.replace(/[_-](\w)/g, (_, char) => char.toUpperCase());
223
+ return result.charAt(0).toLowerCase() + result.slice(1);
224
+ }
225
+ function stripId(key) {
226
+ if (key === 'id')
227
+ return '';
228
+ if (key.endsWith('Id'))
229
+ return key.slice(0, -2);
230
+ if (key.endsWith('_id'))
231
+ return key.slice(0, -3);
232
+ return key;
233
+ }
234
+ /** Is this key one the wire format owns (so it is never dispatched to a method)? */
235
+ export function isReservedKey(key) {
236
+ return RESERVED_KEYS.has(key);
237
+ }
238
+ //# sourceMappingURL=filter_class.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter_class.js","sourceRoot":"","sources":["../../src/filter_class.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import type { NormalizeConstructor } from '@adonisjs/core/types/helpers';
2
+ import type { LucidModel, ModelPaginatorContract, ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
3
+ import { type ApplyFromRequestOptions, type FilterRequestContext } from './apply_from_request.js';
4
+ import type { FilterClass } from './filter_class.js';
5
+ import type { FilterSpec } from './filter_spec.js';
6
+ import type { ResolvedPagination } from './runner.js';
7
+ /**
8
+ * Opt a Lucid model into filtering — the `adonis-lucid-filter` shape, with the rest of the
9
+ * pipeline (search, sort, page bounds) included.
10
+ *
11
+ * ```ts
12
+ * export default class User extends compose(BaseModel, Filterable) {
13
+ * static $filter = () => UserFilter
14
+ * }
15
+ * ```
16
+ *
17
+ * The model then answers two calls, and the first **hands the query builder back** — nothing is
18
+ * executed until you say so:
19
+ *
20
+ * ```ts
21
+ * // keep composing, then page it with what the request asked for
22
+ * const { query } = await User.filter(ctx)
23
+ * query.whereNotNull('confirmedAt').preload('team')
24
+ * return query.filterPaginate()
25
+ *
26
+ * // or the one-liner
27
+ * return User.filterPaginate(ctx)
28
+ * ```
29
+ *
30
+ * The builder comes back inside an object rather than as the promise's own value on purpose: a
31
+ * Lucid query builder is *thenable*, so `await`ing a promise that resolves to one would run the
32
+ * query and hand you rows instead of the builder. Destructuring keeps the builder a builder.
33
+ */
34
+ /**
35
+ * What {@link Filterable} adds to a model, spelled out so the mixin's return type can be named
36
+ * (an inferred anonymous class would drag Lucid's internals into every consumer's build).
37
+ */
38
+ export interface FilteredQuery<M extends LucidModel> extends ResolvedPagination {
39
+ /** The query builder, filtered/searched/sorted — yours to keep composing on, not yet executed. */
40
+ query: ModelQueryBuilderContract<M>;
41
+ }
42
+ export interface FilterableModelStatics {
43
+ /** The filter this model is read through. A thunk, so the filter may import the model back. */
44
+ $filter?: () => FilterClass | FilterSpec;
45
+ filter<M extends LucidModel>(this: M, ctx?: FilterRequestContext, filter?: FilterClass | FilterSpec, options?: ApplyFromRequestOptions): Promise<FilteredQuery<M>>;
46
+ filterPaginate<M extends LucidModel>(this: M, ctx?: FilterRequestContext, filter?: FilterClass | FilterSpec, options?: ApplyFromRequestOptions): Promise<ModelPaginatorContract<InstanceType<M>>>;
47
+ }
48
+ export declare function Filterable<T extends NormalizeConstructor<LucidModel>>(superclass: T): T & FilterableModelStatics;
49
+ //# sourceMappingURL=filterable_mixin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filterable_mixin.d.ts","sourceRoot":"","sources":["../../src/filterable_mixin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACzE,OAAO,KAAK,EACV,UAAU,EACV,sBAAsB,EACtB,yBAAyB,EAC1B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAE1B,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH;;;GAGG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,UAAU,CAAE,SAAQ,kBAAkB;IAC7E,kGAAkG;IAClG,KAAK,EAAE,yBAAyB,CAAC,CAAC,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,sBAAsB;IACrC,+FAA+F;IAC/F,OAAO,CAAC,EAAE,MAAM,WAAW,GAAG,UAAU,CAAC;IACzC,MAAM,CAAC,CAAC,SAAS,UAAU,EACzB,IAAI,EAAE,CAAC,EACP,GAAG,CAAC,EAAE,oBAAoB,EAC1B,MAAM,CAAC,EAAE,WAAW,GAAG,UAAU,EACjC,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7B,cAAc,CAAC,CAAC,SAAS,UAAU,EACjC,IAAI,EAAE,CAAC,EACP,GAAG,CAAC,EAAE,oBAAoB,EAC1B,MAAM,CAAC,EAAE,WAAW,GAAG,UAAU,EACjC,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,sBAAsB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACrD;AAED,wBAAgB,UAAU,CAAC,CAAC,SAAS,oBAAoB,CAAC,UAAU,CAAC,EACnE,UAAU,EAAE,CAAC,GACZ,CAAC,GAAG,sBAAsB,CA+D5B"}