json-rest-api 1.0.13 → 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.
- package/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +13 -0
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +9 -5
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +8 -8
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- 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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
139
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
+
}
|