@dudousxd/nestjs-filter 1.0.1

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 (116) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +108 -0
  4. package/dist/adapter/adapter.d.ts +158 -0
  5. package/dist/adapter/adapter.d.ts.map +1 -0
  6. package/dist/adapter/adapter.js +2 -0
  7. package/dist/adapter/adapter.js.map +1 -0
  8. package/dist/als-store.d.ts +15 -0
  9. package/dist/als-store.d.ts.map +1 -0
  10. package/dist/als-store.js +6 -0
  11. package/dist/als-store.js.map +1 -0
  12. package/dist/base-filter.d.ts +46 -0
  13. package/dist/base-filter.d.ts.map +1 -0
  14. package/dist/base-filter.js +95 -0
  15. package/dist/base-filter.js.map +1 -0
  16. package/dist/cli/generate.d.ts +3 -0
  17. package/dist/cli/generate.d.ts.map +1 -0
  18. package/dist/cli/generate.js +43 -0
  19. package/dist/cli/generate.js.map +1 -0
  20. package/dist/decorator/apply-filter.decorator.d.ts +11 -0
  21. package/dist/decorator/apply-filter.decorator.d.ts.map +1 -0
  22. package/dist/decorator/apply-filter.decorator.js +22 -0
  23. package/dist/decorator/apply-filter.decorator.js.map +1 -0
  24. package/dist/decorator/filter-for.decorator.d.ts +4 -0
  25. package/dist/decorator/filter-for.decorator.d.ts.map +1 -0
  26. package/dist/decorator/filter-for.decorator.js +29 -0
  27. package/dist/decorator/filter-for.decorator.js.map +1 -0
  28. package/dist/decorator/filterable.decorator.d.ts +5 -0
  29. package/dist/decorator/filterable.decorator.d.ts.map +1 -0
  30. package/dist/decorator/filterable.decorator.js +24 -0
  31. package/dist/decorator/filterable.decorator.js.map +1 -0
  32. package/dist/decorator/relations.decorator.d.ts +29 -0
  33. package/dist/decorator/relations.decorator.d.ts.map +1 -0
  34. package/dist/decorator/relations.decorator.js +40 -0
  35. package/dist/decorator/relations.decorator.js.map +1 -0
  36. package/dist/errors/exceptions.d.ts +32 -0
  37. package/dist/errors/exceptions.d.ts.map +1 -0
  38. package/dist/errors/exceptions.js +56 -0
  39. package/dist/errors/exceptions.js.map +1 -0
  40. package/dist/filter/filter-exception.filter.d.ts +6 -0
  41. package/dist/filter/filter-exception.filter.d.ts.map +1 -0
  42. package/dist/filter/filter-exception.filter.js +32 -0
  43. package/dist/filter/filter-exception.filter.js.map +1 -0
  44. package/dist/index.d.ts +23 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +19 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/input/dispatcher.d.ts +2 -0
  49. package/dist/input/dispatcher.d.ts.map +1 -0
  50. package/dist/input/dispatcher.js +25 -0
  51. package/dist/input/dispatcher.js.map +1 -0
  52. package/dist/input/normalizer.d.ts +9 -0
  53. package/dist/input/normalizer.d.ts.map +1 -0
  54. package/dist/input/normalizer.js +51 -0
  55. package/dist/input/normalizer.js.map +1 -0
  56. package/dist/input/source-resolver.d.ts +3 -0
  57. package/dist/input/source-resolver.d.ts.map +1 -0
  58. package/dist/input/source-resolver.js +49 -0
  59. package/dist/input/source-resolver.js.map +1 -0
  60. package/dist/input/validator.d.ts +3 -0
  61. package/dist/input/validator.d.ts.map +1 -0
  62. package/dist/input/validator.js +33 -0
  63. package/dist/input/validator.js.map +1 -0
  64. package/dist/interceptor/apply-filter.interceptor.d.ts +10 -0
  65. package/dist/interceptor/apply-filter.interceptor.d.ts.map +1 -0
  66. package/dist/interceptor/apply-filter.interceptor.js +81 -0
  67. package/dist/interceptor/apply-filter.interceptor.js.map +1 -0
  68. package/dist/module.d.ts +15 -0
  69. package/dist/module.d.ts.map +1 -0
  70. package/dist/module.js +111 -0
  71. package/dist/module.js.map +1 -0
  72. package/dist/operators/column-filter.dto.d.ts +22 -0
  73. package/dist/operators/column-filter.dto.d.ts.map +1 -0
  74. package/dist/operators/column-filter.dto.js +61 -0
  75. package/dist/operators/column-filter.dto.js.map +1 -0
  76. package/dist/operators/index.d.ts +5 -0
  77. package/dist/operators/index.d.ts.map +1 -0
  78. package/dist/operators/index.js +4 -0
  79. package/dist/operators/index.js.map +1 -0
  80. package/dist/operators/types.d.ts +21 -0
  81. package/dist/operators/types.d.ts.map +1 -0
  82. package/dist/operators/types.js +28 -0
  83. package/dist/operators/types.js.map +1 -0
  84. package/dist/operators/validate-column-filter.d.ts +24 -0
  85. package/dist/operators/validate-column-filter.d.ts.map +1 -0
  86. package/dist/operators/validate-column-filter.js +118 -0
  87. package/dist/operators/validate-column-filter.js.map +1 -0
  88. package/dist/runner.d.ts +105 -0
  89. package/dist/runner.d.ts.map +1 -0
  90. package/dist/runner.js +554 -0
  91. package/dist/runner.js.map +1 -0
  92. package/dist/testing/filter-testing.module.d.ts +7 -0
  93. package/dist/testing/filter-testing.module.d.ts.map +1 -0
  94. package/dist/testing/filter-testing.module.js +41 -0
  95. package/dist/testing/filter-testing.module.js.map +1 -0
  96. package/dist/testing/index.d.ts +3 -0
  97. package/dist/testing/index.d.ts.map +1 -0
  98. package/dist/testing/index.js +3 -0
  99. package/dist/testing/index.js.map +1 -0
  100. package/dist/testing/mock-query-builder.d.ts +6 -0
  101. package/dist/testing/mock-query-builder.d.ts.map +1 -0
  102. package/dist/testing/mock-query-builder.js +22 -0
  103. package/dist/testing/mock-query-builder.js.map +1 -0
  104. package/dist/tokens.d.ts +8 -0
  105. package/dist/tokens.d.ts.map +1 -0
  106. package/dist/tokens.js +8 -0
  107. package/dist/tokens.js.map +1 -0
  108. package/dist/types.d.ts +110 -0
  109. package/dist/types.d.ts.map +1 -0
  110. package/dist/types.js +2 -0
  111. package/dist/types.js.map +1 -0
  112. package/dist/utils/escape-like.d.ts +10 -0
  113. package/dist/utils/escape-like.d.ts.map +1 -0
  114. package/dist/utils/escape-like.js +15 -0
  115. package/dist/utils/escape-like.js.map +1 -0
  116. package/package.json +82 -0
@@ -0,0 +1,105 @@
1
+ import { type Type } from '@nestjs/common';
2
+ import { ModuleRef } from '@nestjs/core';
3
+ import type { FilterAdapter } from './adapter/adapter.js';
4
+ import type { FilterContext, FilterModuleOptions } from './types.js';
5
+ export declare class FilterRunner {
6
+ private readonly moduleRef;
7
+ private readonly options;
8
+ private readonly logger;
9
+ private adapter;
10
+ constructor(moduleRef: ModuleRef, options: FilterModuleOptions, injectedAdapter: FilterAdapter | null);
11
+ /**
12
+ * Lazily resolves the adapter from the DI container if the injected
13
+ * value is null. This handles the case where the adapter module
14
+ * (e.g. MikroOrmFilterModule) is imported after FilterModule.forRoot()
15
+ * and the FilterRunner's local injection gets null.
16
+ */
17
+ private resolveAdapter;
18
+ apply<F extends object, Q>(FilterClass: Type<F>, input: unknown, qb: Q, context?: FilterContext): Promise<Q>;
19
+ private resolveFilter;
20
+ private runSetup;
21
+ /**
22
+ * Resolves a method for a whitelisted key, bypassing static allowed/blocked checks.
23
+ * Only checks the @FilterFor map directly.
24
+ */
25
+ private resolveWhitelistedMethod;
26
+ /**
27
+ * Applies relation-bound input keys by delegating to the related filter
28
+ * via the adapter's applyRelationConstraint.
29
+ */
30
+ private applyRelation;
31
+ /**
32
+ * Extracts the structured input shape from raw input.
33
+ *
34
+ * Supports:
35
+ * - `{ filter: {...}, include: [...], search: '...' }` (new structured format)
36
+ * - Any other shape is treated as the filter portion directly (backward compat for internal calls)
37
+ */
38
+ private extractStructuredInput;
39
+ /**
40
+ * Extracts `where` (ColumnFilter[]) from input and returns the remaining
41
+ * input keys for @FilterFor dispatch.
42
+ *
43
+ * Detects three input modes:
44
+ * 1. Plain Record<string, unknown> → no column filters, input passes through
45
+ * 2. Object with `where: ColumnFilter[]` → column filters extracted, remaining keys pass through
46
+ * 3. null/undefined/non-object → no column filters, empty remaining input
47
+ */
48
+ private extractColumnFilters;
49
+ /**
50
+ * Resolves the set of auto-field names from @Filterable metadata.
51
+ *
52
+ * Returns:
53
+ * - null if autoFields is `false` (opt-out)
54
+ * - Set of all possible keys when autoFields is `true` (represented as a "match-all" set)
55
+ * - Set of explicit field names when autoFields is a string array
56
+ *
57
+ * When autoFields is `true` (the default), the set contains all possible
58
+ * keys from the `allowed` list if present; otherwise it introspects entity
59
+ * metadata via the adapter's `getEntityFields()` to restrict to real columns.
60
+ *
61
+ * When metadata introspection is unavailable (adapter doesn't implement
62
+ * `getEntityFields` or returns null), falls back to accept-all with a
63
+ * logged warning.
64
+ */
65
+ private resolveAutoFields;
66
+ /**
67
+ * Parses raw include input into an array of string paths.
68
+ *
69
+ * Supports:
70
+ * - comma-separated string: `'role,posts'` → `['role', 'posts']`
71
+ * - string array: `['role', 'posts']` → `['role', 'posts']`
72
+ * - falsy values: `undefined`, `null`, `''` → `[]`
73
+ */
74
+ parseIncludes(raw: unknown): string[];
75
+ /**
76
+ * Validates include paths against the allowlist (if defined) or entity relations.
77
+ * Silently skips invalid paths.
78
+ */
79
+ private validateIncludes;
80
+ /**
81
+ * Applies global search across string columns or a tsvector column.
82
+ */
83
+ private applyGlobalSearch;
84
+ /**
85
+ * Applies filters dynamically against an entity without requiring a filter class.
86
+ *
87
+ * Uses entity metadata (via adapter) for auto-fields, operators from structured
88
+ * input, includes, and search. No @FilterFor methods, no setup() hook, no
89
+ * whitelist/blacklist — intended for admin endpoints that query any table.
90
+ *
91
+ * @param entity - The entity class to query against.
92
+ * @param input - Raw input (structured or flat).
93
+ * @param qb - The query builder instance.
94
+ * @param context - Optional filter context.
95
+ * @returns The query builder with filters applied.
96
+ */
97
+ applyDynamic<Q>(entity: Type<unknown>, input: unknown, qb: Q, context?: FilterContext): Promise<Q>;
98
+ /**
99
+ * Applies global search for dynamic mode: auto-detects all string columns
100
+ * from entity metadata (no filter class with static search config).
101
+ */
102
+ private applyGlobalSearchDynamic;
103
+ private handleUnknownKey;
104
+ }
105
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACvE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAgB1D,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAWrE,qBACa,YAAY;IAMrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IACK,OAAO,CAAC,QAAQ,CAAC,OAAO;IANzD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;IAExD,OAAO,CAAC,OAAO,CAAuB;gBAGnB,SAAS,EAAE,SAAS,EACW,OAAO,EAAE,mBAAmB,EACpD,eAAe,EAAE,aAAa,GAAG,IAAI;IAK/D;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IAahB,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAC7B,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,EACpB,KAAK,EAAE,OAAO,EACd,EAAE,EAAE,CAAC,EACL,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,CAAC,CAAC;YA8LC,aAAa;YAmBb,QAAQ;IAUtB;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAKhC;;;OAGG;YACW,aAAa;IAsB3B;;;;;;OAMG;IACH,OAAO,CAAC,sBAAsB;IAqB9B;;;;;;;;OAQG;IACH,OAAO,CAAC,oBAAoB;IAwB5B;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,iBAAiB;IA2CzB;;;;;;;OAOG;IACH,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,EAAE;IAWrC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAyBxB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA0CzB;;;;;;;;;;;;OAYG;IACG,YAAY,CAAC,CAAC,EAClB,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,EACrB,KAAK,EAAE,OAAO,EACd,EAAE,EAAE,CAAC,EACL,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,CAAC,CAAC;IA4Eb;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAsBhC,OAAO,CAAC,gBAAgB;CAOzB"}
package/dist/runner.js ADDED
@@ -0,0 +1,554 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ var FilterRunner_1;
14
+ import { Inject, Injectable, Logger } from '@nestjs/common';
15
+ import { ModuleRef } from '@nestjs/core';
16
+ import { runWithFilterState } from './als-store.js';
17
+ import { getFilterForMap } from './decorator/filter-for.decorator.js';
18
+ import { getFilterableMetadata } from './decorator/filterable.decorator.js';
19
+ import { resolveRelation } from './decorator/relations.decorator.js';
20
+ import { FilterMethodException, FilterNotRegisteredException, UnknownFilterKeyException, } from './errors/exceptions.js';
21
+ import { resolveDispatchTarget } from './input/dispatcher.js';
22
+ import { normalizeInput } from './input/normalizer.js';
23
+ import { validateInput } from './input/validator.js';
24
+ import { validateColumnFilters } from './operators/validate-column-filter.js';
25
+ import { FILTER_ADAPTER, FILTER_MODULE_OPTIONS } from './tokens.js';
26
+ const MATCH_ALL_SET = { has: () => true };
27
+ let FilterRunner = FilterRunner_1 = class FilterRunner {
28
+ moduleRef;
29
+ options;
30
+ logger = new Logger(FilterRunner_1.name);
31
+ adapter;
32
+ constructor(moduleRef, options, injectedAdapter) {
33
+ this.moduleRef = moduleRef;
34
+ this.options = options;
35
+ this.adapter = injectedAdapter;
36
+ }
37
+ /**
38
+ * Lazily resolves the adapter from the DI container if the injected
39
+ * value is null. This handles the case where the adapter module
40
+ * (e.g. MikroOrmFilterModule) is imported after FilterModule.forRoot()
41
+ * and the FilterRunner's local injection gets null.
42
+ */
43
+ resolveAdapter() {
44
+ if (this.adapter)
45
+ return this.adapter;
46
+ try {
47
+ const resolved = this.moduleRef.get(FILTER_ADAPTER, { strict: false });
48
+ if (resolved) {
49
+ this.adapter = resolved;
50
+ }
51
+ return this.adapter;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ async apply(FilterClass, input, qb, context = {}) {
58
+ const filter = await this.resolveFilter(FilterClass);
59
+ const adapter = this.resolveAdapter();
60
+ // Extract structured input: { filter, include, search }
61
+ const rawInput = this.extractStructuredInput(input);
62
+ const filterInput = rawInput.filter;
63
+ const rawInclude = rawInput.include;
64
+ const rawSearch = rawInput.search;
65
+ // Extract column filters from the filter portion before normalization
66
+ const { columnFilters, remainingInput } = this.extractColumnFilters(filterInput);
67
+ const normalized = normalizeInput(remainingInput, {
68
+ normalizer: this.options.inputNormalizer ?? 'camelCase',
69
+ dropId: this.options.dropId ?? true,
70
+ ...(this.options.stripEmpty !== undefined && { stripEmpty: this.options.stripEmpty }),
71
+ });
72
+ const finalInput = this.options.validation === 'off' ? normalized : await validateInput(FilterClass, normalized);
73
+ const $whitelisted = new Set();
74
+ const $blacklisted = new Set();
75
+ const $pushed = [];
76
+ return runWithFilterState({
77
+ $query: qb,
78
+ $input: Object.freeze({ ...finalInput }),
79
+ $context: context,
80
+ $adapter: adapter,
81
+ $whitelisted,
82
+ $blacklisted,
83
+ $pushed,
84
+ }, async () => {
85
+ await this.runSetup(filter);
86
+ // Apply column filters via adapter before @FilterFor dispatch
87
+ if (columnFilters.length > 0 && adapter?.applyColumnFilters) {
88
+ validateColumnFilters(columnFilters);
89
+ adapter.applyColumnFilters(qb, columnFilters);
90
+ }
91
+ else if (columnFilters.length > 0 && !adapter?.applyColumnFilters) {
92
+ this.logger.warn('Column filters (where) provided but adapter does not support applyColumnFilters. Skipping.');
93
+ }
94
+ // Resolve auto-fields configuration
95
+ const autoFieldSet = this.resolveAutoFields(FilterClass);
96
+ const filterableMeta = getFilterableMetadata(FilterClass);
97
+ // Collect relation-bound keys for batched processing
98
+ const relationBatches = new Map();
99
+ for (const [key, value] of Object.entries(finalInput)) {
100
+ if (value === undefined)
101
+ continue;
102
+ if ($blacklisted.has(key))
103
+ continue;
104
+ const methodName = $whitelisted.has(key)
105
+ ? this.resolveWhitelistedMethod(FilterClass, key)
106
+ : resolveDispatchTarget(FilterClass, key);
107
+ if (methodName) {
108
+ try {
109
+ const method = filter[methodName];
110
+ await method.call(filter, value, key);
111
+ }
112
+ catch (cause) {
113
+ throw new FilterMethodException(key, value, cause);
114
+ }
115
+ continue;
116
+ }
117
+ // Check if this key is mapped to a relation
118
+ const relation = resolveRelation(FilterClass, key);
119
+ if (relation) {
120
+ const [relationName, config] = relation;
121
+ if (!relationBatches.has(relationName)) {
122
+ relationBatches.set(relationName, { config, entries: [] });
123
+ }
124
+ relationBatches.get(relationName).entries.push([key, value]);
125
+ continue;
126
+ }
127
+ // Check if this key is an auto-field
128
+ if (autoFieldSet?.has(key)) {
129
+ if (adapter?.applyAutoField) {
130
+ adapter.applyAutoField(qb, key, value);
131
+ }
132
+ else {
133
+ this.logger.warn(`Auto-field "${key}" provided but adapter does not support applyAutoField. Skipping.`);
134
+ }
135
+ continue;
136
+ }
137
+ // Check if this is a dot-notation relation field (e.g. 'posts.title')
138
+ if (key.includes('.') &&
139
+ autoFieldSet &&
140
+ filterableMeta &&
141
+ adapter?.getEntityRelations &&
142
+ adapter?.applyAutoRelationField) {
143
+ const dotIndex = key.indexOf('.');
144
+ const relName = key.substring(0, dotIndex);
145
+ const fieldName = key.substring(dotIndex + 1);
146
+ if (fieldName.length > 0) {
147
+ const relations = adapter.getEntityRelations(filterableMeta.entity);
148
+ if (relations) {
149
+ const isRelation = relations.some((r) => r.name === relName);
150
+ if (isRelation) {
151
+ adapter.applyAutoRelationField(qb, relName, fieldName, value);
152
+ continue;
153
+ }
154
+ }
155
+ }
156
+ }
157
+ this.handleUnknownKey(key);
158
+ }
159
+ // Apply relation constraints in batch per relation
160
+ for (const [relationName, { config, entries }] of relationBatches) {
161
+ await this.applyRelation(config.filter, qb, relationName, entries, context);
162
+ }
163
+ // Process pushed entries (BFS: pushed handlers may push more entries)
164
+ const MAX_PUSH_ITERATIONS = 100;
165
+ let pushIterations = 0;
166
+ while ($pushed.length > 0) {
167
+ if (++pushIterations > MAX_PUSH_ITERATIONS) {
168
+ throw new FilterMethodException('$push', undefined, new Error(`Push loop exceeded ${MAX_PUSH_ITERATIONS} iterations — possible infinite cycle.`));
169
+ }
170
+ const [key, value] = $pushed.shift();
171
+ if (value === undefined)
172
+ continue;
173
+ if ($blacklisted.has(key))
174
+ continue;
175
+ const methodName = $whitelisted.has(key)
176
+ ? this.resolveWhitelistedMethod(FilterClass, key)
177
+ : resolveDispatchTarget(FilterClass, key);
178
+ if (!methodName) {
179
+ this.handleUnknownKey(key);
180
+ continue;
181
+ }
182
+ try {
183
+ const method = filter[methodName];
184
+ await method.call(filter, value, key);
185
+ }
186
+ catch (cause) {
187
+ throw new FilterMethodException(key, value, cause);
188
+ }
189
+ }
190
+ // Apply includes (eager loading)
191
+ const includes = this.parseIncludes(rawInclude);
192
+ if (includes.length > 0 && adapter?.applyIncludes && filterableMeta) {
193
+ const allowedIncludes = FilterClass
194
+ .includes;
195
+ const validIncludes = this.validateIncludes(includes, allowedIncludes, adapter, filterableMeta.entity);
196
+ if (validIncludes.length > 0) {
197
+ adapter.applyIncludes(qb, validIncludes, filterableMeta.entity);
198
+ }
199
+ }
200
+ // Apply global search
201
+ if (rawSearch && typeof rawSearch === 'string' && rawSearch.trim()) {
202
+ this.applyGlobalSearch(qb, rawSearch.trim(), FilterClass, adapter, filterableMeta);
203
+ }
204
+ return qb;
205
+ });
206
+ }
207
+ async resolveFilter(FilterClass) {
208
+ try {
209
+ return await this.moduleRef.resolve(FilterClass, undefined, { strict: false });
210
+ }
211
+ catch (resolveErr) {
212
+ try {
213
+ return this.moduleRef.get(FilterClass, { strict: false });
214
+ }
215
+ catch {
216
+ if (resolveErr instanceof Error &&
217
+ !resolveErr.message.includes('could not find') &&
218
+ !resolveErr.message.includes('Could not find')) {
219
+ throw resolveErr;
220
+ }
221
+ throw new FilterNotRegisteredException(FilterClass.name);
222
+ }
223
+ }
224
+ }
225
+ async runSetup(filter) {
226
+ const maybe = filter;
227
+ if (typeof maybe.setup !== 'function')
228
+ return;
229
+ try {
230
+ await maybe.setup();
231
+ }
232
+ catch (cause) {
233
+ throw new FilterMethodException('setup', undefined, cause);
234
+ }
235
+ }
236
+ /**
237
+ * Resolves a method for a whitelisted key, bypassing static allowed/blocked checks.
238
+ * Only checks the @FilterFor map directly.
239
+ */
240
+ resolveWhitelistedMethod(FilterClass, key) {
241
+ const map = getFilterForMap(FilterClass);
242
+ return map.get(key) ?? null;
243
+ }
244
+ /**
245
+ * Applies relation-bound input keys by delegating to the related filter
246
+ * via the adapter's applyRelationConstraint.
247
+ */
248
+ async applyRelation(RelatedFilterClass, qb, relationName, entries, context) {
249
+ if (!this.adapter?.applyRelationConstraint) {
250
+ this.logger.warn(`Relation "${relationName}" skipped: adapter does not support applyRelationConstraint.`);
251
+ return;
252
+ }
253
+ const inputObj = {};
254
+ for (const [key, value] of entries) {
255
+ inputObj[key] = value;
256
+ }
257
+ await this.adapter.applyRelationConstraint(qb, relationName, async (relationQb) => {
258
+ await this.apply(RelatedFilterClass, { filter: inputObj }, relationQb, context);
259
+ });
260
+ }
261
+ /**
262
+ * Extracts the structured input shape from raw input.
263
+ *
264
+ * Supports:
265
+ * - `{ filter: {...}, include: [...], search: '...' }` (new structured format)
266
+ * - Any other shape is treated as the filter portion directly (backward compat for internal calls)
267
+ */
268
+ extractStructuredInput(input) {
269
+ if (input == null || typeof input !== 'object') {
270
+ return { filter: input, include: undefined, search: undefined };
271
+ }
272
+ const inputObj = input;
273
+ // Detect structured format: must have a 'filter' key (even if undefined/null)
274
+ if ('filter' in inputObj) {
275
+ return {
276
+ filter: inputObj.filter ?? undefined,
277
+ include: inputObj.include ?? undefined,
278
+ search: inputObj.search ?? undefined,
279
+ };
280
+ }
281
+ // Not structured — treat entire input as the filter portion
282
+ return { filter: input, include: undefined, search: undefined };
283
+ }
284
+ /**
285
+ * Extracts `where` (ColumnFilter[]) from input and returns the remaining
286
+ * input keys for @FilterFor dispatch.
287
+ *
288
+ * Detects three input modes:
289
+ * 1. Plain Record<string, unknown> → no column filters, input passes through
290
+ * 2. Object with `where: ColumnFilter[]` → column filters extracted, remaining keys pass through
291
+ * 3. null/undefined/non-object → no column filters, empty remaining input
292
+ */
293
+ extractColumnFilters(input) {
294
+ if (input == null || typeof input !== 'object') {
295
+ return { columnFilters: [], remainingInput: input };
296
+ }
297
+ const inputObj = input;
298
+ if (!('where' in inputObj) || !Array.isArray(inputObj.where)) {
299
+ return { columnFilters: [], remainingInput: input };
300
+ }
301
+ const columnFilters = inputObj.where;
302
+ // Build remaining input without the 'where' key
303
+ const remaining = {};
304
+ for (const [key, value] of Object.entries(inputObj)) {
305
+ if (key !== 'where') {
306
+ remaining[key] = value;
307
+ }
308
+ }
309
+ return { columnFilters, remainingInput: remaining };
310
+ }
311
+ /**
312
+ * Resolves the set of auto-field names from @Filterable metadata.
313
+ *
314
+ * Returns:
315
+ * - null if autoFields is `false` (opt-out)
316
+ * - Set of all possible keys when autoFields is `true` (represented as a "match-all" set)
317
+ * - Set of explicit field names when autoFields is a string array
318
+ *
319
+ * When autoFields is `true` (the default), the set contains all possible
320
+ * keys from the `allowed` list if present; otherwise it introspects entity
321
+ * metadata via the adapter's `getEntityFields()` to restrict to real columns.
322
+ *
323
+ * When metadata introspection is unavailable (adapter doesn't implement
324
+ * `getEntityFields` or returns null), falls back to accept-all with a
325
+ * logged warning.
326
+ */
327
+ resolveAutoFields(FilterClass) {
328
+ const meta = getFilterableMetadata(FilterClass);
329
+ if (!meta)
330
+ return null;
331
+ const autoFieldsConfig = meta.autoFields ?? true;
332
+ if (autoFieldsConfig === false)
333
+ return null;
334
+ if (autoFieldsConfig === true) {
335
+ // When autoFields is true with an allowed list, only allowed keys are auto-applicable
336
+ if (meta.allowed) {
337
+ // Remove keys that already have @FilterFor mappings
338
+ const filterForMap = getFilterForMap(FilterClass);
339
+ const set = new Set();
340
+ for (const key of meta.allowed) {
341
+ if (!filterForMap.has(key))
342
+ set.add(key);
343
+ }
344
+ return set;
345
+ }
346
+ // Introspect entity metadata to restrict auto-fields to real columns
347
+ const adapter = this.resolveAdapter();
348
+ if (adapter?.getEntityFields) {
349
+ const entityFields = adapter.getEntityFields(meta.entity);
350
+ if (entityFields) {
351
+ const filterForMap = getFilterForMap(FilterClass);
352
+ const set = new Set();
353
+ for (const field of entityFields) {
354
+ if (!filterForMap.has(field.name))
355
+ set.add(field.name);
356
+ }
357
+ return set;
358
+ }
359
+ }
360
+ // Fallback: adapter doesn't support metadata introspection — accept all with warning
361
+ this.logger.warn(`autoFields: true on ${FilterClass.name} cannot validate fields against entity metadata. The adapter does not implement getEntityFields() or returned null. All input keys will be accepted (legacy behavior). Consider upgrading your adapter or using an explicit autoFields list.`);
362
+ return MATCH_ALL_SET;
363
+ }
364
+ // Explicit list of auto-field names
365
+ return new Set(autoFieldsConfig);
366
+ }
367
+ /**
368
+ * Parses raw include input into an array of string paths.
369
+ *
370
+ * Supports:
371
+ * - comma-separated string: `'role,posts'` → `['role', 'posts']`
372
+ * - string array: `['role', 'posts']` → `['role', 'posts']`
373
+ * - falsy values: `undefined`, `null`, `''` → `[]`
374
+ */
375
+ parseIncludes(raw) {
376
+ if (!raw)
377
+ return [];
378
+ if (typeof raw === 'string')
379
+ return raw
380
+ .split(',')
381
+ .map((s) => s.trim())
382
+ .filter(Boolean);
383
+ if (Array.isArray(raw))
384
+ return raw.filter((s) => typeof s === 'string');
385
+ return [];
386
+ }
387
+ /**
388
+ * Validates include paths against the allowlist (if defined) or entity relations.
389
+ * Silently skips invalid paths.
390
+ */
391
+ validateIncludes(includes, allowlist, adapter, entity) {
392
+ const maxDepth = this.options.maxIncludeDepth ?? 3;
393
+ return includes.filter((path) => {
394
+ const segments = path.split('.');
395
+ if (segments.length > maxDepth)
396
+ return false;
397
+ if (segments.some((s) => !s))
398
+ return false;
399
+ if (allowlist) {
400
+ return allowlist.includes(path);
401
+ }
402
+ // Validate first segment against entity relations
403
+ if (adapter.getEntityRelations) {
404
+ const relations = adapter.getEntityRelations(entity);
405
+ if (relations) {
406
+ return relations.some((r) => r.name === segments[0]);
407
+ }
408
+ }
409
+ return true;
410
+ });
411
+ }
412
+ /**
413
+ * Applies global search across string columns or a tsvector column.
414
+ */
415
+ applyGlobalSearch(qb, searchTerm, FilterClass, adapter, filterableMeta) {
416
+ if (!adapter || !filterableMeta)
417
+ return;
418
+ const searchConfig = FilterClass.search;
419
+ if (searchConfig && typeof searchConfig === 'object' && 'vector' in searchConfig) {
420
+ // tsvector search
421
+ if (adapter.applyVectorSearch) {
422
+ adapter.applyVectorSearch(qb, searchTerm, searchConfig.vector);
423
+ }
424
+ return;
425
+ }
426
+ // ILIKE search
427
+ let columns;
428
+ if (Array.isArray(searchConfig)) {
429
+ columns = searchConfig;
430
+ }
431
+ else {
432
+ // Auto-detect: get all string columns from entity metadata
433
+ const fields = adapter.getEntityFields?.(filterableMeta.entity);
434
+ columns = fields?.filter((f) => f.type === 'string').map((f) => f.name) ?? [];
435
+ }
436
+ if (columns.length > 10) {
437
+ this.logger.warn(`Global search on ${columns.length} columns may be slow. Consider declaring static search = [...] on ${FilterClass.name}`);
438
+ }
439
+ if (columns.length > 0 && adapter.applySearch) {
440
+ adapter.applySearch(qb, searchTerm, columns, filterableMeta.entity);
441
+ }
442
+ }
443
+ /**
444
+ * Applies filters dynamically against an entity without requiring a filter class.
445
+ *
446
+ * Uses entity metadata (via adapter) for auto-fields, operators from structured
447
+ * input, includes, and search. No @FilterFor methods, no setup() hook, no
448
+ * whitelist/blacklist — intended for admin endpoints that query any table.
449
+ *
450
+ * @param entity - The entity class to query against.
451
+ * @param input - Raw input (structured or flat).
452
+ * @param qb - The query builder instance.
453
+ * @param context - Optional filter context.
454
+ * @returns The query builder with filters applied.
455
+ */
456
+ async applyDynamic(entity, input, qb, context = {}) {
457
+ const adapter = this.resolveAdapter();
458
+ // Extract structured input: { filter, include, search }
459
+ const rawInput = this.extractStructuredInput(input);
460
+ const filterInput = rawInput.filter;
461
+ const rawInclude = rawInput.include;
462
+ const rawSearch = rawInput.search;
463
+ // Extract column filters before normalization
464
+ const { columnFilters, remainingInput } = this.extractColumnFilters(filterInput);
465
+ const normalized = normalizeInput(remainingInput, {
466
+ normalizer: this.options.inputNormalizer ?? 'camelCase',
467
+ dropId: this.options.dropId ?? true,
468
+ ...(this.options.stripEmpty !== undefined && { stripEmpty: this.options.stripEmpty }),
469
+ });
470
+ // Apply column filters via adapter
471
+ if (columnFilters.length > 0 && adapter?.applyColumnFilters) {
472
+ validateColumnFilters(columnFilters);
473
+ adapter.applyColumnFilters(qb, columnFilters);
474
+ }
475
+ else if (columnFilters.length > 0 && !adapter?.applyColumnFilters) {
476
+ this.logger.warn('Column filters (where) provided but adapter does not support applyColumnFilters. Skipping.');
477
+ }
478
+ // Auto-fields: all entity columns (no filter class = no @FilterFor to check)
479
+ if (adapter?.getEntityFields && adapter?.applyAutoField) {
480
+ const fields = adapter.getEntityFields(entity);
481
+ if (fields) {
482
+ const fieldNames = new Set(fields.map((f) => f.name));
483
+ for (const [key, value] of Object.entries(normalized)) {
484
+ if (value === undefined)
485
+ continue;
486
+ if (key.includes('.')) {
487
+ // Dot-notation relation
488
+ const dotIndex = key.indexOf('.');
489
+ const relName = key.substring(0, dotIndex);
490
+ const fieldName = key.substring(dotIndex + 1);
491
+ if (fieldName.length > 0 &&
492
+ adapter.getEntityRelations &&
493
+ adapter.applyAutoRelationField) {
494
+ const rels = adapter.getEntityRelations(entity);
495
+ if (rels?.some((r) => r.name === relName)) {
496
+ adapter.applyAutoRelationField(qb, relName, fieldName, value);
497
+ }
498
+ // Unknown relation: silently skipped
499
+ }
500
+ }
501
+ else if (fieldNames.has(key)) {
502
+ adapter.applyAutoField(qb, key, value);
503
+ }
504
+ // Unknown keys silently skipped
505
+ }
506
+ }
507
+ }
508
+ // Includes — no allowlist (no filter class), validate against entity metadata only
509
+ const includes = this.parseIncludes(rawInclude);
510
+ if (includes.length > 0 && adapter?.applyIncludes) {
511
+ const validIncludes = this.validateIncludes(includes, undefined, adapter, entity);
512
+ if (validIncludes.length > 0) {
513
+ adapter.applyIncludes(qb, validIncludes, entity);
514
+ }
515
+ }
516
+ // Search — auto-detect all string columns from entity metadata
517
+ if (rawSearch && typeof rawSearch === 'string' && rawSearch.trim()) {
518
+ this.applyGlobalSearchDynamic(qb, rawSearch.trim(), entity, adapter);
519
+ }
520
+ return qb;
521
+ }
522
+ /**
523
+ * Applies global search for dynamic mode: auto-detects all string columns
524
+ * from entity metadata (no filter class with static search config).
525
+ */
526
+ applyGlobalSearchDynamic(qb, searchTerm, entity, adapter) {
527
+ if (!adapter)
528
+ return;
529
+ const fields = adapter.getEntityFields?.(entity);
530
+ const columns = fields?.filter((f) => f.type === 'string').map((f) => f.name) ?? [];
531
+ if (columns.length > 10) {
532
+ this.logger.warn(`Global search on ${columns.length} columns may be slow. Consider using a filter class with static search = [...]`);
533
+ }
534
+ if (columns.length > 0 && adapter.applySearch) {
535
+ adapter.applySearch(qb, searchTerm, columns, entity);
536
+ }
537
+ }
538
+ handleUnknownKey(key) {
539
+ const policy = this.options.onUnknownKey ?? 'ignore';
540
+ if (policy === 'throw')
541
+ throw new UnknownFilterKeyException(key);
542
+ if (policy === 'warn') {
543
+ this.logger.warn(`Unknown filter key: "${key}"`);
544
+ }
545
+ }
546
+ };
547
+ FilterRunner = FilterRunner_1 = __decorate([
548
+ Injectable(),
549
+ __param(1, Inject(FILTER_MODULE_OPTIONS)),
550
+ __param(2, Inject(FILTER_ADAPTER)),
551
+ __metadata("design:paramtypes", [ModuleRef, Object, Object])
552
+ ], FilterRunner);
553
+ export { FilterRunner };
554
+ //# sourceMappingURL=runner.js.map