json-rest-api 1.0.22 → 1.0.23

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