json-rest-api 1.0.7 → 1.0.8
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/.claude/agents/library-guide-expert.md +29 -0
- package/.claude/agents/rest-api-internals-expert.md +53 -0
- package/CLAUDE.md +139 -1
- package/README.md +2 -5
- package/docTest.js +238 -0
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
- package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
- package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
- package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
- package/docs/GUIDE/index.md +58 -0
- package/docs/GUIDE.md +61 -2238
- package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
- package/docs/README.md +57 -2239
- package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
- package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
- package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
- package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
- package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
- package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
- package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
- package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
- package/docs/assets/css/style.scss +78 -5
- package/docs/index.md +2 -12
- package/helper-reorganization-plan.md +85 -0
- package/helper-usage-analysis.md +94 -0
- package/package.json +5 -4
- package/plugins/core/bulk-operations-plugin.js +2 -2
- package/plugins/core/connectors/express-plugin.js +90 -32
- package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
- package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
- package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
- package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
- package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
- package/plugins/core/lib/querying/knex-window-queries.js +328 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
- package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
- package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
- package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
- package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
- package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
- package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
- package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
- package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
- package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
- package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
- package/plugins/core/lib/writing/relationship-processor.js +252 -0
- package/plugins/core/rest-api-knex-plugin.js +183 -62
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
- package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
- package/plugins/core/rest-api-plugin-methods/common.js +605 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
- package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
- package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
- package/plugins/core/rest-api-plugin-methods/get.js +187 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
- package/plugins/core/rest-api-plugin-methods/post.js +157 -0
- package/plugins/core/rest-api-plugin-methods/put.js +257 -0
- package/plugins/core/rest-api-plugin-methods/query.js +208 -0
- package/plugins/core/rest-api-plugin.js +108 -2498
- package/plugins/core/socketio-plugin.js +28 -5
- package/plugins/storage/local-storage.js +2 -2
- package/tests/computed-fields.test.js +483 -0
- package/tests/cors.test.js +4 -4
- package/tests/curl.test.js +5 -4
- package/tests/custom-idproperty-relationships.test.js +891 -0
- package/tests/field-getters.test.js +353 -0
- package/tests/field-setters.test.js +310 -0
- package/tests/fixtures/api-configs.js +800 -28
- package/tests/jwt-auth.test.js +0 -2
- package/tests/nested-includes.test.js +1 -1
- package/tests/pagination-cursor-multifield.test.js +514 -0
- package/tests/pagination-enhanced.test.js +12 -9
- package/tests/polymorphic-relationships.test.js +10 -8
- package/tests/polymorphic-simple.test.js +6 -4
- package/tests/positioning-advanced.test.js +12 -12
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +24 -24
- package/tests/positioning.test.js +3 -3
- package/tests/query-limits.test.js +24 -8
- package/tests/relationship-endpoints.test.js +4 -5
- package/tests/relationships.test.js +0 -1
- package/tests/socketio.test.js +12 -1
- package/tests/virtual-fields.test.js +13 -13
- package/PLAN_FOR_WEBSOCKETS.md +0 -398
- package/README_1_Initial_Setup.md +0 -393
- package/README_X_Hooks.md +0 -647
- package/TODO_rest-api-plugin-extras.todo +0 -23
- package/connectors_ideas/express-plugin.js +0 -120
- package/connectors_ideas/http-plugin.js +0 -138
- package/docs/examples/express-plugin.js +0 -366
- package/example.js +0 -301
- package/plugins/core/HELPER_MODULES.md +0 -285
- package/plugins/core/lib/compile-schemas.js +0 -245
- package/plugins/core/lib/database-value-normalizers.js +0 -192
- package/plugins/core/lib/default-data-helpers.js +0 -100
- package/plugins/core/lib/knex-cross-table-search.js +0 -442
- package/plugins/core/lib/knex-json-api-helpers.js +0 -84
- package/plugins/core/lib/knex-json-api-transformers.js +0 -449
- package/plugins/core/lib/knex-pagination-helpers.js +0 -193
- package/plugins/core/lib/knex-window-queries.js +0 -142
- package/plugins/core/lib/many-to-many-manipulations.js +0 -343
- package/plugins/core/lib/relationship-processor.js +0 -278
- package/plugins/core/lib/schema-helpers.js +0 -343
- package/plugins/core/lib/scope-validations.js +0 -252
- package/plugins/core/rest-api-relationships-plugin.js +0 -700
- package/plugins/core/utils/knex-query-helpers-base.js +0 -56
- /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
- /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
- /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
- /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
- /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
- /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
- /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
- /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
- /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
- /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
- /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
- /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
|
@@ -1,56 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @description Knex query filter hooks for complex SQL query building
|
|
4
|
-
*
|
|
5
|
-
* This module contains three separate hooks for filtering queries:
|
|
6
|
-
* 1. polymorphicFiltersHook - Handles polymorphic relationship filters
|
|
7
|
-
* 2. crossTableFiltersHook - Handles filters requiring JOINs
|
|
8
|
-
* 3. basicFiltersHook - Handles basic filters on the main table
|
|
9
|
-
*
|
|
10
|
-
* These hooks are designed to run in this specific order to ensure
|
|
11
|
-
* proper field qualification when JOINs are present. They implement
|
|
12
|
-
* sophisticated query building patterns to handle complex filtering
|
|
13
|
-
* scenarios while maintaining SQL injection safety and performance.
|
|
14
|
-
*
|
|
15
|
-
* Why this is useful upstream:
|
|
16
|
-
* - Enables filtering on polymorphic relationships (e.g., find all comments on posts by John)
|
|
17
|
-
* - Supports cross-table filtering without N+1 queries
|
|
18
|
-
* - Automatically adds JOINs based on filter requirements
|
|
19
|
-
* - Handles field qualification to prevent ambiguous column errors
|
|
20
|
-
* - Provides extensible hook system for custom filter logic
|
|
21
|
-
* - Maintains SQL injection safety through parameterized queries
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Hook 1: Polymorphic Filters Hook
|
|
26
|
-
*
|
|
27
|
-
* Processes filters that target polymorphic relationships, where a single
|
|
28
|
-
* relationship can point to different types of resources. This hook adds
|
|
29
|
-
* conditional JOINs for each possible type and applies OR conditions to
|
|
30
|
-
* search across all of them. Must run first to establish JOINs before
|
|
31
|
-
* other hooks process their filters.
|
|
2
|
+
* Processes filters that target polymorphic relationships where a single relationship can point to different types of resources
|
|
32
3
|
*
|
|
33
4
|
* @param {Object} hookParams - Hook parameters containing context
|
|
34
5
|
* @param {Object} dependencies - Dependencies injected by the plugin
|
|
35
6
|
*
|
|
36
|
-
* @example
|
|
37
|
-
* //
|
|
38
|
-
* searchSchema
|
|
7
|
+
* @example
|
|
8
|
+
* // Input: Search schema with polymorphic filter
|
|
9
|
+
* const searchSchema = {
|
|
39
10
|
* commentable_title: {
|
|
40
11
|
* type: 'string',
|
|
41
|
-
* polymorphicField: 'commentable', //
|
|
12
|
+
* polymorphicField: 'commentable', // Points to the polymorphic relationship
|
|
42
13
|
* targetFields: {
|
|
43
14
|
* posts: 'title', // When commentable_type='posts', search posts.title
|
|
44
|
-
* videos: 'title', // When commentable_type='videos', search videos.title
|
|
15
|
+
* videos: 'title', // When commentable_type='videos', search videos.title
|
|
45
16
|
* articles: 'headline' // When commentable_type='articles', search articles.headline
|
|
46
17
|
* },
|
|
47
|
-
*
|
|
18
|
+
* filterOperator: 'like'
|
|
48
19
|
* }
|
|
49
|
-
* }
|
|
20
|
+
* };
|
|
50
21
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* //
|
|
22
|
+
* // Filter request: { commentable_title: 'JavaScript' }
|
|
23
|
+
*
|
|
24
|
+
* // Result: Adds conditional LEFT JOINs and WHERE conditions
|
|
25
|
+
* // SQL generated:
|
|
54
26
|
* // LEFT JOIN posts ON comments.commentable_type = 'posts' AND comments.commentable_id = posts.id
|
|
55
27
|
* // LEFT JOIN videos ON comments.commentable_type = 'videos' AND comments.commentable_id = videos.id
|
|
56
28
|
* // LEFT JOIN articles ON comments.commentable_type = 'articles' AND comments.commentable_id = articles.id
|
|
@@ -60,13 +32,44 @@
|
|
|
60
32
|
* // (comments.commentable_type = 'articles' AND articles.headline LIKE '%JavaScript%')
|
|
61
33
|
* // )
|
|
62
34
|
*
|
|
63
|
-
* @example
|
|
64
|
-
* //
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Input: Complex polymorphic filter with cross-table paths
|
|
37
|
+
* const searchSchema = {
|
|
38
|
+
* commentable_author: {
|
|
39
|
+
* type: 'string',
|
|
40
|
+
* polymorphicField: 'commentable',
|
|
41
|
+
* targetFields: {
|
|
42
|
+
* posts: 'author.name', // Search post author's name
|
|
43
|
+
* videos: 'creator.name' // Search video creator's name
|
|
44
|
+
* }
|
|
45
|
+
* }
|
|
46
|
+
* };
|
|
47
|
+
*
|
|
48
|
+
* // Result: Creates nested JOINs for each polymorphic type
|
|
49
|
+
* // LEFT JOIN posts ON comments.commentable_type = 'posts' AND comments.commentable_id = posts.id
|
|
50
|
+
* // LEFT JOIN users AS posts_author ON posts.author_id = posts_author.id
|
|
51
|
+
* // LEFT JOIN videos ON comments.commentable_type = 'videos' AND comments.commentable_id = videos.id
|
|
52
|
+
* // LEFT JOIN users AS videos_creator ON videos.creator_id = videos_creator.id
|
|
53
|
+
*
|
|
54
|
+
* @description
|
|
55
|
+
* Used by:
|
|
56
|
+
* - rest-api-knex-plugin calls this hook first during query building
|
|
57
|
+
* - Must run before other filter hooks to establish JOINs
|
|
58
|
+
* - Applied when searchSchema contains polymorphicField definitions
|
|
59
|
+
*
|
|
60
|
+
* Purpose:
|
|
61
|
+
* - Enables filtering on polymorphic relationships without knowing the concrete type
|
|
62
|
+
* - Searches across multiple tables with a single filter parameter
|
|
63
|
+
* - Supports queries like "find all comments on content containing 'JavaScript'"
|
|
64
|
+
* - Uses conditional JOINs that only match when type field equals expected value
|
|
65
|
+
* - Maintains performance by leveraging indexed type/id columns
|
|
66
|
+
*
|
|
67
|
+
* Data flow:
|
|
68
|
+
* 1. Identifies filters with polymorphicField in searchSchema
|
|
69
|
+
* 2. For each polymorphic type, adds conditional LEFT JOIN
|
|
70
|
+
* 3. Builds OR conditions checking type field and target field together
|
|
71
|
+
* 4. Sets hasJoins flag for subsequent hooks
|
|
72
|
+
* 5. Returns modified query with polymorphic search capabilities
|
|
70
73
|
*/
|
|
71
74
|
export const polymorphicFiltersHook = async (hookParams, dependencies) => {
|
|
72
75
|
const { log, scopes, knex } = dependencies;
|
|
@@ -263,7 +266,7 @@ export const polymorphicFiltersHook = async (hookParams, dependencies) => {
|
|
|
263
266
|
finalAlias = `${finalAlias}_${pathParts[i]}`;
|
|
264
267
|
}
|
|
265
268
|
|
|
266
|
-
const operator = searchInfo.fieldDef.
|
|
269
|
+
const operator = searchInfo.fieldDef.filterOperator || '=';
|
|
267
270
|
if (operator === 'like') {
|
|
268
271
|
this.where(`${finalAlias}.${fieldName}`, 'like', `%${searchInfo.filterValue}%`);
|
|
269
272
|
} else {
|
|
@@ -271,7 +274,7 @@ export const polymorphicFiltersHook = async (hookParams, dependencies) => {
|
|
|
271
274
|
}
|
|
272
275
|
} else {
|
|
273
276
|
// Direct field
|
|
274
|
-
const operator = searchInfo.fieldDef.
|
|
277
|
+
const operator = searchInfo.fieldDef.filterOperator || '=';
|
|
275
278
|
if (operator === 'like') {
|
|
276
279
|
this.where(`${baseAlias}.${targetFieldPath}`, 'like', `%${searchInfo.filterValue}%`);
|
|
277
280
|
} else {
|
|
@@ -286,66 +289,96 @@ export const polymorphicFiltersHook = async (hookParams, dependencies) => {
|
|
|
286
289
|
};
|
|
287
290
|
|
|
288
291
|
/**
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* Processes filters that require JOINs to access fields in related tables.
|
|
292
|
-
* This hook handles dot-notation field paths (e.g., 'author.name') by building
|
|
293
|
-
* the necessary JOIN chain and applying filters on the joined tables. It also
|
|
294
|
-
* handles DISTINCT when one-to-many JOINs are detected to prevent duplicate results.
|
|
295
|
-
* Must run after polymorphic but before basic to ensure hasJoins flag is set.
|
|
292
|
+
* Processes filters that require JOINs to access fields in related tables using dot notation
|
|
296
293
|
*
|
|
297
294
|
* @param {Object} hookParams - Hook parameters containing context
|
|
298
295
|
* @param {Object} dependencies - Dependencies injected by the plugin
|
|
299
296
|
*
|
|
300
|
-
* @example
|
|
301
|
-
* //
|
|
302
|
-
* searchSchema
|
|
297
|
+
* @example
|
|
298
|
+
* // Input: Simple cross-table filter
|
|
299
|
+
* const searchSchema = {
|
|
303
300
|
* author_name: {
|
|
304
301
|
* type: 'string',
|
|
305
|
-
* actualField: 'author.name', // Dot notation
|
|
306
|
-
*
|
|
302
|
+
* actualField: 'author.name', // Dot notation indicates JOIN needed
|
|
303
|
+
* filterOperator: 'like'
|
|
307
304
|
* }
|
|
308
|
-
* }
|
|
309
|
-
*
|
|
305
|
+
* };
|
|
306
|
+
*
|
|
307
|
+
* // Filter request: { author_name: 'Smith' }
|
|
308
|
+
* // Query before: SELECT * FROM articles
|
|
309
|
+
*
|
|
310
|
+
* // Result: Adds JOIN and qualified WHERE
|
|
311
|
+
* // Query after:
|
|
312
|
+
* // SELECT * FROM articles
|
|
310
313
|
* // LEFT JOIN users AS articles_author ON articles.author_id = articles_author.id
|
|
311
314
|
* // WHERE articles_author.name LIKE '%Smith%'
|
|
312
315
|
*
|
|
313
|
-
* @example
|
|
314
|
-
*
|
|
316
|
+
* @example
|
|
317
|
+
* // Input: Multi-field search across tables
|
|
318
|
+
* const searchSchema = {
|
|
315
319
|
* search: {
|
|
316
320
|
* type: 'string',
|
|
317
|
-
*
|
|
318
|
-
* 'title', //
|
|
319
|
-
* 'content', //
|
|
320
|
-
* 'author.name', //
|
|
321
|
-
* 'category.title' //
|
|
322
|
-
* ]
|
|
321
|
+
* oneOf: [
|
|
322
|
+
* 'title', // Local field
|
|
323
|
+
* 'content', // Local field
|
|
324
|
+
* 'author.name', // Requires JOIN to users
|
|
325
|
+
* 'category.title' // Requires JOIN to categories
|
|
326
|
+
* ],
|
|
327
|
+
* filterOperator: 'like'
|
|
323
328
|
* }
|
|
324
|
-
* }
|
|
325
|
-
*
|
|
326
|
-
* //
|
|
329
|
+
* };
|
|
330
|
+
*
|
|
331
|
+
* // Filter request: { search: 'JavaScript' }
|
|
327
332
|
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
333
|
+
* // Result: Multiple JOINs and OR conditions
|
|
334
|
+
* // SELECT DISTINCT * FROM articles
|
|
335
|
+
* // LEFT JOIN users AS articles_author ON articles.author_id = articles_author.id
|
|
336
|
+
* // LEFT JOIN categories AS articles_category ON articles.category_id = articles_category.id
|
|
337
|
+
* // WHERE (
|
|
338
|
+
* // articles.title LIKE '%JavaScript%' OR
|
|
339
|
+
* // articles.content LIKE '%JavaScript%' OR
|
|
340
|
+
* // articles_author.name LIKE '%JavaScript%' OR
|
|
341
|
+
* // articles_category.title LIKE '%JavaScript%'
|
|
342
|
+
* // )
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* // Input: Deep nested relationships (3 levels)
|
|
346
|
+
* const searchSchema = {
|
|
330
347
|
* company_country: {
|
|
331
348
|
* type: 'string',
|
|
332
|
-
* actualField: 'author.company.country.name'
|
|
333
|
-
* filterUsing: '='
|
|
349
|
+
* actualField: 'author.company.country.name'
|
|
334
350
|
* }
|
|
335
|
-
* }
|
|
336
|
-
*
|
|
351
|
+
* };
|
|
352
|
+
*
|
|
353
|
+
* // Filter request: { company_country: 'USA' }
|
|
354
|
+
*
|
|
355
|
+
* // Result: Chain of JOINs following relationships
|
|
356
|
+
* // SELECT * FROM articles
|
|
337
357
|
* // LEFT JOIN users AS articles_author ON articles.author_id = articles_author.id
|
|
338
358
|
* // LEFT JOIN companies AS articles_author_company ON articles_author.company_id = articles_author_company.id
|
|
339
359
|
* // LEFT JOIN countries AS articles_author_company_country ON articles_author_company.country_id = articles_author_company_country.id
|
|
360
|
+
* // WHERE articles_author_company_country.name = 'USA'
|
|
340
361
|
*
|
|
341
|
-
* @
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
362
|
+
* @description
|
|
363
|
+
* Used by:
|
|
364
|
+
* - rest-api-knex-plugin calls this hook second during query building
|
|
365
|
+
* - Runs after polymorphicFiltersHook but before basicFiltersHook
|
|
366
|
+
* - Applied when filters contain dot notation in actualField or oneOf
|
|
367
|
+
*
|
|
368
|
+
* Purpose:
|
|
369
|
+
* - Enables filtering on related table fields without manual JOIN writing
|
|
370
|
+
* - Automatically builds JOIN chains from dot notation paths
|
|
371
|
+
* - Detects one-to-many relationships and adds DISTINCT to prevent duplicates
|
|
372
|
+
* - Creates unique aliases to avoid naming conflicts
|
|
373
|
+
* - Validates that target fields are indexed for performance
|
|
374
|
+
*
|
|
375
|
+
* Data flow:
|
|
376
|
+
* 1. Scans filters for dot notation in actualField or oneOf arrays
|
|
377
|
+
* 2. For each cross-table reference, builds JOIN chain via buildJoinChain
|
|
378
|
+
* 3. Applies JOINs to query with proper aliasing
|
|
379
|
+
* 4. Adds DISTINCT if any one-to-many JOINs detected
|
|
380
|
+
* 5. Applies WHERE conditions using qualified field names
|
|
381
|
+
* 6. Sets hasJoins flag for basicFiltersHook to use
|
|
349
382
|
*/
|
|
350
383
|
export const crossTableFiltersHook = async (hookParams, dependencies) => {
|
|
351
384
|
const { log, scopes, knex, crossTableSearchHelpers } = dependencies;
|
|
@@ -390,12 +423,12 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
|
|
|
390
423
|
fieldPathMap.set(fieldDef.actualField, `${joinInfo.joinAlias}.${joinInfo.targetField}`);
|
|
391
424
|
}
|
|
392
425
|
|
|
393
|
-
// Check
|
|
394
|
-
if (fieldDef.
|
|
395
|
-
for (const field of fieldDef.
|
|
426
|
+
// Check oneOf for cross-table references
|
|
427
|
+
if (fieldDef.oneOf && Array.isArray(fieldDef.oneOf)) {
|
|
428
|
+
for (const field of fieldDef.oneOf) {
|
|
396
429
|
if (field.includes('.')) {
|
|
397
430
|
hasCrossTableFilters = true;
|
|
398
|
-
log.trace('[JOIN-DETECTION] Cross-table
|
|
431
|
+
log.trace('[JOIN-DETECTION] Cross-table oneOf field found', { filterKey, field, scopeName });
|
|
399
432
|
const joinInfo = await crossTableSearchHelpers.buildJoinChain(scopeName, field);
|
|
400
433
|
if (!joinMap.has(joinInfo.joinAlias)) {
|
|
401
434
|
joinMap.set(joinInfo.joinAlias, joinInfo);
|
|
@@ -418,20 +451,66 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
|
|
|
418
451
|
joinInfo.joinChain.forEach((join) => {
|
|
419
452
|
const joinKey = `${join.joinAlias}:${join.joinCondition}`;
|
|
420
453
|
if (!appliedJoins.has(joinKey)) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
454
|
+
// Check if this is a polymorphic join with AND condition
|
|
455
|
+
if (join.isPolymorphic && join.joinCondition.includes(' AND ')) {
|
|
456
|
+
query.leftJoin(`${join.targetTableName} as ${join.joinAlias}`, function() {
|
|
457
|
+
// Parse polymorphic condition: type_field = 'value' AND id_field = parent.id
|
|
458
|
+
const parts = join.joinCondition.split(' AND ');
|
|
459
|
+
const [typeCondition, idCondition] = parts;
|
|
460
|
+
|
|
461
|
+
// Extract the type field and value from first part
|
|
462
|
+
const typeMatch = typeCondition.match(/(.+?)\s*=\s*'(.+?)'/);
|
|
463
|
+
if (typeMatch) {
|
|
464
|
+
const typeField = typeMatch[1].trim();
|
|
465
|
+
const typeValue = typeMatch[2];
|
|
466
|
+
this.on(typeField, db.raw('?', [typeValue]));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Extract the id fields from second part
|
|
470
|
+
const idMatch = idCondition.match(/(.+?)\s*=\s*(.+)/);
|
|
471
|
+
if (idMatch) {
|
|
472
|
+
this.andOn(idMatch[1].trim(), idMatch[2].trim());
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
} else {
|
|
476
|
+
const [leftSide, rightSide] = join.joinCondition.split(' = ');
|
|
477
|
+
query.leftJoin(`${join.targetTableName} as ${join.joinAlias}`, function() {
|
|
478
|
+
this.on(leftSide, rightSide);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
425
481
|
appliedJoins.add(joinKey);
|
|
426
482
|
}
|
|
427
483
|
});
|
|
428
484
|
} else {
|
|
429
485
|
const joinKey = `${joinInfo.joinAlias}:${joinInfo.joinCondition}`;
|
|
430
486
|
if (!appliedJoins.has(joinKey)) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
487
|
+
// Check if this is a polymorphic join with AND condition
|
|
488
|
+
if (joinInfo.isPolymorphic && joinInfo.joinCondition.includes(' AND ')) {
|
|
489
|
+
query.leftJoin(`${joinInfo.targetTableName} as ${joinInfo.joinAlias}`, function() {
|
|
490
|
+
// Parse polymorphic condition: type_field = 'value' AND id_field = parent.id
|
|
491
|
+
const parts = joinInfo.joinCondition.split(' AND ');
|
|
492
|
+
const [typeCondition, idCondition] = parts;
|
|
493
|
+
|
|
494
|
+
// Extract the type field and value from first part
|
|
495
|
+
const typeMatch = typeCondition.match(/(.+?)\s*=\s*'(.+?)'/);
|
|
496
|
+
if (typeMatch) {
|
|
497
|
+
const typeField = typeMatch[1].trim();
|
|
498
|
+
const typeValue = typeMatch[2];
|
|
499
|
+
this.on(typeField, db.raw('?', [typeValue]));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Extract the id fields from second part
|
|
503
|
+
const idMatch = idCondition.match(/(.+?)\s*=\s*(.+)/);
|
|
504
|
+
if (idMatch) {
|
|
505
|
+
this.andOn(idMatch[1].trim(), idMatch[2].trim());
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
} else {
|
|
509
|
+
const [leftSide, rightSide] = joinInfo.joinCondition.split(' = ');
|
|
510
|
+
query.leftJoin(`${joinInfo.targetTableName} as ${joinInfo.joinAlias}`, function() {
|
|
511
|
+
this.on(leftSide, rightSide);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
435
514
|
appliedJoins.add(joinKey);
|
|
436
515
|
}
|
|
437
516
|
}
|
|
@@ -466,27 +545,72 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
|
|
|
466
545
|
// Skip non-cross-table and polymorphic filters
|
|
467
546
|
if (fieldDef.polymorphicField) continue;
|
|
468
547
|
if (!fieldDef.actualField?.includes('.') &&
|
|
469
|
-
!fieldDef.
|
|
548
|
+
!fieldDef.oneOf?.some(f => f.includes('.'))) {
|
|
470
549
|
continue;
|
|
471
550
|
}
|
|
472
551
|
|
|
473
552
|
// Process cross-table filters
|
|
474
553
|
switch (true) {
|
|
475
|
-
case fieldDef.
|
|
554
|
+
case fieldDef.oneOf && Array.isArray(fieldDef.oneOf): {
|
|
555
|
+
const operator = fieldDef.filterOperator || '=';
|
|
556
|
+
|
|
557
|
+
// Handle split search terms
|
|
558
|
+
let searchTerms = [filterValue];
|
|
559
|
+
if (fieldDef.splitBy && typeof filterValue === 'string') {
|
|
560
|
+
searchTerms = filterValue.split(fieldDef.splitBy).filter(term => term.trim());
|
|
561
|
+
}
|
|
562
|
+
|
|
476
563
|
this.where(function() {
|
|
477
|
-
fieldDef.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
564
|
+
if (fieldDef.matchAll && searchTerms.length > 1) {
|
|
565
|
+
// AND logic - all terms must match
|
|
566
|
+
searchTerms.forEach(term => {
|
|
567
|
+
this.andWhere(function() {
|
|
568
|
+
fieldDef.oneOf.forEach((field, index) => {
|
|
569
|
+
const dbField = fieldPathMap.get(field) ||
|
|
570
|
+
(!field.includes('.') && joinMap.size > 0 ? `${tableName}.${field}` : field);
|
|
571
|
+
|
|
572
|
+
if (operator === 'like') {
|
|
573
|
+
const condition = `%${term}%`;
|
|
574
|
+
if (index === 0) {
|
|
575
|
+
this.where(dbField, 'like', condition);
|
|
576
|
+
} else {
|
|
577
|
+
this.orWhere(dbField, 'like', condition);
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
if (index === 0) {
|
|
581
|
+
this.where(dbField, operator, term);
|
|
582
|
+
} else {
|
|
583
|
+
this.orWhere(dbField, operator, term);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
} else {
|
|
590
|
+
// OR logic for single term or matchAll=false
|
|
591
|
+
fieldDef.oneOf.forEach((field, index) => {
|
|
592
|
+
const dbField = fieldPathMap.get(field) ||
|
|
593
|
+
(!field.includes('.') && joinMap.size > 0 ? `${tableName}.${field}` : field);
|
|
594
|
+
|
|
595
|
+
if (operator === 'like') {
|
|
596
|
+
const condition = `%${filterValue}%`;
|
|
597
|
+
if (index === 0) {
|
|
598
|
+
this.where(dbField, 'like', condition);
|
|
599
|
+
} else {
|
|
600
|
+
this.orWhere(dbField, 'like', condition);
|
|
601
|
+
}
|
|
602
|
+
} else {
|
|
603
|
+
if (index === 0) {
|
|
604
|
+
this.where(dbField, operator, filterValue);
|
|
605
|
+
} else {
|
|
606
|
+
this.orWhere(dbField, operator, filterValue);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
}
|
|
488
611
|
});
|
|
489
612
|
break;
|
|
613
|
+
}
|
|
490
614
|
|
|
491
615
|
case fieldDef.applyFilter && typeof fieldDef.applyFilter === 'function':
|
|
492
616
|
fieldDef.applyFilter.call(this, this, filterValue);
|
|
@@ -498,7 +622,7 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
|
|
|
498
622
|
dbField = fieldPathMap.get(dbField) || dbField;
|
|
499
623
|
}
|
|
500
624
|
|
|
501
|
-
const operator = fieldDef.
|
|
625
|
+
const operator = fieldDef.filterOperator || '=';
|
|
502
626
|
|
|
503
627
|
switch (operator) {
|
|
504
628
|
case 'like':
|
|
@@ -529,94 +653,145 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
|
|
|
529
653
|
};
|
|
530
654
|
|
|
531
655
|
/**
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
* Processes filters that apply directly to fields on the main table.
|
|
535
|
-
* This hook handles standard filtering operations like equality, LIKE,
|
|
536
|
-
* IN, BETWEEN, etc. It automatically qualifies field names with the table
|
|
537
|
-
* name when JOINs are present to prevent ambiguous column errors.
|
|
538
|
-
* Must run last to ensure all JOINs have been established.
|
|
656
|
+
* Processes filters that apply directly to fields on the main table
|
|
539
657
|
*
|
|
540
658
|
* @param {Object} hookParams - Hook parameters containing context
|
|
541
659
|
* @param {Object} dependencies - Dependencies injected by the plugin
|
|
542
660
|
*
|
|
543
|
-
* @example
|
|
544
|
-
*
|
|
661
|
+
* @example
|
|
662
|
+
* // Input: Basic equality filter
|
|
663
|
+
* const searchSchema = {
|
|
545
664
|
* status: {
|
|
546
665
|
* type: 'string',
|
|
547
|
-
*
|
|
666
|
+
* filterOperator: '=' // Default is '=' if not specified
|
|
548
667
|
* }
|
|
549
|
-
* }
|
|
550
|
-
*
|
|
551
|
-
* //
|
|
668
|
+
* };
|
|
669
|
+
*
|
|
670
|
+
* // Filter request: { status: 'published' }
|
|
671
|
+
* // Query before: SELECT * FROM articles
|
|
552
672
|
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
673
|
+
* // Result: Adds qualified WHERE clause
|
|
674
|
+
* // Query after: SELECT * FROM articles WHERE articles.status = 'published'
|
|
675
|
+
*
|
|
676
|
+
* @example
|
|
677
|
+
* // Input: LIKE filter for partial text matching
|
|
678
|
+
* const searchSchema = {
|
|
555
679
|
* title: {
|
|
556
680
|
* type: 'string',
|
|
557
|
-
*
|
|
681
|
+
* filterOperator: 'like'
|
|
682
|
+
* },
|
|
683
|
+
* content: {
|
|
684
|
+
* type: 'string',
|
|
685
|
+
* filterOperator: 'like'
|
|
558
686
|
* }
|
|
559
|
-
* }
|
|
560
|
-
* // For filter: { title: 'JavaScript' }
|
|
561
|
-
* // Generates: WHERE articles.title LIKE '%JavaScript%'
|
|
687
|
+
* };
|
|
562
688
|
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
689
|
+
* // Filter request: { title: 'JavaScript', content: 'async' }
|
|
690
|
+
*
|
|
691
|
+
* // Result: Multiple LIKE conditions
|
|
692
|
+
* // WHERE articles.title LIKE '%JavaScript%'
|
|
693
|
+
* // AND articles.content LIKE '%async%'
|
|
694
|
+
*
|
|
695
|
+
* @example
|
|
696
|
+
* // Input: Multi-field OR search with oneOf
|
|
697
|
+
* const searchSchema = {
|
|
565
698
|
* search: {
|
|
566
699
|
* type: 'string',
|
|
567
|
-
*
|
|
700
|
+
* oneOf: ['title', 'content', 'summary'],
|
|
701
|
+
* filterOperator: 'like',
|
|
702
|
+
* splitBy: ' ', // Split search terms by space
|
|
703
|
+
* matchAll: true // All terms must match somewhere
|
|
568
704
|
* }
|
|
569
|
-
* }
|
|
570
|
-
*
|
|
571
|
-
* //
|
|
572
|
-
*
|
|
573
|
-
* //
|
|
574
|
-
* //
|
|
705
|
+
* };
|
|
706
|
+
*
|
|
707
|
+
* // Filter request: { search: 'REST API' }
|
|
708
|
+
*
|
|
709
|
+
* // Result: Each term must match in at least one field
|
|
710
|
+
* // WHERE (
|
|
711
|
+
* // (articles.title LIKE '%REST%' OR articles.content LIKE '%REST%' OR articles.summary LIKE '%REST%')
|
|
712
|
+
* // AND
|
|
713
|
+
* // (articles.title LIKE '%API%' OR articles.content LIKE '%API%' OR articles.summary LIKE '%API%')
|
|
575
714
|
* // )
|
|
576
715
|
*
|
|
577
|
-
* @example
|
|
578
|
-
*
|
|
716
|
+
* @example
|
|
717
|
+
* // Input: Advanced operators - IN and BETWEEN
|
|
718
|
+
* const searchSchema = {
|
|
579
719
|
* category_id: {
|
|
580
720
|
* type: 'array',
|
|
581
|
-
*
|
|
721
|
+
* filterOperator: 'in'
|
|
722
|
+
* },
|
|
723
|
+
* price: {
|
|
724
|
+
* type: 'array',
|
|
725
|
+
* filterOperator: 'between'
|
|
582
726
|
* },
|
|
583
|
-
*
|
|
727
|
+
* tags: {
|
|
584
728
|
* type: 'array',
|
|
585
|
-
*
|
|
729
|
+
* filterOperator: 'in'
|
|
586
730
|
* }
|
|
587
|
-
* }
|
|
588
|
-
*
|
|
731
|
+
* };
|
|
732
|
+
*
|
|
733
|
+
* // Filter request: {
|
|
589
734
|
* // category_id: [1, 2, 3],
|
|
590
|
-
* //
|
|
735
|
+
* // price: [10.00, 99.99],
|
|
736
|
+
* // tags: ['javascript', 'nodejs']
|
|
591
737
|
* // }
|
|
592
|
-
*
|
|
738
|
+
*
|
|
739
|
+
* // Result: IN and BETWEEN clauses
|
|
593
740
|
* // WHERE articles.category_id IN (1, 2, 3)
|
|
594
|
-
* // AND articles.
|
|
741
|
+
* // AND articles.price BETWEEN 10.00 AND 99.99
|
|
742
|
+
* // AND articles.tags IN ('javascript', 'nodejs')
|
|
595
743
|
*
|
|
596
|
-
* @example
|
|
597
|
-
*
|
|
744
|
+
* @example
|
|
745
|
+
* // Input: Custom filter function
|
|
746
|
+
* const searchSchema = {
|
|
598
747
|
* has_comments: {
|
|
599
748
|
* type: 'boolean',
|
|
600
749
|
* applyFilter: function(query, value) {
|
|
601
|
-
* if (value) {
|
|
750
|
+
* if (value === true) {
|
|
602
751
|
* query.whereExists(function() {
|
|
603
752
|
* this.select('id')
|
|
604
753
|
* .from('comments')
|
|
605
754
|
* .whereRaw('comments.article_id = articles.id');
|
|
606
755
|
* });
|
|
756
|
+
* } else if (value === false) {
|
|
757
|
+
* query.whereNotExists(function() {
|
|
758
|
+
* this.select('id')
|
|
759
|
+
* .from('comments')
|
|
760
|
+
* .whereRaw('comments.article_id = articles.id');
|
|
761
|
+
* });
|
|
607
762
|
* }
|
|
608
763
|
* }
|
|
609
764
|
* }
|
|
610
|
-
* }
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
* //
|
|
615
|
-
* //
|
|
616
|
-
* //
|
|
617
|
-
* //
|
|
618
|
-
*
|
|
619
|
-
*
|
|
765
|
+
* };
|
|
766
|
+
*
|
|
767
|
+
* // Filter request: { has_comments: true }
|
|
768
|
+
*
|
|
769
|
+
* // Result: Subquery to check existence
|
|
770
|
+
* // WHERE EXISTS (
|
|
771
|
+
* // SELECT id FROM comments WHERE comments.article_id = articles.id
|
|
772
|
+
* // )
|
|
773
|
+
*
|
|
774
|
+
* @description
|
|
775
|
+
* Used by:
|
|
776
|
+
* - rest-api-knex-plugin calls this hook last during query building
|
|
777
|
+
* - Runs after all JOINs have been established by previous hooks
|
|
778
|
+
* - Handles all non-cross-table, non-polymorphic filters
|
|
779
|
+
*
|
|
780
|
+
* Purpose:
|
|
781
|
+
* - Implements standard SQL filtering operations safely via Knex
|
|
782
|
+
* - Always qualifies field names with table name to prevent ambiguity
|
|
783
|
+
* - Supports various operators: =, like, in, between, and custom
|
|
784
|
+
* - Enables multi-field OR searches with optional term splitting
|
|
785
|
+
* - Handles null values appropriately (using whereNull)
|
|
786
|
+
* - Allows custom filter logic via applyFilter functions
|
|
787
|
+
*
|
|
788
|
+
* Data flow:
|
|
789
|
+
* 1. Skips filters already handled by polymorphic/cross-table hooks
|
|
790
|
+
* 2. Qualifies all field names with table name (e.g., articles.title)
|
|
791
|
+
* 3. Applies appropriate WHERE clause based on filterOperator
|
|
792
|
+
* 4. For oneOf filters, creates OR conditions across specified fields
|
|
793
|
+
* 5. For custom filters, delegates to applyFilter function
|
|
794
|
+
* 6. Returns query with all basic filters applied
|
|
620
795
|
*/
|
|
621
796
|
export const basicFiltersHook = async (hookParams, dependencies) => {
|
|
622
797
|
const { log, scopes, knex } = dependencies;
|
|
@@ -629,7 +804,17 @@ export const basicFiltersHook = async (hookParams, dependencies) => {
|
|
|
629
804
|
const tableName = hookParams.context?.knexQuery?.tableName;
|
|
630
805
|
const db = hookParams.context?.knexQuery?.db || knex;
|
|
631
806
|
|
|
807
|
+
log.trace('[DEBUG basicFiltersHook] Called with:', {
|
|
808
|
+
scopeName,
|
|
809
|
+
hasFilters: !!filters,
|
|
810
|
+
filters,
|
|
811
|
+
hasSearchSchema: !!searchSchema,
|
|
812
|
+
searchSchemaKeys: searchSchema ? Object.keys(searchSchema.structure || {}) : [],
|
|
813
|
+
tableName
|
|
814
|
+
});
|
|
815
|
+
|
|
632
816
|
if (!filters || !searchSchema) {
|
|
817
|
+
log.trace('[DEBUG basicFiltersHook] Returning early - no filters or searchSchema');
|
|
633
818
|
return;
|
|
634
819
|
}
|
|
635
820
|
|
|
@@ -643,33 +828,83 @@ export const basicFiltersHook = async (hookParams, dependencies) => {
|
|
|
643
828
|
query.where(function() {
|
|
644
829
|
for (const [filterKey, filterValue] of Object.entries(filters)) {
|
|
645
830
|
const fieldDef = searchSchema.structure[filterKey];
|
|
646
|
-
if (!fieldDef)
|
|
831
|
+
if (!fieldDef) {
|
|
832
|
+
log.trace(`[DEBUG basicFiltersHook] No field definition for filter key: ${filterKey}`);
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
log.trace(`[DEBUG basicFiltersHook] Processing filter: ${filterKey} = ${filterValue}, fieldDef:`, fieldDef);
|
|
647
836
|
|
|
648
837
|
// Skip if this is a cross-table filter
|
|
649
838
|
if (fieldDef.actualField?.includes('.') ||
|
|
650
|
-
fieldDef.
|
|
839
|
+
fieldDef.oneOf?.some(f => f.includes('.')) ||
|
|
651
840
|
fieldDef.polymorphicField) {
|
|
841
|
+
log.trace(`[DEBUG basicFiltersHook] Skipping filter ${filterKey} - is cross-table or polymorphic`);
|
|
652
842
|
continue;
|
|
653
843
|
}
|
|
654
844
|
|
|
655
845
|
// Process basic filters
|
|
656
846
|
switch (true) {
|
|
657
|
-
case fieldDef.
|
|
847
|
+
case fieldDef.oneOf && Array.isArray(fieldDef.oneOf): {
|
|
658
848
|
// Multi-field OR search
|
|
849
|
+
const operator = fieldDef.filterOperator || '=';
|
|
850
|
+
|
|
851
|
+
// Handle split search terms
|
|
852
|
+
let searchTerms = [filterValue];
|
|
853
|
+
if (fieldDef.splitBy && typeof filterValue === 'string') {
|
|
854
|
+
searchTerms = filterValue.split(fieldDef.splitBy).filter(term => term.trim());
|
|
855
|
+
}
|
|
856
|
+
|
|
659
857
|
this.where(function() {
|
|
660
|
-
fieldDef.
|
|
661
|
-
//
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
858
|
+
if (fieldDef.matchAll && searchTerms.length > 1) {
|
|
859
|
+
// AND logic - all terms must match
|
|
860
|
+
searchTerms.forEach(term => {
|
|
861
|
+
this.andWhere(function() {
|
|
862
|
+
fieldDef.oneOf.forEach((field, index) => {
|
|
863
|
+
// Always qualify field names
|
|
864
|
+
const dbField = qualifyField(field);
|
|
865
|
+
|
|
866
|
+
if (operator === 'like') {
|
|
867
|
+
const condition = `%${term}%`;
|
|
868
|
+
if (index === 0) {
|
|
869
|
+
this.where(dbField, 'like', condition);
|
|
870
|
+
} else {
|
|
871
|
+
this.orWhere(dbField, 'like', condition);
|
|
872
|
+
}
|
|
873
|
+
} else {
|
|
874
|
+
if (index === 0) {
|
|
875
|
+
this.where(dbField, operator, term);
|
|
876
|
+
} else {
|
|
877
|
+
this.orWhere(dbField, operator, term);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
} else {
|
|
884
|
+
// OR logic for single term or matchAll=false
|
|
885
|
+
fieldDef.oneOf.forEach((field, index) => {
|
|
886
|
+
// Always qualify field names
|
|
887
|
+
const dbField = qualifyField(field);
|
|
888
|
+
|
|
889
|
+
if (operator === 'like') {
|
|
890
|
+
const condition = `%${filterValue}%`;
|
|
891
|
+
if (index === 0) {
|
|
892
|
+
this.where(dbField, 'like', condition);
|
|
893
|
+
} else {
|
|
894
|
+
this.orWhere(dbField, 'like', condition);
|
|
895
|
+
}
|
|
896
|
+
} else {
|
|
897
|
+
if (index === 0) {
|
|
898
|
+
this.where(dbField, operator, filterValue);
|
|
899
|
+
} else {
|
|
900
|
+
this.orWhere(dbField, operator, filterValue);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
}
|
|
671
905
|
});
|
|
672
906
|
break;
|
|
907
|
+
}
|
|
673
908
|
|
|
674
909
|
case fieldDef.applyFilter && typeof fieldDef.applyFilter === 'function':
|
|
675
910
|
// Custom filter
|
|
@@ -682,7 +917,8 @@ export const basicFiltersHook = async (hookParams, dependencies) => {
|
|
|
682
917
|
// Always qualify field names
|
|
683
918
|
const dbField = qualifyField(actualField);
|
|
684
919
|
|
|
685
|
-
const operator = fieldDef.
|
|
920
|
+
const operator = fieldDef.filterOperator || '=';
|
|
921
|
+
log.trace(`[DEBUG basicFiltersHook] Applying filter: ${dbField} ${operator} ${filterValue}`);
|
|
686
922
|
|
|
687
923
|
switch (operator) {
|
|
688
924
|
case 'like':
|