json-rest-api 1.0.14 → 1.0.15

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 (118) hide show
  1. package/connectors_ideas/fastify.js +66 -68
  2. package/connectors_ideas/koa-plugin.js +50 -51
  3. package/eslint.config.js +11 -2
  4. package/examples/url-override-example.js +63 -63
  5. package/index.js +13 -13
  6. package/lib/enhanced-logger.js +51 -51
  7. package/lib/error-formatter.js +61 -61
  8. package/lib/rest-api-errors.js +28 -28
  9. package/package.json +1 -1
  10. package/plugins/core/bulk-operations-plugin.js +142 -145
  11. package/plugins/core/connectors/express-plugin.js +210 -217
  12. package/plugins/core/connectors/lib/busboy-detector.js +84 -84
  13. package/plugins/core/connectors/lib/formidable-detector.js +50 -50
  14. package/plugins/core/connectors/lib/request-helpers.js +74 -74
  15. package/plugins/core/file-handling-plugin.js +125 -125
  16. package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
  17. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
  18. package/plugins/core/lib/anyapi/schema-utils.js +101 -101
  19. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
  20. package/plugins/core/lib/dbTablesOperations.js +134 -135
  21. package/plugins/core/lib/fractional-positioning.js +74 -76
  22. package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
  23. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
  24. package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
  25. package/plugins/core/lib/querying/knex-process-includes.js +50 -50
  26. package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
  27. package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
  28. package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
  29. package/plugins/core/lib/querying/knex-window-queries.js +89 -89
  30. package/plugins/core/lib/querying/url-helpers.js +35 -35
  31. package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
  32. package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
  33. package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
  34. package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
  35. package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
  36. package/plugins/core/lib/querying-writing/field-utils.js +44 -42
  37. package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
  38. package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
  39. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
  40. package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
  41. package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
  42. package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
  43. package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
  44. package/plugins/core/lib/storage/storage-adapter.js +68 -68
  45. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
  46. package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
  47. package/plugins/core/lib/writing/relationship-processor.js +58 -58
  48. package/plugins/core/rest-api-access.js +8 -8
  49. package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
  50. package/plugins/core/rest-api-cors-plugin.js +170 -171
  51. package/plugins/core/rest-api-knex-plugin.js +482 -493
  52. package/plugins/core/rest-api-multihome-plugin.js +247 -251
  53. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
  54. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
  55. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
  57. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
  58. package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
  59. package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
  60. package/plugins/core/rest-api-plugin-methods/common.js +295 -297
  61. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
  62. package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
  63. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
  64. package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
  65. package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
  66. package/plugins/core/rest-api-plugin-methods/get.js +86 -86
  67. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
  68. package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
  69. package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
  70. package/plugins/core/rest-api-plugin-methods/post.js +61 -62
  71. package/plugins/core/rest-api-plugin-methods/put.js +86 -87
  72. package/plugins/core/rest-api-plugin-methods/query.js +88 -88
  73. package/plugins/core/rest-api-plugin-methods/release.js +5 -6
  74. package/plugins/core/rest-api-plugin.js +76 -78
  75. package/plugins/core/rest-api-positioning-plugin.js +251 -252
  76. package/plugins/core/socketio-plugin.js +240 -236
  77. package/plugins/storage/local-storage.js +103 -103
  78. package/plugins/storage/s3-storage.js +41 -42
  79. package/quickTest.js +37 -37
  80. package/scripts/debug-anyapi-review-filter.mjs +18 -18
  81. package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
  82. package/scripts/inspect-anyapi-descriptor.mjs +10 -10
  83. package/scripts/run-docs.js +112 -113
  84. package/test-debug.js +28 -28
  85. package/tests/TEST_TEMPLATE.test.js +51 -51
  86. package/tests/anyapi-basic.test.js +42 -42
  87. package/tests/anyapi-cursor-pagination.test.js +48 -48
  88. package/tests/bulk-operations.test.js +208 -208
  89. package/tests/computed-fields.test.js +204 -204
  90. package/tests/cors.test.js +412 -413
  91. package/tests/curl.test.js +275 -276
  92. package/tests/custom-idproperty-relationships.test.js +266 -266
  93. package/tests/field-getters.test.js +132 -132
  94. package/tests/field-setters.test.js +110 -110
  95. package/tests/fixtures/api-configs.js +699 -699
  96. package/tests/helpers/storage-mode.js +32 -32
  97. package/tests/helpers/test-utils.js +76 -76
  98. package/tests/include-depth-validation.test.js +76 -76
  99. package/tests/includes.test.js +145 -145
  100. package/tests/multihome.test.js +175 -175
  101. package/tests/nested-includes.test.js +255 -255
  102. package/tests/pagination-cursor-multifield.test.js +164 -164
  103. package/tests/pagination-enhanced.test.js +225 -225
  104. package/tests/polymorphic-relationships.test.js +96 -96
  105. package/tests/polymorphic-simple.test.js +44 -44
  106. package/tests/positioning-advanced.test.js +226 -226
  107. package/tests/positioning-extended.test.js +399 -399
  108. package/tests/positioning-multifilter.test.js +204 -204
  109. package/tests/positioning.test.js +266 -266
  110. package/tests/queries.test.js +184 -184
  111. package/tests/query-limits.test.js +154 -155
  112. package/tests/relationship-endpoints.test.js +127 -127
  113. package/tests/relationships.test.js +178 -179
  114. package/tests/rest-api-access.test.js +86 -86
  115. package/tests/rest-api.test.js +103 -103
  116. package/tests/searchschema-merge.test.js +93 -93
  117. package/tests/socketio.test.js +326 -326
  118. package/tests/virtual-fields.test.js +164 -164
@@ -1,63 +1,63 @@
1
- import { analyzeRequiredIndexes, buildJoinChain } from './knex-cross-table-search.js';
1
+ import { analyzeRequiredIndexes, buildJoinChain } from './knex-cross-table-search.js'
2
2
 
3
3
  const createAdapterUtilities = (hookParams, { getStorageAdapter } = {}) => {
4
- const context = hookParams.context || {};
5
- const storageCache = new Map();
4
+ const context = hookParams.context || {}
5
+ const storageCache = new Map()
6
6
 
7
7
  const fetchStorageAdapter = (scopeName) => {
8
- if (!scopeName) return null;
9
- if (storageCache.has(scopeName)) return storageCache.get(scopeName);
8
+ if (!scopeName) return null
9
+ if (storageCache.has(scopeName)) return storageCache.get(scopeName)
10
10
 
11
- let adapter = null;
11
+ let adapter = null
12
12
  if (scopeName === context?.knexQuery?.scopeName) {
13
- adapter = context.storageAdapter
14
- || context.knexQuery?.storageAdapter
15
- || (typeof getStorageAdapter === 'function' ? getStorageAdapter(scopeName) : null);
13
+ adapter = context.storageAdapter ||
14
+ context.knexQuery?.storageAdapter ||
15
+ (typeof getStorageAdapter === 'function' ? getStorageAdapter(scopeName) : null)
16
16
  if (adapter && !context.storageAdapter) {
17
- context.storageAdapter = adapter;
17
+ context.storageAdapter = adapter
18
18
  }
19
19
  } else {
20
- adapter = typeof getStorageAdapter === 'function' ? getStorageAdapter(scopeName) : null;
20
+ adapter = typeof getStorageAdapter === 'function' ? getStorageAdapter(scopeName) : null
21
21
  }
22
22
 
23
- storageCache.set(scopeName, adapter);
24
- return adapter;
25
- };
23
+ storageCache.set(scopeName, adapter)
24
+ return adapter
25
+ }
26
26
 
27
27
  const defaultAliasForScope = (scopeName) => {
28
28
  if (scopeName === context?.knexQuery?.scopeName) {
29
- return context.knexQuery?.tableName || scopeName;
29
+ return context.knexQuery?.tableName || scopeName
30
30
  }
31
- return scopeName;
32
- };
31
+ return scopeName
32
+ }
33
33
 
34
34
  const translateColumn = (scopeName, field, alias = defaultAliasForScope(scopeName)) => {
35
- const adapter = fetchStorageAdapter(scopeName);
36
- const translated = adapter?.translateColumn?.(field) ?? field;
37
- if (!alias) return translated;
38
- return `${alias}.${translated}`;
39
- };
35
+ const adapter = fetchStorageAdapter(scopeName)
36
+ const translated = adapter?.translateColumn?.(field) ?? field
37
+ if (!alias) return translated
38
+ return `${alias}.${translated}`
39
+ }
40
40
 
41
41
  const translateFilterValue = (scopeName, field, value) => {
42
- const adapter = fetchStorageAdapter(scopeName);
43
- if (!adapter?.translateFilterValue) return value;
44
- return adapter.translateFilterValue(field, value);
45
- };
42
+ const adapter = fetchStorageAdapter(scopeName)
43
+ if (!adapter?.translateFilterValue) return value
44
+ return adapter.translateFilterValue(field, value)
45
+ }
46
46
 
47
47
  return {
48
48
  fetchStorageAdapter,
49
49
  defaultAliasForScope,
50
50
  translateColumn,
51
51
  translateFilterValue,
52
- };
53
- };
52
+ }
53
+ }
54
54
 
55
55
  /**
56
56
  * Processes filters that target polymorphic relationships where a single relationship can point to different types of resources
57
- *
57
+ *
58
58
  * @param {Object} hookParams - Hook parameters containing context
59
59
  * @param {Object} dependencies - Dependencies injected by the plugin
60
- *
60
+ *
61
61
  * @example
62
62
  * // Input: Search schema with polymorphic filter
63
63
  * const searchSchema = {
@@ -66,15 +66,15 @@ const createAdapterUtilities = (hookParams, { getStorageAdapter } = {}) => {
66
66
  * polymorphicField: 'commentable', // Points to the polymorphic relationship
67
67
  * targetFields: {
68
68
  * posts: 'title', // When commentable_type='posts', search posts.title
69
- * videos: 'title', // When commentable_type='videos', search videos.title
69
+ * videos: 'title', // When commentable_type='videos', search videos.title
70
70
  * articles: 'headline' // When commentable_type='articles', search articles.headline
71
71
  * },
72
72
  * filterOperator: 'like'
73
73
  * }
74
74
  * };
75
- *
75
+ *
76
76
  * // Filter request: { commentable_title: 'JavaScript' }
77
- *
77
+ *
78
78
  * // Result: Adds conditional LEFT JOINs and WHERE conditions
79
79
  * // SQL generated:
80
80
  * // LEFT JOIN posts ON comments.commentable_type = 'posts' AND comments.commentable_id = posts.id
@@ -85,7 +85,7 @@ const createAdapterUtilities = (hookParams, { getStorageAdapter } = {}) => {
85
85
  * // (comments.commentable_type = 'videos' AND videos.title LIKE '%JavaScript%') OR
86
86
  * // (comments.commentable_type = 'articles' AND articles.headline LIKE '%JavaScript%')
87
87
  * // )
88
- *
88
+ *
89
89
  * @example
90
90
  * // Input: Complex polymorphic filter with cross-table paths
91
91
  * const searchSchema = {
@@ -98,26 +98,26 @@ const createAdapterUtilities = (hookParams, { getStorageAdapter } = {}) => {
98
98
  * }
99
99
  * }
100
100
  * };
101
- *
101
+ *
102
102
  * // Result: Creates nested JOINs for each polymorphic type
103
103
  * // LEFT JOIN posts ON comments.commentable_type = 'posts' AND comments.commentable_id = posts.id
104
104
  * // LEFT JOIN users AS posts_author ON posts.author_id = posts_author.id
105
105
  * // LEFT JOIN videos ON comments.commentable_type = 'videos' AND comments.commentable_id = videos.id
106
106
  * // LEFT JOIN users AS videos_creator ON videos.creator_id = videos_creator.id
107
- *
107
+ *
108
108
  * @description
109
109
  * Used by:
110
110
  * - rest-api-knex-plugin calls this hook first during query building
111
111
  * - Must run before other filter hooks to establish JOINs
112
112
  * - Applied when searchSchema contains polymorphicField definitions
113
- *
113
+ *
114
114
  * Purpose:
115
115
  * - Enables filtering on polymorphic relationships without knowing the concrete type
116
116
  * - Searches across multiple tables with a single filter parameter
117
117
  * - Supports queries like "find all comments on content containing 'JavaScript'"
118
118
  * - Uses conditional JOINs that only match when type field equals expected value
119
119
  * - Maintains performance by leveraging indexed type/id columns
120
- *
120
+ *
121
121
  * Data flow:
122
122
  * 1. Identifies filters with polymorphicField in searchSchema
123
123
  * 2. For each polymorphic type, adds conditional LEFT JOIN
@@ -126,167 +126,166 @@ const createAdapterUtilities = (hookParams, { getStorageAdapter } = {}) => {
126
126
  * 5. Returns modified query with polymorphic search capabilities
127
127
  */
128
128
  export const polymorphicFiltersHook = async (hookParams, dependencies) => {
129
- const { log, scopes, knex } = dependencies;
130
- const adapterUtils = createAdapterUtilities(hookParams, dependencies);
131
-
132
- // Extract context
133
- const scopeName = hookParams.context?.knexQuery?.scopeName;
134
- const filters = hookParams.context?.knexQuery?.filters;
135
- const query = hookParams.context?.knexQuery?.query;
136
- const db = hookParams.context?.knexQuery?.db || knex;
129
+ const { log, scopes, knex } = dependencies
130
+ const adapterUtils = createAdapterUtilities(hookParams, dependencies)
137
131
 
138
- const schemaInfo = scopes[scopeName].vars.schemaInfo;
139
- const tableAlias = adapterUtils.defaultAliasForScope(scopeName);
132
+ // Extract context
133
+ const scopeName = hookParams.context?.knexQuery?.scopeName
134
+ const filters = hookParams.context?.knexQuery?.filters
135
+ const query = hookParams.context?.knexQuery?.query
136
+ const db = hookParams.context?.knexQuery?.db || knex
140
137
 
138
+ const schemaInfo = scopes[scopeName].vars.schemaInfo
139
+ const tableAlias = adapterUtils.defaultAliasForScope(scopeName)
141
140
 
142
141
  if (!filters) {
143
- return;
142
+ return
144
143
  }
145
144
 
146
145
  // Step 1: Identify polymorphic searches
147
- const polymorphicSearches = new Map();
148
- const polymorphicJoins = new Map();
146
+ const polymorphicSearches = new Map()
147
+ const polymorphicJoins = new Map()
149
148
 
150
149
  for (const [filterKey, filterValue] of Object.entries(filters)) {
151
- const fieldDef = schemaInfo.searchSchemaStructure[filterKey];
150
+ const fieldDef = schemaInfo.searchSchemaStructure[filterKey]
152
151
 
153
152
  if (fieldDef?.polymorphicField && fieldDef?.targetFields && filterValue !== undefined) {
154
- log.trace('[POLYMORPHIC-SEARCH] Found polymorphic search:', {
155
- filterKey,
156
- polymorphicField: fieldDef.polymorphicField
157
- });
158
-
153
+ log.trace('[POLYMORPHIC-SEARCH] Found polymorphic search:', {
154
+ filterKey,
155
+ polymorphicField: fieldDef.polymorphicField
156
+ })
157
+
159
158
  polymorphicSearches.set(filterKey, {
160
159
  fieldDef,
161
160
  filterValue,
162
161
  polymorphicField: fieldDef.polymorphicField
163
- });
162
+ })
164
163
  }
165
164
  }
166
165
 
167
166
  if (polymorphicSearches.size === 0) {
168
- return;
167
+ return
169
168
  }
170
169
 
171
170
  // Step 2: Build polymorphic JOINs
172
- log.trace('[POLYMORPHIC-SEARCH] Building JOINs for polymorphic searches');
173
-
171
+ log.trace('[POLYMORPHIC-SEARCH] Building JOINs for polymorphic searches')
172
+
174
173
  for (const [filterKey, searchInfo] of polymorphicSearches) {
175
- const { fieldDef, polymorphicField } = searchInfo;
176
-
174
+ const { fieldDef, polymorphicField } = searchInfo
175
+
177
176
  // Get the relationship definition
178
- const relationships = scopes[scopeName].vars.schemaInfo.schemaRelationships;
179
- const polyRel = relationships[polymorphicField];
180
-
177
+ const relationships = scopes[scopeName].vars.schemaInfo.schemaRelationships
178
+ const polyRel = relationships[polymorphicField]
179
+
181
180
  if (!polyRel?.belongsToPolymorphic) {
182
181
  throw new Error(
183
182
  `Polymorphic field '${polymorphicField}' not found in relationships for scope '${scopeName}'`
184
- );
183
+ )
185
184
  }
186
-
187
- const { typeField, idField } = polyRel.belongsToPolymorphic;
188
-
185
+
186
+ const { typeField, idField } = polyRel.belongsToPolymorphic
187
+
189
188
  // Build JOINs for each target type
190
189
  for (const [targetType, targetFieldPath] of Object.entries(fieldDef.targetFields)) {
191
- const baseAlias = `${tableAlias}_${polymorphicField}_${targetType}`;
192
-
190
+ const baseAlias = `${tableAlias}_${polymorphicField}_${targetType}`
191
+
193
192
  // Skip if we already added this JOIN
194
193
  if (!polymorphicJoins.has(baseAlias)) {
195
- const targetSchema = scopes[targetType].vars.schemaInfo.schemaInstance;
196
- const targetTable = targetSchema?.tableName || targetType;
197
- const targetIdField = scopes[targetType].vars.schemaInfo.idProperty || 'id';
198
-
199
- log.trace('[POLYMORPHIC-SEARCH] Adding conditional JOIN:', {
200
- targetType,
201
- alias: baseAlias
202
- });
203
-
194
+ const targetSchema = scopes[targetType].vars.schemaInfo.schemaInstance
195
+ const targetTable = targetSchema?.tableName || targetType
196
+ const targetIdField = scopes[targetType].vars.schemaInfo.idProperty || 'id'
197
+
198
+ log.trace('[POLYMORPHIC-SEARCH] Adding conditional JOIN:', {
199
+ targetType,
200
+ alias: baseAlias
201
+ })
202
+
204
203
  // Conditional JOIN - only matches when type is correct
205
- query.leftJoin(`${targetTable} as ${baseAlias}`, function() {
206
- const typeColumn = adapterUtils.translateColumn(scopeName, typeField, tableAlias);
207
- const idColumn = adapterUtils.translateColumn(scopeName, idField, tableAlias);
208
- const targetIdColumn = adapterUtils.translateColumn(targetType, targetIdField, baseAlias);
204
+ query.leftJoin(`${targetTable} as ${baseAlias}`, function () {
205
+ const typeColumn = adapterUtils.translateColumn(scopeName, typeField, tableAlias)
206
+ const idColumn = adapterUtils.translateColumn(scopeName, idField, tableAlias)
207
+ const targetIdColumn = adapterUtils.translateColumn(targetType, targetIdField, baseAlias)
209
208
 
210
209
  this.on(typeColumn, db.raw('?', [adapterUtils.translateFilterValue(scopeName, typeField, targetType)]))
211
- .andOn(idColumn, targetIdColumn);
212
- });
213
-
210
+ .andOn(idColumn, targetIdColumn)
211
+ })
212
+
214
213
  polymorphicJoins.set(baseAlias, {
215
214
  targetType,
216
215
  targetTable,
217
216
  targetFieldPath
218
- });
219
- polymorphicJoins.get(baseAlias).baseAlias = baseAlias;
220
- polymorphicJoins.get(baseAlias).targetIdField = targetIdField;
217
+ })
218
+ polymorphicJoins.get(baseAlias).baseAlias = baseAlias
219
+ polymorphicJoins.get(baseAlias).targetIdField = targetIdField
221
220
 
222
221
  if (!polymorphicJoins.get(baseAlias).aliasScopeMap) {
223
- polymorphicJoins.get(baseAlias).aliasScopeMap = new Map();
222
+ polymorphicJoins.get(baseAlias).aliasScopeMap = new Map()
224
223
  }
225
- polymorphicJoins.get(baseAlias).aliasScopeMap.set(baseAlias, targetType);
224
+ polymorphicJoins.get(baseAlias).aliasScopeMap.set(baseAlias, targetType)
226
225
 
227
226
  // Handle cross-table paths
228
227
  if (targetFieldPath.includes('.')) {
229
- log.trace('[POLYMORPHIC-SEARCH] Building cross-table JOINs for path:', targetFieldPath);
230
-
231
- const pathParts = targetFieldPath.split('.');
232
- let currentAlias = baseAlias;
233
- let currentScope = targetType;
234
-
228
+ log.trace('[POLYMORPHIC-SEARCH] Building cross-table JOINs for path:', targetFieldPath)
229
+
230
+ const pathParts = targetFieldPath.split('.')
231
+ let currentAlias = baseAlias
232
+ let currentScope = targetType
233
+
235
234
  // Build JOIN for each segment except the last
236
235
  for (let i = 0; i < pathParts.length - 1; i++) {
237
- const relationshipName = pathParts[i];
238
-
236
+ const relationshipName = pathParts[i]
237
+
239
238
  // Find the foreign key for this relationship
240
- const currentSchema = scopes[currentScope].vars.schemaInfo.schemaInstance;
241
- let foreignKeyField = null;
242
- let nextScope = null;
243
-
239
+ const currentSchema = scopes[currentScope].vars.schemaInfo.schemaInstance
240
+ let foreignKeyField = null
241
+ let nextScope = null
242
+
244
243
  // Search schema for matching belongsTo
245
244
  for (const [fieldName, fieldDef] of Object.entries(currentSchema.structure)) {
246
245
  if (fieldDef.as === relationshipName && fieldDef.belongsTo) {
247
- foreignKeyField = fieldName;
248
- nextScope = fieldDef.belongsTo;
249
- break;
246
+ foreignKeyField = fieldName
247
+ nextScope = fieldDef.belongsTo
248
+ break
250
249
  }
251
250
  }
252
-
251
+
253
252
  if (!foreignKeyField) {
254
253
  // Check relationships for hasOne
255
254
  const currentRelationships = scopes[currentScope].vars.schemaInfo.schemaRelationships
256
- const rel = currentRelationships?.[relationshipName];
255
+ const rel = currentRelationships?.[relationshipName]
257
256
  if (rel?.hasOne) {
258
257
  // Handle hasOne - more complex
259
258
  throw new Error(
260
- `Cross-table polymorphic search through hasOne relationships not yet supported`
261
- );
259
+ 'Cross-table polymorphic search through hasOne relationships not yet supported'
260
+ )
262
261
  }
263
-
262
+
264
263
  throw new Error(
265
264
  `Cannot resolve relationship '${relationshipName}' in path '${targetFieldPath}' for scope '${currentScope}'`
266
- );
265
+ )
267
266
  }
268
-
267
+
269
268
  // Build next JOIN
270
- const nextAlias = `${currentAlias}_${relationshipName}`;
271
- const nextSchema = scopes[nextScope].vars.schemaInfo.schemaInstance;
272
- const nextTable = nextSchema?.tableName || nextScope;
273
-
274
- log.trace('[POLYMORPHIC-SEARCH] Adding cross-table JOIN:', {
275
- from: currentAlias,
269
+ const nextAlias = `${currentAlias}_${relationshipName}`
270
+ const nextSchema = scopes[nextScope].vars.schemaInfo.schemaInstance
271
+ const nextTable = nextSchema?.tableName || nextScope
272
+
273
+ log.trace('[POLYMORPHIC-SEARCH] Adding cross-table JOIN:', {
274
+ from: currentAlias,
276
275
  to: nextAlias,
277
- table: nextTable
278
- });
279
-
280
- const nextIdField = scopes[nextScope].vars.schemaInfo.idProperty || 'id';
281
- const sourceColumn = adapterUtils.translateColumn(currentScope, foreignKeyField, currentAlias);
282
- const targetColumn = adapterUtils.translateColumn(nextScope, nextIdField, nextAlias);
276
+ table: nextTable
277
+ })
278
+
279
+ const nextIdField = scopes[nextScope].vars.schemaInfo.idProperty || 'id'
280
+ const sourceColumn = adapterUtils.translateColumn(currentScope, foreignKeyField, currentAlias)
281
+ const targetColumn = adapterUtils.translateColumn(nextScope, nextIdField, nextAlias)
283
282
 
284
- query.leftJoin(`${nextTable} as ${nextAlias}`, sourceColumn, targetColumn);
283
+ query.leftJoin(`${nextTable} as ${nextAlias}`, sourceColumn, targetColumn)
285
284
 
286
- currentAlias = nextAlias;
287
- currentScope = nextScope;
285
+ currentAlias = nextAlias
286
+ currentScope = nextScope
288
287
 
289
- polymorphicJoins.get(baseAlias).aliasScopeMap.set(currentAlias, currentScope);
288
+ polymorphicJoins.get(baseAlias).aliasScopeMap.set(currentAlias, currentScope)
290
289
  }
291
290
  }
292
291
  }
@@ -294,98 +293,98 @@ export const polymorphicFiltersHook = async (hookParams, dependencies) => {
294
293
  }
295
294
 
296
295
  // Pre-fetch relationships for WHERE clause processing
297
- const polymorphicRelationships = new Map();
296
+ const polymorphicRelationships = new Map()
298
297
  const relationships = scopes[scopeName].vars.schemaInfo.schemaRelationships
299
298
  for (const [filterKey, searchInfo] of polymorphicSearches) {
300
- const { polymorphicField } = searchInfo;
301
- const polyRel = relationships[polymorphicField];
299
+ const { polymorphicField } = searchInfo
300
+ const polyRel = relationships[polymorphicField]
302
301
  if (polyRel?.belongsToPolymorphic) {
303
- polymorphicRelationships.set(filterKey, polyRel);
302
+ polymorphicRelationships.set(filterKey, polyRel)
304
303
  }
305
304
  }
306
305
 
307
306
  // Mark that we have JOINs for other hooks
308
- hookParams.context.knexQuery.hasJoins = true;
307
+ hookParams.context.knexQuery.hasJoins = true
309
308
 
310
309
  // Step 3: Apply WHERE conditions
311
- query.where(function applyPolymorphicWhere() {
310
+ query.where(function applyPolymorphicWhere () {
312
311
  const applyComparison = (builder, scope, alias, field, operator, rawValue) => {
313
- const columnRef = adapterUtils.translateColumn(scope, field, alias);
314
- const normalizedValue = adapterUtils.translateFilterValue(scope, field, rawValue);
315
- const op = operator || '=';
312
+ const columnRef = adapterUtils.translateColumn(scope, field, alias)
313
+ const normalizedValue = adapterUtils.translateFilterValue(scope, field, rawValue)
314
+ const op = operator || '='
316
315
 
317
- const lowerOp = typeof op === 'string' ? op.toLowerCase() : op;
316
+ const lowerOp = typeof op === 'string' ? op.toLowerCase() : op
318
317
  if (normalizedValue === null && (lowerOp === '=' || lowerOp === '==')) {
319
- builder.whereNull(columnRef);
320
- return;
318
+ builder.whereNull(columnRef)
319
+ return
321
320
  }
322
321
 
323
322
  if (lowerOp === 'like') {
324
323
  if (normalizedValue === null || normalizedValue === undefined) {
325
- builder.whereNull(columnRef);
324
+ builder.whereNull(columnRef)
326
325
  } else {
327
- builder.where(columnRef, 'like', `%${normalizedValue}%`);
326
+ builder.where(columnRef, 'like', `%${normalizedValue}%`)
328
327
  }
329
- return;
328
+ return
330
329
  }
331
330
 
332
331
  if (Array.isArray(normalizedValue) && lowerOp === 'in') {
333
- builder.whereIn(columnRef, normalizedValue);
334
- return;
332
+ builder.whereIn(columnRef, normalizedValue)
333
+ return
335
334
  }
336
335
 
337
- builder.where(columnRef, op, normalizedValue);
338
- };
336
+ builder.where(columnRef, op, normalizedValue)
337
+ }
339
338
 
340
339
  for (const [filterKey] of Object.entries(filters)) {
341
- if (!polymorphicSearches.has(filterKey)) continue;
340
+ if (!polymorphicSearches.has(filterKey)) continue
342
341
 
343
- const searchInfo = polymorphicSearches.get(filterKey);
344
- const polyRel = polymorphicRelationships.get(filterKey);
345
- if (!polyRel) continue;
342
+ const searchInfo = polymorphicSearches.get(filterKey)
343
+ const polyRel = polymorphicRelationships.get(filterKey)
344
+ if (!polyRel) continue
346
345
 
347
- const { typeField } = polyRel.belongsToPolymorphic;
346
+ const { typeField } = polyRel.belongsToPolymorphic
348
347
 
349
- this.where(function applyTypeOrBranch() {
348
+ this.where(function applyTypeOrBranch () {
350
349
  for (const [targetType, targetFieldPath] of Object.entries(searchInfo.fieldDef.targetFields)) {
351
- this.orWhere(function applyTargetBranch() {
352
- const typeColumnAlias = tableAlias;
353
- applyComparison(this, scopeName, typeColumnAlias, typeField, '=', targetType);
350
+ this.orWhere(function applyTargetBranch () {
351
+ const typeColumnAlias = tableAlias
352
+ applyComparison(this, scopeName, typeColumnAlias, typeField, '=', targetType)
354
353
 
355
- const baseAlias = `${tableAlias}_${searchInfo.polymorphicField}_${targetType}`;
356
- const joinMeta = polymorphicJoins.get(baseAlias);
357
- const aliasScopeMap = joinMeta?.aliasScopeMap || new Map([[baseAlias, targetType]]);
354
+ const baseAlias = `${tableAlias}_${searchInfo.polymorphicField}_${targetType}`
355
+ const joinMeta = polymorphicJoins.get(baseAlias)
356
+ const aliasScopeMap = joinMeta?.aliasScopeMap || new Map([[baseAlias, targetType]])
358
357
 
359
358
  if (targetFieldPath.includes('.')) {
360
- const pathParts = targetFieldPath.split('.');
361
- const fieldName = pathParts[pathParts.length - 1];
359
+ const pathParts = targetFieldPath.split('.')
360
+ const fieldName = pathParts[pathParts.length - 1]
362
361
 
363
- let finalAlias = baseAlias;
362
+ let finalAlias = baseAlias
364
363
  for (let i = 0; i < pathParts.length - 1; i++) {
365
- finalAlias = `${finalAlias}_${pathParts[i]}`;
364
+ finalAlias = `${finalAlias}_${pathParts[i]}`
366
365
  }
367
366
 
368
- const finalScope = aliasScopeMap.get(finalAlias) || targetType;
369
- const operator = searchInfo.fieldDef.filterOperator || '=';
370
- applyComparison(this, finalScope, finalAlias, fieldName, operator, searchInfo.filterValue);
367
+ const finalScope = aliasScopeMap.get(finalAlias) || targetType
368
+ const operator = searchInfo.fieldDef.filterOperator || '='
369
+ applyComparison(this, finalScope, finalAlias, fieldName, operator, searchInfo.filterValue)
371
370
  } else {
372
- const operator = searchInfo.fieldDef.filterOperator || '=';
373
- const finalScope = aliasScopeMap.get(baseAlias) || targetType;
374
- applyComparison(this, finalScope, baseAlias, targetFieldPath, operator, searchInfo.filterValue);
371
+ const operator = searchInfo.fieldDef.filterOperator || '='
372
+ const finalScope = aliasScopeMap.get(baseAlias) || targetType
373
+ applyComparison(this, finalScope, baseAlias, targetFieldPath, operator, searchInfo.filterValue)
375
374
  }
376
- });
375
+ })
377
376
  }
378
- });
377
+ })
379
378
  }
380
- });
381
- };
379
+ })
380
+ }
382
381
 
383
382
  /**
384
383
  * Processes filters that require JOINs to access fields in related tables using dot notation
385
- *
384
+ *
386
385
  * @param {Object} hookParams - Hook parameters containing context
387
386
  * @param {Object} dependencies - Dependencies injected by the plugin
388
- *
387
+ *
389
388
  * @example
390
389
  * // Input: Simple cross-table filter
391
390
  * const searchSchema = {
@@ -395,16 +394,16 @@ export const polymorphicFiltersHook = async (hookParams, dependencies) => {
395
394
  * filterOperator: 'like'
396
395
  * }
397
396
  * };
398
- *
397
+ *
399
398
  * // Filter request: { author_name: 'Smith' }
400
399
  * // Query before: SELECT * FROM articles
401
- *
400
+ *
402
401
  * // Result: Adds JOIN and qualified WHERE
403
402
  * // Query after:
404
403
  * // SELECT * FROM articles
405
404
  * // LEFT JOIN users AS articles_author ON articles.author_id = articles_author.id
406
405
  * // WHERE articles_author.name LIKE '%Smith%'
407
- *
406
+ *
408
407
  * @example
409
408
  * // Input: Multi-field search across tables
410
409
  * const searchSchema = {
@@ -419,9 +418,9 @@ export const polymorphicFiltersHook = async (hookParams, dependencies) => {
419
418
  * filterOperator: 'like'
420
419
  * }
421
420
  * };
422
- *
421
+ *
423
422
  * // Filter request: { search: 'JavaScript' }
424
- *
423
+ *
425
424
  * // Result: Multiple JOINs and OR conditions
426
425
  * // SELECT DISTINCT * FROM articles
427
426
  * // LEFT JOIN users AS articles_author ON articles.author_id = articles_author.id
@@ -432,7 +431,7 @@ export const polymorphicFiltersHook = async (hookParams, dependencies) => {
432
431
  * // articles_author.name LIKE '%JavaScript%' OR
433
432
  * // articles_category.title LIKE '%JavaScript%'
434
433
  * // )
435
- *
434
+ *
436
435
  * @example
437
436
  * // Input: Deep nested relationships (3 levels)
438
437
  * const searchSchema = {
@@ -441,29 +440,29 @@ export const polymorphicFiltersHook = async (hookParams, dependencies) => {
441
440
  * actualField: 'author.company.country.name'
442
441
  * }
443
442
  * };
444
- *
443
+ *
445
444
  * // Filter request: { company_country: 'USA' }
446
- *
445
+ *
447
446
  * // Result: Chain of JOINs following relationships
448
447
  * // SELECT * FROM articles
449
448
  * // LEFT JOIN users AS articles_author ON articles.author_id = articles_author.id
450
449
  * // LEFT JOIN companies AS articles_author_company ON articles_author.company_id = articles_author_company.id
451
450
  * // LEFT JOIN countries AS articles_author_company_country ON articles_author_company.country_id = articles_author_company_country.id
452
451
  * // WHERE articles_author_company_country.name = 'USA'
453
- *
452
+ *
454
453
  * @description
455
454
  * Used by:
456
455
  * - rest-api-knex-plugin calls this hook second during query building
457
456
  * - Runs after polymorphicFiltersHook but before basicFiltersHook
458
457
  * - Applied when filters contain dot notation in actualField or oneOf
459
- *
458
+ *
460
459
  * Purpose:
461
460
  * - Enables filtering on related table fields without manual JOIN writing
462
461
  * - Automatically builds JOIN chains from dot notation paths
463
462
  * - Detects one-to-many relationships and adds DISTINCT to prevent duplicates
464
463
  * - Creates unique aliases to avoid naming conflicts
465
464
  * - Validates that target fields are indexed for performance
466
- *
465
+ *
467
466
  * Data flow:
468
467
  * 1. Scans filters for dot notation in actualField or oneOf arrays
469
468
  * 2. For each cross-table reference, builds JOIN chain via buildJoinChain
@@ -473,332 +472,332 @@ export const polymorphicFiltersHook = async (hookParams, dependencies) => {
473
472
  * 6. Sets hasJoins flag for basicFiltersHook to use
474
473
  */
475
474
  export const crossTableFiltersHook = async (hookParams, dependencies) => {
476
- const { log, scopes, knex } = dependencies;
477
- const adapterUtils = createAdapterUtilities(hookParams, dependencies);
478
-
475
+ const { log, scopes, knex } = dependencies
476
+ const adapterUtils = createAdapterUtilities(hookParams, dependencies)
477
+
479
478
  // Extract context
480
- const scopeName = hookParams.context?.knexQuery?.scopeName;
481
- const filters = hookParams.context?.knexQuery?.filters;
482
- const query = hookParams.context?.knexQuery?.query;
483
- const db = hookParams.context?.knexQuery?.db || knex;
484
-
485
- const schemaInfo = scopes[scopeName].vars.schemaInfo;
486
- const tableName = schemaInfo.tableName;
487
- const tableAlias = adapterUtils.defaultAliasForScope(scopeName);
488
- const aliasScopeMap = new Map();
489
- aliasScopeMap.set(tableAlias, scopeName);
479
+ const scopeName = hookParams.context?.knexQuery?.scopeName
480
+ const filters = hookParams.context?.knexQuery?.filters
481
+ const query = hookParams.context?.knexQuery?.query
482
+ const db = hookParams.context?.knexQuery?.db || knex
483
+
484
+ const schemaInfo = scopes[scopeName].vars.schemaInfo
485
+ const tableName = schemaInfo.tableName
486
+ const tableAlias = adapterUtils.defaultAliasForScope(scopeName)
487
+ const aliasScopeMap = new Map()
488
+ aliasScopeMap.set(tableAlias, scopeName)
490
489
 
491
490
  if (!filters) {
492
- return;
491
+ return
493
492
  }
494
493
 
495
494
  // Step 1: Analyze indexes
496
- const requiredIndexes = analyzeRequiredIndexes(scopes, log, scopeName, schemaInfo);
495
+ const requiredIndexes = analyzeRequiredIndexes(scopes, log, scopeName, schemaInfo)
497
496
  if (requiredIndexes.length > 0) {
498
- log.debug(`Cross-table search requires indexes:`, requiredIndexes);
497
+ log.debug('Cross-table search requires indexes:', requiredIndexes)
499
498
  }
500
499
 
501
500
  // Step 2: Build JOIN maps
502
- const joinMap = new Map();
503
- const fieldPathMap = new Map();
504
- let hasCrossTableFilters = false;
501
+ const joinMap = new Map()
502
+ const fieldPathMap = new Map()
503
+ let hasCrossTableFilters = false
505
504
 
506
505
  for (const [filterKey, fieldDef] of Object.entries(schemaInfo.searchSchemaStructure)) {
507
- if (filters[filterKey] === undefined) continue;
506
+ if (filters[filterKey] === undefined) continue
508
507
 
509
508
  // Skip polymorphic filters
510
- if (fieldDef.polymorphicField) continue;
509
+ if (fieldDef.polymorphicField) continue
511
510
 
512
511
  // Check actualField for cross-table references
513
512
  if (fieldDef.actualField?.includes('.')) {
514
- hasCrossTableFilters = true;
515
- log.trace('[JOIN-DETECTION] Cross-table actualField found', { filterKey, actualField: fieldDef.actualField, scopeName });
516
- const joinInfo = await buildJoinChain(scopes, log, scopeName, fieldDef.actualField);
513
+ hasCrossTableFilters = true
514
+ log.trace('[JOIN-DETECTION] Cross-table actualField found', { filterKey, actualField: fieldDef.actualField, scopeName })
515
+ const joinInfo = await buildJoinChain(scopes, log, scopeName, fieldDef.actualField)
517
516
  if (!joinMap.has(joinInfo.joinAlias)) {
518
- joinMap.set(joinInfo.joinAlias, joinInfo);
517
+ joinMap.set(joinInfo.joinAlias, joinInfo)
519
518
  }
520
- fieldPathMap.set(fieldDef.actualField, `${joinInfo.joinAlias}.${joinInfo.targetField}`);
519
+ fieldPathMap.set(fieldDef.actualField, `${joinInfo.joinAlias}.${joinInfo.targetField}`)
521
520
  }
522
521
 
523
522
  // Check oneOf for cross-table references
524
523
  if (fieldDef.oneOf && Array.isArray(fieldDef.oneOf)) {
525
524
  for (const field of fieldDef.oneOf) {
526
525
  if (field.includes('.')) {
527
- hasCrossTableFilters = true;
528
- log.trace('[JOIN-DETECTION] Cross-table oneOf field found', { filterKey, field, scopeName });
529
- const joinInfo = await buildJoinChain(scopes, log, scopeName, field);
526
+ hasCrossTableFilters = true
527
+ log.trace('[JOIN-DETECTION] Cross-table oneOf field found', { filterKey, field, scopeName })
528
+ const joinInfo = await buildJoinChain(scopes, log, scopeName, field)
530
529
  if (!joinMap.has(joinInfo.joinAlias)) {
531
- joinMap.set(joinInfo.joinAlias, joinInfo);
530
+ joinMap.set(joinInfo.joinAlias, joinInfo)
532
531
  }
533
- fieldPathMap.set(field, `${joinInfo.joinAlias}.${joinInfo.targetField}`);
532
+ fieldPathMap.set(field, `${joinInfo.joinAlias}.${joinInfo.targetField}`)
534
533
  }
535
534
  }
536
535
  }
537
536
  }
538
537
 
539
538
  if (!hasCrossTableFilters) {
540
- return;
539
+ return
541
540
  }
542
541
 
543
542
  joinMap.forEach((joinInfo) => {
544
543
  if (joinInfo.joinAlias) {
545
- aliasScopeMap.set(joinInfo.joinAlias, joinInfo.targetScopeName || joinInfo.targetTableName);
544
+ aliasScopeMap.set(joinInfo.joinAlias, joinInfo.targetScopeName || joinInfo.targetTableName)
546
545
  }
547
546
  if (joinInfo.isMultiLevel && Array.isArray(joinInfo.joinChain)) {
548
547
  joinInfo.joinChain.forEach((join) => {
549
548
  if (join.joinAlias) {
550
- aliasScopeMap.set(join.joinAlias, join.targetScopeName || join.targetTableName);
549
+ aliasScopeMap.set(join.joinAlias, join.targetScopeName || join.targetTableName)
551
550
  }
552
- });
551
+ })
553
552
  }
554
- });
553
+ })
555
554
 
556
555
  const translateQualifiedColumn = (qualified) => {
557
- const trimmed = qualified.trim();
556
+ const trimmed = qualified.trim()
558
557
  if (!trimmed.includes('.')) {
559
- return adapterUtils.translateColumn(scopeName, trimmed, tableAlias);
558
+ return adapterUtils.translateColumn(scopeName, trimmed, tableAlias)
560
559
  }
561
560
 
562
- const [alias, ...fieldParts] = trimmed.split('.');
563
- const field = fieldParts.join('.');
564
- const scopeForAlias = aliasScopeMap.get(alias) || scopeName;
565
- return adapterUtils.translateColumn(scopeForAlias, field, alias);
566
- };
561
+ const [alias, ...fieldParts] = trimmed.split('.')
562
+ const field = fieldParts.join('.')
563
+ const scopeForAlias = aliasScopeMap.get(alias) || scopeName
564
+ return adapterUtils.translateColumn(scopeForAlias, field, alias)
565
+ }
567
566
 
568
567
  const resolveFieldColumn = (field) => {
569
568
  if (field.includes('.')) {
570
- const qualified = fieldPathMap.get(field) || field;
571
- return translateQualifiedColumn(qualified);
569
+ const qualified = fieldPathMap.get(field) || field
570
+ return translateQualifiedColumn(qualified)
572
571
  }
573
- return adapterUtils.translateColumn(scopeName, field, tableAlias);
574
- };
572
+ return adapterUtils.translateColumn(scopeName, field, tableAlias)
573
+ }
575
574
 
576
575
  const normalizeFieldValue = (field, value) => {
577
576
  if (field.includes('.')) {
578
- const qualified = fieldPathMap.get(field) || field;
579
- const [alias, ...rest] = qualified.split('.');
580
- const fieldName = rest.join('.');
581
- const valueScope = aliasScopeMap.get(alias) || scopeName;
577
+ const qualified = fieldPathMap.get(field) || field
578
+ const [alias, ...rest] = qualified.split('.')
579
+ const fieldName = rest.join('.')
580
+ const valueScope = aliasScopeMap.get(alias) || scopeName
582
581
  if (Array.isArray(value)) {
583
- return value.map((entry) => adapterUtils.translateFilterValue(valueScope, fieldName, entry));
582
+ return value.map((entry) => adapterUtils.translateFilterValue(valueScope, fieldName, entry))
584
583
  }
585
- return adapterUtils.translateFilterValue(valueScope, fieldName, value);
584
+ return adapterUtils.translateFilterValue(valueScope, fieldName, value)
586
585
  }
587
586
  if (Array.isArray(value)) {
588
- return value.map((entry) => adapterUtils.translateFilterValue(scopeName, field, entry));
587
+ return value.map((entry) => adapterUtils.translateFilterValue(scopeName, field, entry))
589
588
  }
590
- return adapterUtils.translateFilterValue(scopeName, field, value);
591
- };
589
+ return adapterUtils.translateFilterValue(scopeName, field, value)
590
+ }
592
591
 
593
592
  // Step 3: Apply JOINs
594
- const appliedJoins = new Set();
593
+ const appliedJoins = new Set()
595
594
 
596
595
  const applyPolymorphicJoin = (join) => {
597
- query.leftJoin(`${join.targetTableName} as ${join.joinAlias}`, function() {
598
- const parts = join.joinCondition.split(' AND ');
599
- const [typeCondition, idCondition] = parts;
596
+ query.leftJoin(`${join.targetTableName} as ${join.joinAlias}`, function () {
597
+ const parts = join.joinCondition.split(' AND ')
598
+ const [typeCondition, idCondition] = parts
600
599
 
601
- const typeMatch = typeCondition.match(/(.+?)\s*=\s*'(.+?)'/);
600
+ const typeMatch = typeCondition.match(/(.+?)\s*=\s*'(.+?)'/)
602
601
  if (typeMatch) {
603
- const typeColumnToken = typeMatch[1].trim();
604
- const typeAlias = typeColumnToken.split('.')[0];
605
- const typeField = typeColumnToken.split('.').slice(1).join('.');
606
- const typeScope = aliasScopeMap.get(typeAlias) || scopeName;
607
- const translatedColumn = translateQualifiedColumn(typeColumnToken);
608
- const translatedValue = adapterUtils.translateFilterValue(typeScope, typeField, typeMatch[2]);
609
- this.on(translatedColumn, db.raw('?', [translatedValue]));
602
+ const typeColumnToken = typeMatch[1].trim()
603
+ const typeAlias = typeColumnToken.split('.')[0]
604
+ const typeField = typeColumnToken.split('.').slice(1).join('.')
605
+ const typeScope = aliasScopeMap.get(typeAlias) || scopeName
606
+ const translatedColumn = translateQualifiedColumn(typeColumnToken)
607
+ const translatedValue = adapterUtils.translateFilterValue(typeScope, typeField, typeMatch[2])
608
+ this.on(translatedColumn, db.raw('?', [translatedValue]))
610
609
  }
611
610
 
612
- const idMatch = idCondition.match(/(.+?)\s*=\s*(.+)/);
611
+ const idMatch = idCondition.match(/(.+?)\s*=\s*(.+)/)
613
612
  if (idMatch) {
614
- const leftToken = idMatch[1].trim();
615
- const rightToken = idMatch[2].trim();
616
- const leftColumn = translateQualifiedColumn(leftToken);
617
- const rightColumn = translateQualifiedColumn(rightToken);
618
- this.andOn(leftColumn, rightColumn);
613
+ const leftToken = idMatch[1].trim()
614
+ const rightToken = idMatch[2].trim()
615
+ const leftColumn = translateQualifiedColumn(leftToken)
616
+ const rightColumn = translateQualifiedColumn(rightToken)
617
+ this.andOn(leftColumn, rightColumn)
619
618
  }
620
- });
621
- };
619
+ })
620
+ }
622
621
 
623
622
  const applyStandardJoin = (join) => {
624
- const [leftSide, rightSide] = join.joinCondition.split(' = ');
625
- const translatedLeft = translateQualifiedColumn(leftSide.trim());
626
- const translatedRight = translateQualifiedColumn(rightSide.trim());
627
- query.leftJoin(`${join.targetTableName} as ${join.joinAlias}`, function() {
628
- this.on(translatedLeft, translatedRight);
629
- });
630
- };
623
+ const [leftSide, rightSide] = join.joinCondition.split(' = ')
624
+ const translatedLeft = translateQualifiedColumn(leftSide.trim())
625
+ const translatedRight = translateQualifiedColumn(rightSide.trim())
626
+ query.leftJoin(`${join.targetTableName} as ${join.joinAlias}`, function () {
627
+ this.on(translatedLeft, translatedRight)
628
+ })
629
+ }
631
630
 
632
631
  const processJoin = (join) => {
633
- const joinKey = `${join.joinAlias}:${join.joinCondition}`;
634
- if (appliedJoins.has(joinKey)) return;
632
+ const joinKey = `${join.joinAlias}:${join.joinCondition}`
633
+ if (appliedJoins.has(joinKey)) return
635
634
 
636
635
  if (join.isPolymorphic && join.joinCondition.includes(' AND ')) {
637
- applyPolymorphicJoin(join);
636
+ applyPolymorphicJoin(join)
638
637
  } else {
639
- applyStandardJoin(join);
638
+ applyStandardJoin(join)
640
639
  }
641
640
 
642
- appliedJoins.add(joinKey);
643
- };
641
+ appliedJoins.add(joinKey)
642
+ }
644
643
 
645
644
  joinMap.forEach((joinInfo) => {
646
645
  if (joinInfo.isMultiLevel && Array.isArray(joinInfo.joinChain)) {
647
- joinInfo.joinChain.forEach(processJoin);
646
+ joinInfo.joinChain.forEach(processJoin)
648
647
  } else {
649
- processJoin(joinInfo);
648
+ processJoin(joinInfo)
650
649
  }
651
- });
650
+ })
652
651
 
653
652
  // Step 4: Handle DISTINCT
654
- let hasOneToManyJoins = false;
653
+ let hasOneToManyJoins = false
655
654
  joinMap.forEach((joinInfo) => {
656
655
  if (joinInfo.isOneToMany) {
657
- hasOneToManyJoins = true;
656
+ hasOneToManyJoins = true
658
657
  } else if (joinInfo.isMultiLevel && joinInfo.joinChain) {
659
658
  joinInfo.joinChain.forEach(join => {
660
- if (join.isOneToMany) hasOneToManyJoins = true;
661
- });
659
+ if (join.isOneToMany) hasOneToManyJoins = true
660
+ })
662
661
  }
663
- });
662
+ })
664
663
 
665
664
  if (hasOneToManyJoins) {
666
- log.trace('[DISTINCT] Adding DISTINCT to query due to one-to-many JOINs');
667
- query.distinct();
665
+ log.trace('[DISTINCT] Adding DISTINCT to query due to one-to-many JOINs')
666
+ query.distinct()
668
667
  }
669
668
 
670
669
  // Store state for basic filters hook
671
- hookParams.context.knexQuery.hasJoins = true;
670
+ hookParams.context.knexQuery.hasJoins = true
672
671
 
673
672
  // Step 5: Apply WHERE conditions for cross-table filters
674
- query.where(function() {
673
+ query.where(function () {
675
674
  for (const [filterKey, filterValue] of Object.entries(filters)) {
676
- const fieldDef = searchSchemaStructure[filterKey];
677
- if (!fieldDef) continue;
675
+ const fieldDef = searchSchemaStructure[filterKey]
676
+ if (!fieldDef) continue
678
677
 
679
678
  // Skip non-cross-table and polymorphic filters
680
- if (fieldDef.polymorphicField) continue;
679
+ if (fieldDef.polymorphicField) continue
681
680
  if (!fieldDef.actualField?.includes('.') &&
682
681
  !fieldDef.oneOf?.some(f => f.includes('.'))) {
683
- continue;
682
+ continue
684
683
  }
685
684
 
686
685
  // Process cross-table filters
687
686
  switch (true) {
688
687
  case fieldDef.oneOf && Array.isArray(fieldDef.oneOf): {
689
- const operator = fieldDef.filterOperator || '=';
688
+ const operator = fieldDef.filterOperator || '='
690
689
 
691
- let searchTerms = [filterValue];
690
+ let searchTerms = [filterValue]
692
691
  if (fieldDef.splitBy && typeof filterValue === 'string') {
693
- searchTerms = filterValue.split(fieldDef.splitBy).filter(term => term.trim());
692
+ searchTerms = filterValue.split(fieldDef.splitBy).filter(term => term.trim())
694
693
  } else if (Array.isArray(filterValue)) {
695
- searchTerms = filterValue;
694
+ searchTerms = filterValue
696
695
  }
697
696
 
698
697
  const applyTermComparison = (builder, method, field, raw) => {
699
- const columnRef = resolveFieldColumn(field);
700
- let normalized = normalizeFieldValue(field, raw);
698
+ const columnRef = resolveFieldColumn(field)
699
+ const normalized = normalizeFieldValue(field, raw)
701
700
 
702
701
  if (operator === 'like') {
703
- const value = Array.isArray(normalized) ? normalized[0] : normalized;
702
+ const value = Array.isArray(normalized) ? normalized[0] : normalized
704
703
  if (value === null || value === undefined) {
705
- builder[method === 'or' ? 'orWhereNull' : 'whereNull'](columnRef);
704
+ builder[method === 'or' ? 'orWhereNull' : 'whereNull'](columnRef)
706
705
  } else {
707
- builder[method === 'or' ? 'orWhere' : 'where'](columnRef, 'like', `%${String(value)}%`);
706
+ builder[method === 'or' ? 'orWhere' : 'where'](columnRef, 'like', `%${String(value)}%`)
708
707
  }
709
- return;
708
+ return
710
709
  }
711
710
 
712
711
  if (operator === 'in') {
713
- const values = Array.isArray(normalized) ? normalized : [normalized];
714
- builder[method === 'or' ? 'orWhereIn' : 'whereIn'](columnRef, values);
715
- return;
712
+ const values = Array.isArray(normalized) ? normalized : [normalized]
713
+ builder[method === 'or' ? 'orWhereIn' : 'whereIn'](columnRef, values)
714
+ return
716
715
  }
717
716
 
718
- const value = Array.isArray(normalized) ? normalized[0] : normalized;
717
+ const value = Array.isArray(normalized) ? normalized[0] : normalized
719
718
  if (value === null && (operator === '=' || operator === '==')) {
720
- builder[method === 'or' ? 'orWhereNull' : 'whereNull'](columnRef);
719
+ builder[method === 'or' ? 'orWhereNull' : 'whereNull'](columnRef)
721
720
  } else {
722
- builder[method === 'or' ? 'orWhere' : 'where'](columnRef, operator, value);
721
+ builder[method === 'or' ? 'orWhere' : 'where'](columnRef, operator, value)
723
722
  }
724
- };
723
+ }
725
724
 
726
- this.where(function() {
725
+ this.where(function () {
727
726
  if (fieldDef.matchAll && searchTerms.length > 1) {
728
727
  searchTerms.forEach(term => {
729
- this.andWhere(function() {
728
+ this.andWhere(function () {
730
729
  fieldDef.oneOf.forEach((field, index) => {
731
- const method = index === 0 ? 'and' : 'or';
732
- applyTermComparison(this, method, field, term);
733
- });
734
- });
735
- });
730
+ const method = index === 0 ? 'and' : 'or'
731
+ applyTermComparison(this, method, field, term)
732
+ })
733
+ })
734
+ })
736
735
  } else {
737
736
  fieldDef.oneOf.forEach((field, index) => {
738
- const method = index === 0 ? 'and' : 'or';
739
- applyTermComparison(this, method, field, filterValue);
740
- });
737
+ const method = index === 0 ? 'and' : 'or'
738
+ applyTermComparison(this, method, field, filterValue)
739
+ })
741
740
  }
742
- });
743
- break;
741
+ })
742
+ break
744
743
  }
745
744
 
746
745
  case fieldDef.applyFilter && typeof fieldDef.applyFilter === 'function':
747
- fieldDef.applyFilter.call(this, this, filterValue);
748
- break;
746
+ fieldDef.applyFilter.call(this, this, filterValue)
747
+ break
749
748
 
750
749
  default:
751
- const targetField = fieldDef.actualField || filterKey;
752
- const columnRef = resolveFieldColumn(targetField);
753
- const operator = fieldDef.filterOperator || '=';
754
- const normalized = normalizeFieldValue(targetField, filterValue);
750
+ const targetField = fieldDef.actualField || filterKey
751
+ const columnRef = resolveFieldColumn(targetField)
752
+ const operator = fieldDef.filterOperator || '='
753
+ const normalized = normalizeFieldValue(targetField, filterValue)
755
754
 
756
755
  switch (operator) {
757
756
  case 'like': {
758
- const value = Array.isArray(normalized) ? normalized[0] : normalized;
757
+ const value = Array.isArray(normalized) ? normalized[0] : normalized
759
758
  if (value === null || value === undefined) {
760
- this.whereNull(columnRef);
759
+ this.whereNull(columnRef)
761
760
  } else {
762
- this.where(columnRef, 'like', `%${String(value)}%`);
761
+ this.where(columnRef, 'like', `%${String(value)}%`)
763
762
  }
764
- break;
763
+ break
765
764
  }
766
765
  case 'in': {
767
- const values = Array.isArray(normalized) ? normalized : [normalized];
768
- this.whereIn(columnRef, values);
769
- break;
766
+ const values = Array.isArray(normalized) ? normalized : [normalized]
767
+ this.whereIn(columnRef, values)
768
+ break
770
769
  }
771
770
  case 'between': {
772
- const values = Array.isArray(normalized) ? normalized : [normalized];
771
+ const values = Array.isArray(normalized) ? normalized : [normalized]
773
772
  if (values.length === 2) {
774
- this.whereBetween(columnRef, values);
773
+ this.whereBetween(columnRef, values)
775
774
  } else {
776
- this.where(columnRef, operator, values[0]);
775
+ this.where(columnRef, operator, values[0])
777
776
  }
778
- break;
777
+ break
779
778
  }
780
779
  default: {
781
- const value = Array.isArray(normalized) ? normalized[0] : normalized;
780
+ const value = Array.isArray(normalized) ? normalized[0] : normalized
782
781
  if (value === null && (operator === '=' || operator === '==')) {
783
- this.whereNull(columnRef);
782
+ this.whereNull(columnRef)
784
783
  } else {
785
- this.where(columnRef, operator, value);
784
+ this.where(columnRef, operator, value)
786
785
  }
787
- break;
786
+ break
788
787
  }
789
788
  }
790
- break;
789
+ break
791
790
  }
792
791
  }
793
- });
794
- };
792
+ })
793
+ }
795
794
 
796
795
  /**
797
796
  * Processes filters that apply directly to fields on the main table
798
- *
797
+ *
799
798
  * @param {Object} hookParams - Hook parameters containing context
800
799
  * @param {Object} dependencies - Dependencies injected by the plugin
801
- *
800
+ *
802
801
  * @example
803
802
  * // Input: Basic equality filter
804
803
  * const searchSchema = {
@@ -807,13 +806,13 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
807
806
  * filterOperator: '=' // Default is '=' if not specified
808
807
  * }
809
808
  * };
810
- *
809
+ *
811
810
  * // Filter request: { status: 'published' }
812
811
  * // Query before: SELECT * FROM articles
813
- *
812
+ *
814
813
  * // Result: Adds qualified WHERE clause
815
814
  * // Query after: SELECT * FROM articles WHERE articles.status = 'published'
816
- *
815
+ *
817
816
  * @example
818
817
  * // Input: LIKE filter for partial text matching
819
818
  * const searchSchema = {
@@ -826,13 +825,13 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
826
825
  * filterOperator: 'like'
827
826
  * }
828
827
  * };
829
- *
828
+ *
830
829
  * // Filter request: { title: 'JavaScript', content: 'async' }
831
- *
830
+ *
832
831
  * // Result: Multiple LIKE conditions
833
- * // WHERE articles.title LIKE '%JavaScript%'
832
+ * // WHERE articles.title LIKE '%JavaScript%'
834
833
  * // AND articles.content LIKE '%async%'
835
- *
834
+ *
836
835
  * @example
837
836
  * // Input: Multi-field OR search with oneOf
838
837
  * const searchSchema = {
@@ -844,16 +843,16 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
844
843
  * matchAll: true // All terms must match somewhere
845
844
  * }
846
845
  * };
847
- *
846
+ *
848
847
  * // Filter request: { search: 'REST API' }
849
- *
848
+ *
850
849
  * // Result: Each term must match in at least one field
851
850
  * // WHERE (
852
851
  * // (articles.title LIKE '%REST%' OR articles.content LIKE '%REST%' OR articles.summary LIKE '%REST%')
853
852
  * // AND
854
853
  * // (articles.title LIKE '%API%' OR articles.content LIKE '%API%' OR articles.summary LIKE '%API%')
855
854
  * // )
856
- *
855
+ *
857
856
  * @example
858
857
  * // Input: Advanced operators - IN and BETWEEN
859
858
  * const searchSchema = {
@@ -870,18 +869,18 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
870
869
  * filterOperator: 'in'
871
870
  * }
872
871
  * };
873
- *
874
- * // Filter request: {
872
+ *
873
+ * // Filter request: {
875
874
  * // category_id: [1, 2, 3],
876
875
  * // price: [10.00, 99.99],
877
876
  * // tags: ['javascript', 'nodejs']
878
877
  * // }
879
- *
878
+ *
880
879
  * // Result: IN and BETWEEN clauses
881
880
  * // WHERE articles.category_id IN (1, 2, 3)
882
- * // AND articles.price BETWEEN 10.00 AND 99.99
881
+ * // AND articles.price BETWEEN 10.00 AND 99.99
883
882
  * // AND articles.tags IN ('javascript', 'nodejs')
884
- *
883
+ *
885
884
  * @example
886
885
  * // Input: Custom filter function
887
886
  * const searchSchema = {
@@ -904,20 +903,20 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
904
903
  * }
905
904
  * }
906
905
  * };
907
- *
906
+ *
908
907
  * // Filter request: { has_comments: true }
909
- *
908
+ *
910
909
  * // Result: Subquery to check existence
911
910
  * // WHERE EXISTS (
912
911
  * // SELECT id FROM comments WHERE comments.article_id = articles.id
913
912
  * // )
914
- *
913
+ *
915
914
  * @description
916
915
  * Used by:
917
916
  * - rest-api-knex-plugin calls this hook last during query building
918
917
  * - Runs after all JOINs have been established by previous hooks
919
918
  * - Handles all non-cross-table, non-polymorphic filters
920
- *
919
+ *
921
920
  * Purpose:
922
921
  * - Implements standard SQL filtering operations safely via Knex
923
922
  * - Always qualifies field names with table name to prevent ambiguity
@@ -925,7 +924,7 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
925
924
  * - Enables multi-field OR searches with optional term splitting
926
925
  * - Handles null values appropriately (using whereNull)
927
926
  * - Allows custom filter logic via applyFilter functions
928
- *
927
+ *
929
928
  * Data flow:
930
929
  * 1. Skips filters already handled by polymorphic/cross-table hooks
931
930
  * 2. Qualifies all field names with table name (e.g., articles.title)
@@ -935,18 +934,18 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
935
934
  * 6. Returns query with all basic filters applied
936
935
  */
937
936
  export const basicFiltersHook = async (hookParams, dependencies) => {
938
- const { log, scopes, knex } = dependencies;
939
- const adapterUtils = createAdapterUtilities(hookParams, dependencies);
940
-
937
+ const { log, scopes, knex } = dependencies
938
+ const adapterUtils = createAdapterUtilities(hookParams, dependencies)
939
+
941
940
  // Extract context
942
- const scopeName = hookParams.context?.knexQuery?.scopeName;
943
- const filters = hookParams.context?.knexQuery?.filters;
944
- const query = hookParams.context?.knexQuery?.query;
945
- const db = hookParams.context?.knexQuery?.db || knex;
946
-
947
- const schemaInfo = scopes[scopeName].vars.schemaInfo;
948
- const tableName = schemaInfo.tableName;
949
- const tableAlias = adapterUtils.defaultAliasForScope(scopeName);
941
+ const scopeName = hookParams.context?.knexQuery?.scopeName
942
+ const filters = hookParams.context?.knexQuery?.filters
943
+ const query = hookParams.context?.knexQuery?.query
944
+ const db = hookParams.context?.knexQuery?.db || knex
945
+
946
+ const schemaInfo = scopes[scopeName].vars.schemaInfo
947
+ const tableName = schemaInfo.tableName
948
+ const tableAlias = adapterUtils.defaultAliasForScope(scopeName)
950
949
 
951
950
  log.trace('[DEBUG basicFiltersHook] Called with:', {
952
951
  scopeName,
@@ -954,167 +953,167 @@ export const basicFiltersHook = async (hookParams, dependencies) => {
954
953
  filters,
955
954
  searchSchemaKeys: Object.keys(schemaInfo.searchSchemaStructure || {}),
956
955
  tableName
957
- });
956
+ })
958
957
 
959
958
  if (!filters) {
960
- log.trace('[DEBUG basicFiltersHook] Returning early - no filters');
961
- return;
959
+ log.trace('[DEBUG basicFiltersHook] Returning early - no filters')
960
+ return
962
961
  }
963
962
 
964
963
  // Check if we have any JOINs applied (to know if we need to qualify fields)
965
964
  // Instead of relying on hasJoins flag, always qualify fields for safety
966
- const qualifyField = (field) => adapterUtils.translateColumn(scopeName, field, tableAlias);
965
+ const qualifyField = (field) => adapterUtils.translateColumn(scopeName, field, tableAlias)
967
966
  const normalizeValue = (field, value) => {
968
967
  if (Array.isArray(value)) {
969
- return value.map((entry) => adapterUtils.translateFilterValue(scopeName, field, entry));
968
+ return value.map((entry) => adapterUtils.translateFilterValue(scopeName, field, entry))
970
969
  }
971
- return adapterUtils.translateFilterValue(scopeName, field, value);
972
- };
970
+ return adapterUtils.translateFilterValue(scopeName, field, value)
971
+ }
973
972
 
974
973
  // Main WHERE group
975
- query.where(function() {
974
+ query.where(function () {
976
975
  for (const [filterKey, filterValue] of Object.entries(filters)) {
977
- const fieldDef = schemaInfo.searchSchemaStructure[filterKey];
976
+ const fieldDef = schemaInfo.searchSchemaStructure[filterKey]
978
977
  if (!fieldDef) {
979
- log.trace(`[DEBUG basicFiltersHook] No field definition for filter key: ${filterKey}`);
980
- continue;
978
+ log.trace(`[DEBUG basicFiltersHook] No field definition for filter key: ${filterKey}`)
979
+ continue
981
980
  }
982
- log.trace(`[DEBUG basicFiltersHook] Processing filter: ${filterKey} = ${filterValue}, fieldDef:`, fieldDef);
981
+ log.trace(`[DEBUG basicFiltersHook] Processing filter: ${filterKey} = ${filterValue}, fieldDef:`, fieldDef)
983
982
 
984
983
  // Skip if this is a cross-table filter
985
984
  if (fieldDef.actualField?.includes('.') ||
986
985
  fieldDef.oneOf?.some(f => f.includes('.')) ||
987
986
  fieldDef.polymorphicField) {
988
- log.trace(`[DEBUG basicFiltersHook] Skipping filter ${filterKey} - is cross-table or polymorphic`);
989
- continue;
987
+ log.trace(`[DEBUG basicFiltersHook] Skipping filter ${filterKey} - is cross-table or polymorphic`)
988
+ continue
990
989
  }
991
990
 
992
991
  // Process basic filters
993
992
  switch (true) {
994
993
  case fieldDef.oneOf && Array.isArray(fieldDef.oneOf): {
995
994
  // Multi-field OR search
996
- const operator = fieldDef.filterOperator || '=';
997
-
995
+ const operator = fieldDef.filterOperator || '='
996
+
998
997
  // Handle split search terms
999
- let searchTerms = [filterValue];
998
+ let searchTerms = [filterValue]
1000
999
  if (fieldDef.splitBy && typeof filterValue === 'string') {
1001
- searchTerms = filterValue.split(fieldDef.splitBy).filter(term => term.trim());
1000
+ searchTerms = filterValue.split(fieldDef.splitBy).filter(term => term.trim())
1002
1001
  }
1003
-
1004
- this.where(function() {
1002
+
1003
+ this.where(function () {
1005
1004
  if (fieldDef.matchAll && searchTerms.length > 1) {
1006
1005
  searchTerms.forEach(term => {
1007
- const normalizedTerm = normalizeValue(fieldDef.oneOf[0], term);
1008
- this.andWhere(function() {
1006
+ const normalizedTerm = normalizeValue(fieldDef.oneOf[0], term)
1007
+ this.andWhere(function () {
1009
1008
  fieldDef.oneOf.forEach((field, index) => {
1010
- const columnRef = qualifyField(field);
1009
+ const columnRef = qualifyField(field)
1011
1010
  const perFieldValue = Array.isArray(normalizedTerm)
1012
1011
  ? normalizeValue(field, term)
1013
- : normalizeValue(field, term);
1014
- const normalized = Array.isArray(perFieldValue) ? perFieldValue[0] : perFieldValue;
1012
+ : normalizeValue(field, term)
1013
+ const normalized = Array.isArray(perFieldValue) ? perFieldValue[0] : perFieldValue
1015
1014
 
1016
1015
  if (operator === 'like') {
1017
1016
  if (normalized === null || normalized === undefined) {
1018
- this[index === 0 ? 'whereNull' : 'orWhereNull'](columnRef);
1017
+ this[index === 0 ? 'whereNull' : 'orWhereNull'](columnRef)
1019
1018
  } else {
1020
- this[index === 0 ? 'where' : 'orWhere'](columnRef, 'like', `%${String(normalized)}%`);
1019
+ this[index === 0 ? 'where' : 'orWhere'](columnRef, 'like', `%${String(normalized)}%`)
1021
1020
  }
1022
1021
  } else if (operator === 'in') {
1023
- const values = Array.isArray(perFieldValue) ? perFieldValue : [perFieldValue];
1024
- this[index === 0 ? 'whereIn' : 'orWhereIn'](columnRef, values);
1022
+ const values = Array.isArray(perFieldValue) ? perFieldValue : [perFieldValue]
1023
+ this[index === 0 ? 'whereIn' : 'orWhereIn'](columnRef, values)
1025
1024
  } else {
1026
1025
  if (normalized === null && (operator === '=' || operator === '==')) {
1027
- this[index === 0 ? 'whereNull' : 'orWhereNull'](columnRef);
1026
+ this[index === 0 ? 'whereNull' : 'orWhereNull'](columnRef)
1028
1027
  } else {
1029
- this[index === 0 ? 'where' : 'orWhere'](columnRef, operator, normalized);
1028
+ this[index === 0 ? 'where' : 'orWhere'](columnRef, operator, normalized)
1030
1029
  }
1031
1030
  }
1032
- });
1033
- });
1034
- });
1031
+ })
1032
+ })
1033
+ })
1035
1034
  } else {
1036
1035
  fieldDef.oneOf.forEach((field, index) => {
1037
- const columnRef = qualifyField(field);
1038
- const normalizedValue = normalizeValue(field, filterValue);
1039
- const primary = Array.isArray(normalizedValue) ? normalizedValue[0] : normalizedValue;
1036
+ const columnRef = qualifyField(field)
1037
+ const normalizedValue = normalizeValue(field, filterValue)
1038
+ const primary = Array.isArray(normalizedValue) ? normalizedValue[0] : normalizedValue
1040
1039
 
1041
1040
  if (operator === 'like') {
1042
1041
  if (primary === null || primary === undefined) {
1043
- this[index === 0 ? 'whereNull' : 'orWhereNull'](columnRef);
1042
+ this[index === 0 ? 'whereNull' : 'orWhereNull'](columnRef)
1044
1043
  } else {
1045
- this[index === 0 ? 'where' : 'orWhere'](columnRef, 'like', `%${String(primary)}%`);
1044
+ this[index === 0 ? 'where' : 'orWhere'](columnRef, 'like', `%${String(primary)}%`)
1046
1045
  }
1047
1046
  } else if (operator === 'in') {
1048
- const values = Array.isArray(normalizedValue) ? normalizedValue : [normalizedValue];
1049
- this[index === 0 ? 'whereIn' : 'orWhereIn'](columnRef, values);
1047
+ const values = Array.isArray(normalizedValue) ? normalizedValue : [normalizedValue]
1048
+ this[index === 0 ? 'whereIn' : 'orWhereIn'](columnRef, values)
1050
1049
  } else {
1051
1050
  if (primary === null && (operator === '=' || operator === '==')) {
1052
- this[index === 0 ? 'whereNull' : 'orWhereNull'](columnRef);
1051
+ this[index === 0 ? 'whereNull' : 'orWhereNull'](columnRef)
1053
1052
  } else {
1054
- this[index === 0 ? 'where' : 'orWhere'](columnRef, operator, primary);
1053
+ this[index === 0 ? 'where' : 'orWhere'](columnRef, operator, primary)
1055
1054
  }
1056
1055
  }
1057
- });
1056
+ })
1058
1057
  }
1059
- });
1060
- break;
1058
+ })
1059
+ break
1061
1060
  }
1062
1061
 
1063
1062
  case fieldDef.applyFilter && typeof fieldDef.applyFilter === 'function':
1064
1063
  // Custom filter
1065
- fieldDef.applyFilter.call(this, this, filterValue);
1066
- break;
1064
+ fieldDef.applyFilter.call(this, this, filterValue)
1065
+ break
1067
1066
 
1068
1067
  default:
1069
1068
  // Standard filtering
1070
- const actualField = fieldDef.actualField || filterKey;
1069
+ const actualField = fieldDef.actualField || filterKey
1071
1070
  // Always qualify field names
1072
- const dbField = qualifyField(actualField);
1071
+ const dbField = qualifyField(actualField)
1073
1072
 
1074
- const operator = fieldDef.filterOperator || '=';
1075
- const normalized = normalizeValue(actualField, filterValue);
1076
- log.trace(`[DEBUG basicFiltersHook] Applying filter: ${dbField} ${operator} ${filterValue}`);
1073
+ const operator = fieldDef.filterOperator || '='
1074
+ const normalized = normalizeValue(actualField, filterValue)
1075
+ log.trace(`[DEBUG basicFiltersHook] Applying filter: ${dbField} ${operator} ${filterValue}`)
1077
1076
 
1078
1077
  switch (operator) {
1079
1078
  case 'like': {
1080
- const value = Array.isArray(normalized) ? normalized[0] : normalized;
1079
+ const value = Array.isArray(normalized) ? normalized[0] : normalized
1081
1080
  if (value === null || value === undefined) {
1082
- this.whereNull(dbField);
1081
+ this.whereNull(dbField)
1083
1082
  } else {
1084
- this.where(dbField, 'like', `%${String(value)}%`);
1083
+ this.where(dbField, 'like', `%${String(value)}%`)
1085
1084
  }
1086
- break;
1085
+ break
1087
1086
  }
1088
1087
  case 'in': {
1089
- const values = Array.isArray(normalized) ? normalized : [normalized];
1090
- this.whereIn(dbField, values);
1091
- break;
1088
+ const values = Array.isArray(normalized) ? normalized : [normalized]
1089
+ this.whereIn(dbField, values)
1090
+ break
1092
1091
  }
1093
1092
  case 'between': {
1094
- const values = Array.isArray(normalized) ? normalized : [normalized];
1093
+ const values = Array.isArray(normalized) ? normalized : [normalized]
1095
1094
  if (values.length === 2) {
1096
- this.whereBetween(dbField, values);
1095
+ this.whereBetween(dbField, values)
1097
1096
  } else {
1098
1097
  if (values[0] === null || values[0] === undefined) {
1099
- this.whereNull(dbField);
1098
+ this.whereNull(dbField)
1100
1099
  } else {
1101
- this.where(dbField, operator, values[0]);
1100
+ this.where(dbField, operator, values[0])
1102
1101
  }
1103
1102
  }
1104
- break;
1103
+ break
1105
1104
  }
1106
1105
  default: {
1107
- const value = Array.isArray(normalized) ? normalized[0] : normalized;
1106
+ const value = Array.isArray(normalized) ? normalized[0] : normalized
1108
1107
  if (value === null || value === undefined) {
1109
- this.whereNull(dbField);
1108
+ this.whereNull(dbField)
1110
1109
  } else {
1111
- this.where(dbField, operator, value);
1110
+ this.where(dbField, operator, value)
1112
1111
  }
1113
- break;
1112
+ break
1114
1113
  }
1115
1114
  }
1116
- break;
1115
+ break
1117
1116
  }
1118
1117
  }
1119
- });
1120
- };
1118
+ })
1119
+ }