json-rest-api 1.0.29 → 2.0.0
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/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,5 +1,118 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertScalarQueryField } from '../querying-writing/field-utils.js'
|
|
2
|
+
import { buildJoinChain } from './knex-cross-table-search.js'
|
|
2
3
|
import { createStorageAdapterUtilities } from './storage-adapter-utils.js'
|
|
4
|
+
import { unwrapQueryBuilderState } from './query-builder-utils.js'
|
|
5
|
+
import { RestApiResourceError, RestApiValidationError } from '../../../../lib/rest-api-errors.js'
|
|
6
|
+
import { resolveSortField } from './query-field-sort-helpers.js'
|
|
7
|
+
import { databaseIdentityExpression } from '../querying-writing/database-value-normalizers.js'
|
|
8
|
+
|
|
9
|
+
const referenceVisibility = Symbol('referenceVisibility')
|
|
10
|
+
|
|
11
|
+
async function buildSearchJoinQuery ({ scopeName, scopes, context, adapterUtils, db }) {
|
|
12
|
+
const scope = scopes[scopeName]
|
|
13
|
+
const storageAdapter = adapterUtils.fetchStorageAdapter(scopeName)
|
|
14
|
+
if (!scope || !storageAdapter) throw new RestApiResourceError(`Related resource '${scopeName}' not found`, { subtype: 'related_type_not_found' })
|
|
15
|
+
const tableName = storageAdapter.getTableName()
|
|
16
|
+
const query = storageAdapter.buildBaseQuery({ transaction: db }).select(`${tableName}.*`)
|
|
17
|
+
const state = await scope.applyQueryFilters({
|
|
18
|
+
query,
|
|
19
|
+
scopeName,
|
|
20
|
+
tableName,
|
|
21
|
+
db,
|
|
22
|
+
storageAdapter,
|
|
23
|
+
schemaInfo: scope.vars.schemaInfo,
|
|
24
|
+
queryPurpose: 'search-join',
|
|
25
|
+
isAnyApi: storageAdapter.isCanonical()
|
|
26
|
+
}, { ...context, queryParams: {}, scopeName, schemaInfo: scope.vars.schemaInfo, storageAdapter })
|
|
27
|
+
return { query: unwrapQueryBuilderState(state, query) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function resolveSearchColumns (references, { scopes, context, adapterUtils, db, operation = 'filter' }) {
|
|
31
|
+
const columns = new Map()
|
|
32
|
+
for (const [key, { scopeName, field, alias }] of references) {
|
|
33
|
+
const column = adapterUtils.translateColumn(scopeName, field, alias)
|
|
34
|
+
const schemaInfo = scopes[scopeName]?.vars?.schemaInfo
|
|
35
|
+
const fieldDef = schemaInfo?.schemaStructure?.[field]
|
|
36
|
+
assertScalarQueryField(fieldDef, field, operation)
|
|
37
|
+
let idField, typeField, targets
|
|
38
|
+
if (fieldDef?.belongsTo) {
|
|
39
|
+
idField = field
|
|
40
|
+
targets = [fieldDef.belongsTo]
|
|
41
|
+
} else {
|
|
42
|
+
for (const [name, definition] of Object.entries({ ...schemaInfo?.schemaStructure, ...schemaInfo?.schemaRelationships })) {
|
|
43
|
+
const polymorphic = definition?.belongsToPolymorphic
|
|
44
|
+
if (!polymorphic) continue
|
|
45
|
+
const referenceId = polymorphic.idField || `${name}_id`
|
|
46
|
+
const referenceType = polymorphic.typeField || `${name}_type`
|
|
47
|
+
if (field !== referenceId && field !== referenceType) continue
|
|
48
|
+
idField = referenceId
|
|
49
|
+
typeField = referenceType
|
|
50
|
+
targets = polymorphic.types
|
|
51
|
+
break
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!targets) {
|
|
55
|
+
columns.set(key, column)
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const selections = []
|
|
60
|
+
const targetAlias = alias === '__jra_reference' ? '__jra_reference_target' : '__jra_reference'
|
|
61
|
+
for (const target of targets) {
|
|
62
|
+
const state = await buildSearchJoinQuery({ scopeName: target, scopes, context, adapterUtils, db })
|
|
63
|
+
const selection = db.queryBuilder().from(state.query.as(targetAlias)).select(db.raw('1'))
|
|
64
|
+
.whereColumn(adapterUtils.translateColumn(target, 'id', targetAlias), adapterUtils.translateColumn(scopeName, idField, alias))
|
|
65
|
+
if (typeField) selection.where(adapterUtils.translateColumn(scopeName, typeField, alias), adapterUtils.translateFilterValue(scopeName, typeField, target))
|
|
66
|
+
selections.push(selection)
|
|
67
|
+
}
|
|
68
|
+
columns.set(key, { column, [referenceVisibility]: selections })
|
|
69
|
+
}
|
|
70
|
+
return columns
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function prepareReferenceSortColumns ({ query, fields, scopeName, tableAlias, context }, dependencies) {
|
|
74
|
+
const { scopes, knex, getStorageAdapter } = dependencies
|
|
75
|
+
const storageAdapter = getStorageAdapter(scopeName)
|
|
76
|
+
const tableName = storageAdapter.getTableName()
|
|
77
|
+
const db = context.db || context.transaction || knex
|
|
78
|
+
const schemaInfo = scopes[scopeName].vars.schemaInfo
|
|
79
|
+
const adapterUtils = createStorageAdapterUtilities({
|
|
80
|
+
context: {
|
|
81
|
+
...context, knexQuery: { scopeName, tableName, storageAdapter }
|
|
82
|
+
}
|
|
83
|
+
}, dependencies)
|
|
84
|
+
const references = new Map(fields.map(field => [field, {
|
|
85
|
+
scopeName, alias: tableName, field: resolveSortField(field, schemaInfo)
|
|
86
|
+
}]))
|
|
87
|
+
const resolved = await resolveSearchColumns(references, { scopes, context, adapterUtils, db, operation: 'sort' })
|
|
88
|
+
const columns = new Map()
|
|
89
|
+
if (![...resolved.values()].some(reference => reference?.[referenceVisibility])) return columns
|
|
90
|
+
const resultNames = new Set([
|
|
91
|
+
...fields,
|
|
92
|
+
...Object.keys(scopes[scopeName].vars.schemaInfo?.queryFields || {}),
|
|
93
|
+
...Object.keys(schemaInfo.schemaStructure).flatMap(field => [field, storageAdapter.translateColumn(field)])
|
|
94
|
+
])
|
|
95
|
+
let resultIndex = 0
|
|
96
|
+
for (const [field, reference] of resolved) {
|
|
97
|
+
const selections = reference?.[referenceVisibility]
|
|
98
|
+
if (!selections) continue
|
|
99
|
+
const joinAlias = `__jra_sort_${columns.size}${tableAlias === `__jra_sort_${columns.size}` ? '_target' : ''}`
|
|
100
|
+
while (resultNames.has(`__jra_sort_value_${resultIndex}`)) resultIndex++
|
|
101
|
+
const resultColumn = `__jra_sort_value_${resultIndex++}`
|
|
102
|
+
// A filtered copy preserves the source column's affinity and collation.
|
|
103
|
+
const visibleSource = storageAdapter.buildBaseQuery({ transaction: db })
|
|
104
|
+
.select({ __jra_id: `${tableName}.${storageAdapter.getIdColumn()}`, __jra_value: reference.column })
|
|
105
|
+
.where(function () {
|
|
106
|
+
if (selections.length === 0) this.whereRaw('1 = 0')
|
|
107
|
+
for (const selection of selections) this.orWhereExists(selection)
|
|
108
|
+
})
|
|
109
|
+
const column = `${joinAlias}.__jra_value`
|
|
110
|
+
query.leftJoin(visibleSource.as(joinAlias), `${tableAlias}.${storageAdapter.getIdColumn()}`, `${joinAlias}.__jra_id`)
|
|
111
|
+
query.select({ [resultColumn]: databaseIdentityExpression(db, column) })
|
|
112
|
+
columns.set(field, { column, resultColumn, referenceField: references.get(field).field })
|
|
113
|
+
}
|
|
114
|
+
return columns
|
|
115
|
+
}
|
|
3
116
|
|
|
4
117
|
// Resolve operator with sensible defaults for fields declared in searchSchema.
|
|
5
118
|
// - If filterOperator is provided, use it as-is
|
|
@@ -10,22 +123,41 @@ export function resolveSearchOperator (fieldDef) {
|
|
|
10
123
|
}
|
|
11
124
|
|
|
12
125
|
// Apply a comparison for a single field/operator/value onto a query builder.
|
|
13
|
-
// Handles
|
|
14
|
-
// and uses ILIKE for case-insensitive matching on Postgres.
|
|
126
|
+
// Handles text matching, IN, BETWEEN, and explicit null comparisons.
|
|
15
127
|
export function applyWhereForOperator ({ builder, columnRef, operator, value, knex, or = false }) {
|
|
16
128
|
const method = or ? 'orWhere' : 'where'
|
|
17
129
|
const methodNull = or ? 'orWhereNull' : 'whereNull'
|
|
18
130
|
const methodIn = or ? 'orWhereIn' : 'whereIn'
|
|
131
|
+
|
|
132
|
+
const selections = columnRef?.[referenceVisibility]
|
|
133
|
+
if (selections) {
|
|
134
|
+
const visible = function () {
|
|
135
|
+
if (selections.length === 0) this.whereRaw('1 = 0')
|
|
136
|
+
for (const selection of selections) this.orWhereExists(selection)
|
|
137
|
+
}
|
|
138
|
+
// Compare the column itself to preserve driver affinity and collation.
|
|
139
|
+
builder[method](function () {
|
|
140
|
+
this.where(function () {
|
|
141
|
+
this.where(visible)
|
|
142
|
+
applyWhereForOperator({ builder: this, columnRef: columnRef.column, operator, value, knex })
|
|
143
|
+
}).orWhere(function () {
|
|
144
|
+
this.whereNot(visible)
|
|
145
|
+
applyWhereForOperator({ builder: this, columnRef: knex.raw('null'), operator, value, knex })
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
return
|
|
149
|
+
}
|
|
19
150
|
const likeOp = 'like'
|
|
20
151
|
|
|
21
152
|
const op = typeof operator === 'string' ? operator.toLowerCase() : operator
|
|
153
|
+
const textSearch = ['like', 'contains', 'startswith', 'endswith'].includes(op)
|
|
22
154
|
|
|
23
155
|
// Normalize scalar from possibly array input
|
|
24
156
|
const firstVal = Array.isArray(value) ? value[0] : value
|
|
25
157
|
|
|
26
158
|
// Null handling for equality and string ops
|
|
27
159
|
if (firstVal === null || firstVal === undefined) {
|
|
28
|
-
if (
|
|
160
|
+
if (textSearch) {
|
|
29
161
|
builder[methodNull](columnRef)
|
|
30
162
|
return
|
|
31
163
|
}
|
|
@@ -33,19 +165,18 @@ export function applyWhereForOperator ({ builder, columnRef, operator, value, kn
|
|
|
33
165
|
builder[methodNull](columnRef)
|
|
34
166
|
return
|
|
35
167
|
}
|
|
168
|
+
if (op === '!=' || op === '<>' || op === 'is not') {
|
|
169
|
+
builder[or ? 'orWhereNotNull' : 'whereNotNull'](columnRef)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
36
172
|
}
|
|
37
173
|
|
|
38
174
|
// Text search operators
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
builder[method](columnRef, likeOp, `${String(firstVal)}%`)
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
if (op === 'endswith') {
|
|
48
|
-
builder[method](columnRef, likeOp, `%${String(firstVal)}`)
|
|
175
|
+
if (textSearch) {
|
|
176
|
+
const textColumn = knex.client.config.client === 'pg' ? knex.raw('cast(?? as text)', [columnRef]) : columnRef
|
|
177
|
+
const prefix = op === 'startswith' ? '' : '%'
|
|
178
|
+
const suffix = op === 'endswith' ? '' : '%'
|
|
179
|
+
builder[method](textColumn, likeOp, `${prefix}${String(firstVal)}${suffix}`)
|
|
49
180
|
return
|
|
50
181
|
}
|
|
51
182
|
|
|
@@ -56,16 +187,10 @@ export function applyWhereForOperator ({ builder, columnRef, operator, value, kn
|
|
|
56
187
|
return
|
|
57
188
|
}
|
|
58
189
|
if (op === 'between') {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
builder.whereBetween(columnRef, values)
|
|
62
|
-
} else if (values.length === 1) {
|
|
63
|
-
if (values[0] === null || values[0] === undefined) {
|
|
64
|
-
builder[methodNull](columnRef)
|
|
65
|
-
} else {
|
|
66
|
-
builder[method](columnRef, '=', values[0])
|
|
67
|
-
}
|
|
190
|
+
if (!Array.isArray(value) || value.length !== 2 || value.some(bound => bound == null)) {
|
|
191
|
+
throw new RestApiValidationError('A between filter requires exactly two non-null bounds')
|
|
68
192
|
}
|
|
193
|
+
builder[or ? 'orWhereBetween' : 'whereBetween'](columnRef, value)
|
|
69
194
|
return
|
|
70
195
|
}
|
|
71
196
|
|
|
@@ -78,73 +203,6 @@ export function applyWhereForOperator ({ builder, columnRef, operator, value, kn
|
|
|
78
203
|
*
|
|
79
204
|
* @param {Object} hookParams - Hook parameters containing context
|
|
80
205
|
* @param {Object} dependencies - Dependencies injected by the plugin
|
|
81
|
-
*
|
|
82
|
-
* @example
|
|
83
|
-
* // Input: Search schema with polymorphic filter
|
|
84
|
-
* const searchSchema = {
|
|
85
|
-
* commentable_title: {
|
|
86
|
-
* type: 'string',
|
|
87
|
-
* polymorphicField: 'commentable', // Points to the polymorphic relationship
|
|
88
|
-
* targetFields: {
|
|
89
|
-
* posts: 'title', // When commentable_type='posts', search posts.title
|
|
90
|
-
* videos: 'title', // When commentable_type='videos', search videos.title
|
|
91
|
-
* articles: 'headline' // When commentable_type='articles', search articles.headline
|
|
92
|
-
* },
|
|
93
|
-
* filterOperator: 'like'
|
|
94
|
-
* }
|
|
95
|
-
* };
|
|
96
|
-
*
|
|
97
|
-
* // Filter request: { commentable_title: 'JavaScript' }
|
|
98
|
-
*
|
|
99
|
-
* // Result: Adds conditional LEFT JOINs and WHERE conditions
|
|
100
|
-
* // SQL generated:
|
|
101
|
-
* // LEFT JOIN posts ON comments.commentable_type = 'posts' AND comments.commentable_id = posts.id
|
|
102
|
-
* // LEFT JOIN videos ON comments.commentable_type = 'videos' AND comments.commentable_id = videos.id
|
|
103
|
-
* // LEFT JOIN articles ON comments.commentable_type = 'articles' AND comments.commentable_id = articles.id
|
|
104
|
-
* // WHERE (
|
|
105
|
-
* // (comments.commentable_type = 'posts' AND posts.title LIKE '%JavaScript%') OR
|
|
106
|
-
* // (comments.commentable_type = 'videos' AND videos.title LIKE '%JavaScript%') OR
|
|
107
|
-
* // (comments.commentable_type = 'articles' AND articles.headline LIKE '%JavaScript%')
|
|
108
|
-
* // )
|
|
109
|
-
*
|
|
110
|
-
* @example
|
|
111
|
-
* // Input: Complex polymorphic filter with cross-table paths
|
|
112
|
-
* const searchSchema = {
|
|
113
|
-
* commentable_author: {
|
|
114
|
-
* type: 'string',
|
|
115
|
-
* polymorphicField: 'commentable',
|
|
116
|
-
* targetFields: {
|
|
117
|
-
* posts: 'author.name', // Search post author's name
|
|
118
|
-
* videos: 'creator.name' // Search video creator's name
|
|
119
|
-
* }
|
|
120
|
-
* }
|
|
121
|
-
* };
|
|
122
|
-
*
|
|
123
|
-
* // Result: Creates nested JOINs for each polymorphic type
|
|
124
|
-
* // LEFT JOIN posts ON comments.commentable_type = 'posts' AND comments.commentable_id = posts.id
|
|
125
|
-
* // LEFT JOIN users AS posts_author ON posts.author_id = posts_author.id
|
|
126
|
-
* // LEFT JOIN videos ON comments.commentable_type = 'videos' AND comments.commentable_id = videos.id
|
|
127
|
-
* // LEFT JOIN users AS videos_creator ON videos.creator_id = videos_creator.id
|
|
128
|
-
*
|
|
129
|
-
* @description
|
|
130
|
-
* Used by:
|
|
131
|
-
* - rest-api-knex-plugin calls this hook first during query building
|
|
132
|
-
* - Must run before other filter hooks to establish JOINs
|
|
133
|
-
* - Applied when searchSchema contains polymorphicField definitions
|
|
134
|
-
*
|
|
135
|
-
* Purpose:
|
|
136
|
-
* - Enables filtering on polymorphic relationships without knowing the concrete type
|
|
137
|
-
* - Searches across multiple tables with a single filter parameter
|
|
138
|
-
* - Supports queries like "find all comments on content containing 'JavaScript'"
|
|
139
|
-
* - Uses conditional JOINs that only match when type field equals expected value
|
|
140
|
-
* - Maintains performance by leveraging indexed type/id columns
|
|
141
|
-
*
|
|
142
|
-
* Data flow:
|
|
143
|
-
* 1. Identifies filters with polymorphicField in searchSchema
|
|
144
|
-
* 2. For each polymorphic type, adds conditional LEFT JOIN
|
|
145
|
-
* 3. Builds OR conditions checking type field and target field together
|
|
146
|
-
* 4. Sets hasJoins flag for subsequent hooks
|
|
147
|
-
* 5. Returns modified query with polymorphic search capabilities
|
|
148
206
|
*/
|
|
149
207
|
export const polymorphicFiltersHook = async (hookParams, dependencies) => {
|
|
150
208
|
const { log, scopes, knex } = dependencies
|
|
@@ -222,7 +280,8 @@ export const polymorphicFiltersHook = async (hookParams, dependencies) => {
|
|
|
222
280
|
})
|
|
223
281
|
|
|
224
282
|
// Conditional JOIN - only matches when type is correct
|
|
225
|
-
|
|
283
|
+
const targetState = await buildSearchJoinQuery({ scopeName: targetType, scopes, context: hookParams.context, adapterUtils, db })
|
|
284
|
+
query.leftJoin(targetState.query.as(baseAlias), function () {
|
|
226
285
|
const typeColumn = adapterUtils.translateColumn(scopeName, typeField, tableAlias)
|
|
227
286
|
const idColumn = adapterUtils.translateColumn(scopeName, idField, tableAlias)
|
|
228
287
|
const targetIdColumn = adapterUtils.translateColumn(targetType, targetIdField, baseAlias)
|
|
@@ -243,71 +302,74 @@ export const polymorphicFiltersHook = async (hookParams, dependencies) => {
|
|
|
243
302
|
polymorphicJoins.get(baseAlias).aliasScopeMap = new Map()
|
|
244
303
|
}
|
|
245
304
|
polymorphicJoins.get(baseAlias).aliasScopeMap.set(baseAlias, targetType)
|
|
305
|
+
}
|
|
246
306
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
307
|
+
// Handle cross-table paths
|
|
308
|
+
if (targetFieldPath.includes('.')) {
|
|
309
|
+
log.trace('[POLYMORPHIC-SEARCH] Building cross-table JOINs for path:', targetFieldPath)
|
|
310
|
+
|
|
311
|
+
const pathParts = targetFieldPath.split('.')
|
|
312
|
+
let currentAlias = baseAlias
|
|
313
|
+
let currentScope = targetType
|
|
314
|
+
|
|
315
|
+
// Build JOIN for each segment except the last
|
|
316
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
317
|
+
const relationshipName = pathParts[i]
|
|
318
|
+
|
|
319
|
+
// Find the foreign key for this relationship
|
|
320
|
+
const currentSchema = scopes[currentScope].vars.schemaInfo.schemaInstance
|
|
321
|
+
let foreignKeyField = null
|
|
322
|
+
let nextScope = null
|
|
323
|
+
|
|
324
|
+
// Search schema for matching belongsTo
|
|
325
|
+
for (const [fieldName, fieldDef] of Object.entries(currentSchema.structure)) {
|
|
326
|
+
if (fieldDef.as === relationshipName && fieldDef.belongsTo) {
|
|
327
|
+
foreignKeyField = fieldName
|
|
328
|
+
nextScope = fieldDef.belongsTo
|
|
329
|
+
break
|
|
271
330
|
}
|
|
331
|
+
}
|
|
272
332
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
throw new Error(
|
|
280
|
-
'Cross-table polymorphic search through hasOne relationships not yet supported'
|
|
281
|
-
)
|
|
282
|
-
}
|
|
283
|
-
|
|
333
|
+
if (!foreignKeyField) {
|
|
334
|
+
// Check relationships for hasOne
|
|
335
|
+
const currentRelationships = scopes[currentScope].vars.schemaInfo.schemaRelationships
|
|
336
|
+
const rel = currentRelationships?.[relationshipName]
|
|
337
|
+
if (rel?.hasOne) {
|
|
338
|
+
// Handle hasOne - more complex
|
|
284
339
|
throw new Error(
|
|
285
|
-
|
|
340
|
+
'Cross-table polymorphic search through hasOne relationships not yet supported'
|
|
286
341
|
)
|
|
287
342
|
}
|
|
288
343
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
log.trace('[POLYMORPHIC-SEARCH] Adding cross-table JOIN:', {
|
|
295
|
-
from: currentAlias,
|
|
296
|
-
to: nextAlias,
|
|
297
|
-
table: nextTable
|
|
298
|
-
})
|
|
344
|
+
throw new Error(
|
|
345
|
+
`Cannot resolve relationship '${relationshipName}' in path '${targetFieldPath}' for scope '${currentScope}'`
|
|
346
|
+
)
|
|
347
|
+
}
|
|
299
348
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
349
|
+
// Build next JOIN
|
|
350
|
+
const nextAlias = `${currentAlias}_${relationshipName}`
|
|
351
|
+
const nextSchema = scopes[nextScope].vars.schemaInfo.schemaInstance
|
|
352
|
+
const nextTable = nextSchema?.tableName || nextScope
|
|
303
353
|
|
|
304
|
-
|
|
354
|
+
log.trace('[POLYMORPHIC-SEARCH] Adding cross-table JOIN:', {
|
|
355
|
+
from: currentAlias,
|
|
356
|
+
to: nextAlias,
|
|
357
|
+
table: nextTable
|
|
358
|
+
})
|
|
305
359
|
|
|
306
|
-
|
|
307
|
-
|
|
360
|
+
const nextIdField = scopes[nextScope].vars.schemaInfo.idProperty || 'id'
|
|
361
|
+
const sourceColumn = adapterUtils.translateColumn(currentScope, foreignKeyField, currentAlias)
|
|
362
|
+
const targetColumn = adapterUtils.translateColumn(nextScope, nextIdField, nextAlias)
|
|
308
363
|
|
|
309
|
-
|
|
364
|
+
if (!polymorphicJoins.get(baseAlias).aliasScopeMap.has(nextAlias)) {
|
|
365
|
+
const nextState = await buildSearchJoinQuery({ scopeName: nextScope, scopes, context: hookParams.context, adapterUtils, db })
|
|
366
|
+
query.leftJoin(nextState.query.as(nextAlias), sourceColumn, targetColumn)
|
|
310
367
|
}
|
|
368
|
+
|
|
369
|
+
currentAlias = nextAlias
|
|
370
|
+
currentScope = nextScope
|
|
371
|
+
|
|
372
|
+
polymorphicJoins.get(baseAlias).aliasScopeMap.set(currentAlias, currentScope)
|
|
311
373
|
}
|
|
312
374
|
}
|
|
313
375
|
}
|
|
@@ -327,10 +389,24 @@ export const polymorphicFiltersHook = async (hookParams, dependencies) => {
|
|
|
327
389
|
// Mark that we have JOINs for other hooks
|
|
328
390
|
hookParams.context.knexQuery.hasJoins = true
|
|
329
391
|
|
|
392
|
+
const references = new Map()
|
|
393
|
+
for (const { fieldDef, polymorphicField } of polymorphicSearches.values()) {
|
|
394
|
+
for (const [target, path] of Object.entries(fieldDef.targetFields)) {
|
|
395
|
+
const baseAlias = `${tableAlias}_${polymorphicField}_${target}`
|
|
396
|
+
const parts = path.split('.')
|
|
397
|
+
const field = parts.pop()
|
|
398
|
+
const alias = [baseAlias, ...parts].join('_')
|
|
399
|
+
const targetScope = polymorphicJoins.get(baseAlias).aliasScopeMap.get(alias)
|
|
400
|
+
references.set(adapterUtils.translateColumn(targetScope, field, alias), { scopeName: targetScope, field, alias })
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const columns = await resolveSearchColumns(references, { scopes, context: hookParams.context, adapterUtils, db })
|
|
404
|
+
|
|
330
405
|
// Step 3: Apply WHERE conditions
|
|
331
406
|
query.where(function applyPolymorphicWhere () {
|
|
332
407
|
const applyComparison = (builder, scope, alias, field, operator, rawValue) => {
|
|
333
|
-
const
|
|
408
|
+
const qualified = adapterUtils.translateColumn(scope, field, alias)
|
|
409
|
+
const columnRef = columns.get(qualified) || qualified
|
|
334
410
|
const normalizedValue = adapterUtils.translateFilterValue(scope, field, rawValue)
|
|
335
411
|
applyWhereForOperator({ builder, columnRef, operator, value: normalizedValue, knex })
|
|
336
412
|
}
|
|
@@ -351,6 +427,7 @@ export const polymorphicFiltersHook = async (hookParams, dependencies) => {
|
|
|
351
427
|
applyComparison(this, scopeName, typeColumnAlias, typeField, '=', targetType)
|
|
352
428
|
|
|
353
429
|
const baseAlias = `${tableAlias}_${searchInfo.polymorphicField}_${targetType}`
|
|
430
|
+
this.whereNotNull(adapterUtils.translateColumn(targetType, 'id', baseAlias))
|
|
354
431
|
const joinMeta = polymorphicJoins.get(baseAlias)
|
|
355
432
|
const aliasScopeMap = joinMeta?.aliasScopeMap || new Map([[baseAlias, targetType]])
|
|
356
433
|
|
|
@@ -383,92 +460,6 @@ export const polymorphicFiltersHook = async (hookParams, dependencies) => {
|
|
|
383
460
|
*
|
|
384
461
|
* @param {Object} hookParams - Hook parameters containing context
|
|
385
462
|
* @param {Object} dependencies - Dependencies injected by the plugin
|
|
386
|
-
*
|
|
387
|
-
* @example
|
|
388
|
-
* // Input: Simple cross-table filter
|
|
389
|
-
* const searchSchema = {
|
|
390
|
-
* author_name: {
|
|
391
|
-
* type: 'string',
|
|
392
|
-
* actualField: 'author.name', // Dot notation indicates JOIN needed
|
|
393
|
-
* filterOperator: 'like'
|
|
394
|
-
* }
|
|
395
|
-
* };
|
|
396
|
-
*
|
|
397
|
-
* // Filter request: { author_name: 'Smith' }
|
|
398
|
-
* // Query before: SELECT * FROM articles
|
|
399
|
-
*
|
|
400
|
-
* // Result: Adds JOIN and qualified WHERE
|
|
401
|
-
* // Query after:
|
|
402
|
-
* // SELECT * FROM articles
|
|
403
|
-
* // LEFT JOIN users AS articles_author ON articles.author_id = articles_author.id
|
|
404
|
-
* // WHERE articles_author.name LIKE '%Smith%'
|
|
405
|
-
*
|
|
406
|
-
* @example
|
|
407
|
-
* // Input: Multi-field search across tables
|
|
408
|
-
* const searchSchema = {
|
|
409
|
-
* search: {
|
|
410
|
-
* type: 'string',
|
|
411
|
-
* oneOf: [
|
|
412
|
-
* 'title', // Local field
|
|
413
|
-
* 'content', // Local field
|
|
414
|
-
* 'author.name', // Requires JOIN to users
|
|
415
|
-
* 'category.title' // Requires JOIN to categories
|
|
416
|
-
* ],
|
|
417
|
-
* filterOperator: 'like'
|
|
418
|
-
* }
|
|
419
|
-
* };
|
|
420
|
-
*
|
|
421
|
-
* // Filter request: { search: 'JavaScript' }
|
|
422
|
-
*
|
|
423
|
-
* // Result: Multiple JOINs and OR conditions
|
|
424
|
-
* // SELECT DISTINCT * FROM articles
|
|
425
|
-
* // LEFT JOIN users AS articles_author ON articles.author_id = articles_author.id
|
|
426
|
-
* // LEFT JOIN categories AS articles_category ON articles.category_id = articles_category.id
|
|
427
|
-
* // WHERE (
|
|
428
|
-
* // articles.title LIKE '%JavaScript%' OR
|
|
429
|
-
* // articles.content LIKE '%JavaScript%' OR
|
|
430
|
-
* // articles_author.name LIKE '%JavaScript%' OR
|
|
431
|
-
* // articles_category.title LIKE '%JavaScript%'
|
|
432
|
-
* // )
|
|
433
|
-
*
|
|
434
|
-
* @example
|
|
435
|
-
* // Input: Deep nested relationships (3 levels)
|
|
436
|
-
* const searchSchema = {
|
|
437
|
-
* company_country: {
|
|
438
|
-
* type: 'string',
|
|
439
|
-
* actualField: 'author.company.country.name'
|
|
440
|
-
* }
|
|
441
|
-
* };
|
|
442
|
-
*
|
|
443
|
-
* // Filter request: { company_country: 'USA' }
|
|
444
|
-
*
|
|
445
|
-
* // Result: Chain of JOINs following relationships
|
|
446
|
-
* // SELECT * FROM articles
|
|
447
|
-
* // LEFT JOIN users AS articles_author ON articles.author_id = articles_author.id
|
|
448
|
-
* // LEFT JOIN companies AS articles_author_company ON articles_author.company_id = articles_author_company.id
|
|
449
|
-
* // LEFT JOIN countries AS articles_author_company_country ON articles_author_company.country_id = articles_author_company_country.id
|
|
450
|
-
* // WHERE articles_author_company_country.name = 'USA'
|
|
451
|
-
*
|
|
452
|
-
* @description
|
|
453
|
-
* Used by:
|
|
454
|
-
* - rest-api-knex-plugin calls this hook second during query building
|
|
455
|
-
* - Runs after polymorphicFiltersHook but before basicFiltersHook
|
|
456
|
-
* - Applied when filters contain dot notation in actualField or oneOf
|
|
457
|
-
*
|
|
458
|
-
* Purpose:
|
|
459
|
-
* - Enables filtering on related table fields without manual JOIN writing
|
|
460
|
-
* - Automatically builds JOIN chains from dot notation paths
|
|
461
|
-
* - Detects one-to-many relationships and adds DISTINCT to prevent duplicates
|
|
462
|
-
* - Creates unique aliases to avoid naming conflicts
|
|
463
|
-
* - Validates that target fields are indexed for performance
|
|
464
|
-
*
|
|
465
|
-
* Data flow:
|
|
466
|
-
* 1. Scans filters for dot notation in actualField or oneOf arrays
|
|
467
|
-
* 2. For each cross-table reference, builds JOIN chain via buildJoinChain
|
|
468
|
-
* 3. Applies JOINs to query with proper aliasing
|
|
469
|
-
* 4. Adds DISTINCT if any one-to-many JOINs detected
|
|
470
|
-
* 5. Applies WHERE conditions using qualified field names
|
|
471
|
-
* 6. Sets hasJoins flag for basicFiltersHook to use
|
|
472
463
|
*/
|
|
473
464
|
export const crossTableFiltersHook = async (hookParams, dependencies) => {
|
|
474
465
|
const { log, scopes, knex } = dependencies
|
|
@@ -489,13 +480,7 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
|
|
|
489
480
|
const aliasScopeMap = new Map()
|
|
490
481
|
aliasScopeMap.set(tableAlias, scopeName)
|
|
491
482
|
|
|
492
|
-
//
|
|
493
|
-
const requiredIndexes = analyzeRequiredIndexes(scopes, log, scopeName, schemaInfo)
|
|
494
|
-
if (requiredIndexes.length > 0) {
|
|
495
|
-
log.debug('Cross-table search requires indexes:', requiredIndexes)
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Step 2: Build JOIN maps
|
|
483
|
+
// Build JOIN maps.
|
|
499
484
|
const joinMap = new Map()
|
|
500
485
|
const fieldPathMap = new Map()
|
|
501
486
|
let hasCrossTableFilters = false
|
|
@@ -562,13 +547,7 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
|
|
|
562
547
|
return adapterUtils.translateColumn(scopeForAlias, field, alias)
|
|
563
548
|
}
|
|
564
549
|
|
|
565
|
-
const resolveFieldColumn =
|
|
566
|
-
if (field.includes('.')) {
|
|
567
|
-
const qualified = fieldPathMap.get(field) || field
|
|
568
|
-
return translateQualifiedColumn(qualified)
|
|
569
|
-
}
|
|
570
|
-
return adapterUtils.translateColumn(scopeName, field, tableAlias)
|
|
571
|
-
}
|
|
550
|
+
const resolveFieldColumn = field => columns.get(field)
|
|
572
551
|
|
|
573
552
|
const normalizeFieldValue = (field, value) => {
|
|
574
553
|
if (field.includes('.')) {
|
|
@@ -587,11 +566,11 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
|
|
|
587
566
|
return adapterUtils.translateFilterValue(scopeName, field, value)
|
|
588
567
|
}
|
|
589
568
|
|
|
590
|
-
//
|
|
569
|
+
// Apply JOINs.
|
|
591
570
|
const appliedJoins = new Set()
|
|
592
571
|
|
|
593
|
-
const applyPolymorphicJoin = (join) => {
|
|
594
|
-
query.leftJoin(
|
|
572
|
+
const applyPolymorphicJoin = (join, source) => {
|
|
573
|
+
query.leftJoin(source.as(join.joinAlias), function () {
|
|
595
574
|
const parts = join.joinCondition.split(' AND ')
|
|
596
575
|
const [typeCondition, idCondition] = parts
|
|
597
576
|
|
|
@@ -617,37 +596,40 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
|
|
|
617
596
|
})
|
|
618
597
|
}
|
|
619
598
|
|
|
620
|
-
const applyStandardJoin = (join) => {
|
|
599
|
+
const applyStandardJoin = (join, source) => {
|
|
621
600
|
const [leftSide, rightSide] = join.joinCondition.split(' = ')
|
|
622
601
|
const translatedLeft = translateQualifiedColumn(leftSide.trim())
|
|
623
602
|
const translatedRight = translateQualifiedColumn(rightSide.trim())
|
|
624
|
-
query.leftJoin(
|
|
603
|
+
query.leftJoin(source.as(join.joinAlias), function () {
|
|
625
604
|
this.on(translatedLeft, translatedRight)
|
|
626
605
|
})
|
|
627
606
|
}
|
|
628
607
|
|
|
629
|
-
const processJoin = (join) => {
|
|
608
|
+
const processJoin = async (join) => {
|
|
630
609
|
const joinKey = `${join.joinAlias}:${join.joinCondition}`
|
|
631
610
|
if (appliedJoins.has(joinKey)) return
|
|
632
611
|
|
|
612
|
+
const state = join.relationshipType === 'manyToMany_pivot' && dependencies.buildSearchMembershipQuery
|
|
613
|
+
? await dependencies.buildSearchMembershipQuery({ ...join, db, context: hookParams.context })
|
|
614
|
+
: await buildSearchJoinQuery({ scopeName: join.targetScopeName, scopes, context: hookParams.context, adapterUtils, db })
|
|
633
615
|
if (join.isPolymorphic && join.joinCondition.includes(' AND ')) {
|
|
634
|
-
applyPolymorphicJoin(join)
|
|
616
|
+
applyPolymorphicJoin(join, state.query)
|
|
635
617
|
} else {
|
|
636
|
-
applyStandardJoin(join)
|
|
618
|
+
applyStandardJoin(join, state.query)
|
|
637
619
|
}
|
|
638
620
|
|
|
639
621
|
appliedJoins.add(joinKey)
|
|
640
622
|
}
|
|
641
623
|
|
|
642
|
-
joinMap.
|
|
624
|
+
for (const joinInfo of joinMap.values()) {
|
|
643
625
|
if (joinInfo.isMultiLevel && Array.isArray(joinInfo.joinChain)) {
|
|
644
|
-
joinInfo.joinChain
|
|
626
|
+
for (const join of joinInfo.joinChain) await processJoin(join)
|
|
645
627
|
} else {
|
|
646
|
-
processJoin(joinInfo)
|
|
628
|
+
await processJoin(joinInfo)
|
|
647
629
|
}
|
|
648
|
-
}
|
|
630
|
+
}
|
|
649
631
|
|
|
650
|
-
//
|
|
632
|
+
// Handle DISTINCT.
|
|
651
633
|
let hasOneToManyJoins = false
|
|
652
634
|
joinMap.forEach((joinInfo) => {
|
|
653
635
|
if (joinInfo.isOneToMany) {
|
|
@@ -667,6 +649,22 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
|
|
|
667
649
|
// Store state for basic filters hook
|
|
668
650
|
hookParams.context.knexQuery.hasJoins = true
|
|
669
651
|
|
|
652
|
+
const references = new Map()
|
|
653
|
+
for (const filterKey of Object.keys(filters)) {
|
|
654
|
+
const fieldDef = schemaInfo.searchSchemaStructure[filterKey]
|
|
655
|
+
if (!fieldDef || fieldDef.polymorphicField ||
|
|
656
|
+
(!fieldDef.actualField?.includes('.') && !fieldDef.oneOf?.some(field => field.includes('.')))) continue
|
|
657
|
+
for (const field of fieldDef.oneOf || [fieldDef.actualField || filterKey]) {
|
|
658
|
+
if (field.includes('.')) {
|
|
659
|
+
const [alias, ...parts] = (fieldPathMap.get(field) || field).split('.')
|
|
660
|
+
references.set(field, { scopeName: aliasScopeMap.get(alias) || scopeName, field: parts.join('.'), alias })
|
|
661
|
+
} else {
|
|
662
|
+
references.set(field, { scopeName, field, alias: tableAlias })
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const columns = await resolveSearchColumns(references, { scopes, context: hookParams.context, adapterUtils, db })
|
|
667
|
+
|
|
670
668
|
// Step 5: Apply WHERE conditions for cross-table filters
|
|
671
669
|
query.where(function () {
|
|
672
670
|
for (const [filterKey, filterValue] of Object.entries(filters)) {
|
|
@@ -688,8 +686,6 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
|
|
|
688
686
|
let searchTerms = [filterValue]
|
|
689
687
|
if (fieldDef.splitBy && typeof filterValue === 'string') {
|
|
690
688
|
searchTerms = filterValue.split(fieldDef.splitBy).filter(term => term.trim())
|
|
691
|
-
} else if (Array.isArray(filterValue)) {
|
|
692
|
-
searchTerms = filterValue
|
|
693
689
|
}
|
|
694
690
|
|
|
695
691
|
const applyTermComparison = (builder, method, field, raw) => {
|
|
@@ -706,27 +702,25 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
|
|
|
706
702
|
}
|
|
707
703
|
|
|
708
704
|
this.where(function () {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
applyTermComparison(this, method, field, term)
|
|
715
|
-
})
|
|
705
|
+
searchTerms.forEach((term, termIndex) => {
|
|
706
|
+
const join = fieldDef.matchAll || termIndex === 0 ? 'where' : 'orWhere'
|
|
707
|
+
this[join](function () {
|
|
708
|
+
fieldDef.oneOf.forEach((field, index) => {
|
|
709
|
+
applyTermComparison(this, index === 0 ? 'and' : 'or', field, term)
|
|
716
710
|
})
|
|
717
711
|
})
|
|
718
|
-
}
|
|
719
|
-
fieldDef.oneOf.forEach((field, index) => {
|
|
720
|
-
const method = index === 0 ? 'and' : 'or'
|
|
721
|
-
applyTermComparison(this, method, field, filterValue)
|
|
722
|
-
})
|
|
723
|
-
}
|
|
712
|
+
})
|
|
724
713
|
})
|
|
725
714
|
break
|
|
726
715
|
}
|
|
727
716
|
|
|
728
717
|
case fieldDef.applyFilter && typeof fieldDef.applyFilter === 'function':
|
|
729
|
-
fieldDef.applyFilter.call(this, this, filterValue
|
|
718
|
+
fieldDef.applyFilter.call(this, this, filterValue, {
|
|
719
|
+
column: field => translateQualifiedColumn(fieldPathMap.get(field) || field),
|
|
720
|
+
value: normalizeFieldValue,
|
|
721
|
+
context: hookParams.context,
|
|
722
|
+
scopeName
|
|
723
|
+
})
|
|
730
724
|
break
|
|
731
725
|
|
|
732
726
|
default: {
|
|
@@ -747,141 +741,6 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
|
|
|
747
741
|
*
|
|
748
742
|
* @param {Object} hookParams - Hook parameters containing context
|
|
749
743
|
* @param {Object} dependencies - Dependencies injected by the plugin
|
|
750
|
-
*
|
|
751
|
-
* @example
|
|
752
|
-
* // Input: Basic equality filter
|
|
753
|
-
* const searchSchema = {
|
|
754
|
-
* status: {
|
|
755
|
-
* type: 'string',
|
|
756
|
-
* filterOperator: '=' // Default is '=' if not specified
|
|
757
|
-
* }
|
|
758
|
-
* };
|
|
759
|
-
*
|
|
760
|
-
* // Filter request: { status: 'published' }
|
|
761
|
-
* // Query before: SELECT * FROM articles
|
|
762
|
-
*
|
|
763
|
-
* // Result: Adds qualified WHERE clause
|
|
764
|
-
* // Query after: SELECT * FROM articles WHERE articles.status = 'published'
|
|
765
|
-
*
|
|
766
|
-
* @example
|
|
767
|
-
* // Input: LIKE filter for partial text matching
|
|
768
|
-
* const searchSchema = {
|
|
769
|
-
* title: {
|
|
770
|
-
* type: 'string',
|
|
771
|
-
* filterOperator: 'like'
|
|
772
|
-
* },
|
|
773
|
-
* content: {
|
|
774
|
-
* type: 'string',
|
|
775
|
-
* filterOperator: 'like'
|
|
776
|
-
* }
|
|
777
|
-
* };
|
|
778
|
-
*
|
|
779
|
-
* // Filter request: { title: 'JavaScript', content: 'async' }
|
|
780
|
-
*
|
|
781
|
-
* // Result: Multiple LIKE conditions
|
|
782
|
-
* // WHERE articles.title LIKE '%JavaScript%'
|
|
783
|
-
* // AND articles.content LIKE '%async%'
|
|
784
|
-
*
|
|
785
|
-
* @example
|
|
786
|
-
* // Input: Multi-field OR search with oneOf
|
|
787
|
-
* const searchSchema = {
|
|
788
|
-
* search: {
|
|
789
|
-
* type: 'string',
|
|
790
|
-
* oneOf: ['title', 'content', 'summary'],
|
|
791
|
-
* filterOperator: 'like',
|
|
792
|
-
* splitBy: ' ', // Split search terms by space
|
|
793
|
-
* matchAll: true // All terms must match somewhere
|
|
794
|
-
* }
|
|
795
|
-
* };
|
|
796
|
-
*
|
|
797
|
-
* // Filter request: { search: 'REST API' }
|
|
798
|
-
*
|
|
799
|
-
* // Result: Each term must match in at least one field
|
|
800
|
-
* // WHERE (
|
|
801
|
-
* // (articles.title LIKE '%REST%' OR articles.content LIKE '%REST%' OR articles.summary LIKE '%REST%')
|
|
802
|
-
* // AND
|
|
803
|
-
* // (articles.title LIKE '%API%' OR articles.content LIKE '%API%' OR articles.summary LIKE '%API%')
|
|
804
|
-
* // )
|
|
805
|
-
*
|
|
806
|
-
* @example
|
|
807
|
-
* // Input: Advanced operators - IN and BETWEEN
|
|
808
|
-
* const searchSchema = {
|
|
809
|
-
* category_id: {
|
|
810
|
-
* type: 'array',
|
|
811
|
-
* filterOperator: 'in'
|
|
812
|
-
* },
|
|
813
|
-
* price: {
|
|
814
|
-
* type: 'array',
|
|
815
|
-
* filterOperator: 'between'
|
|
816
|
-
* },
|
|
817
|
-
* tags: {
|
|
818
|
-
* type: 'array',
|
|
819
|
-
* filterOperator: 'in'
|
|
820
|
-
* }
|
|
821
|
-
* };
|
|
822
|
-
*
|
|
823
|
-
* // Filter request: {
|
|
824
|
-
* // category_id: [1, 2, 3],
|
|
825
|
-
* // price: [10.00, 99.99],
|
|
826
|
-
* // tags: ['javascript', 'nodejs']
|
|
827
|
-
* // }
|
|
828
|
-
*
|
|
829
|
-
* // Result: IN and BETWEEN clauses
|
|
830
|
-
* // WHERE articles.category_id IN (1, 2, 3)
|
|
831
|
-
* // AND articles.price BETWEEN 10.00 AND 99.99
|
|
832
|
-
* // AND articles.tags IN ('javascript', 'nodejs')
|
|
833
|
-
*
|
|
834
|
-
* @example
|
|
835
|
-
* // Input: Custom filter function
|
|
836
|
-
* const searchSchema = {
|
|
837
|
-
* has_comments: {
|
|
838
|
-
* type: 'boolean',
|
|
839
|
-
* applyFilter: function(query, value) {
|
|
840
|
-
* if (value === true) {
|
|
841
|
-
* query.whereExists(function() {
|
|
842
|
-
* this.select('id')
|
|
843
|
-
* .from('comments')
|
|
844
|
-
* .whereRaw('comments.article_id = articles.id');
|
|
845
|
-
* });
|
|
846
|
-
* } else if (value === false) {
|
|
847
|
-
* query.whereNotExists(function() {
|
|
848
|
-
* this.select('id')
|
|
849
|
-
* .from('comments')
|
|
850
|
-
* .whereRaw('comments.article_id = articles.id');
|
|
851
|
-
* });
|
|
852
|
-
* }
|
|
853
|
-
* }
|
|
854
|
-
* }
|
|
855
|
-
* };
|
|
856
|
-
*
|
|
857
|
-
* // Filter request: { has_comments: true }
|
|
858
|
-
*
|
|
859
|
-
* // Result: Subquery to check existence
|
|
860
|
-
* // WHERE EXISTS (
|
|
861
|
-
* // SELECT id FROM comments WHERE comments.article_id = articles.id
|
|
862
|
-
* // )
|
|
863
|
-
*
|
|
864
|
-
* @description
|
|
865
|
-
* Used by:
|
|
866
|
-
* - rest-api-knex-plugin calls this hook last during query building
|
|
867
|
-
* - Runs after all JOINs have been established by previous hooks
|
|
868
|
-
* - Handles all non-cross-table, non-polymorphic filters
|
|
869
|
-
*
|
|
870
|
-
* Purpose:
|
|
871
|
-
* - Implements standard SQL filtering operations safely via Knex
|
|
872
|
-
* - Always qualifies field names with table name to prevent ambiguity
|
|
873
|
-
* - Supports various operators: =, like, in, between, and custom
|
|
874
|
-
* - Enables multi-field OR searches with optional term splitting
|
|
875
|
-
* - Handles null values appropriately (using whereNull)
|
|
876
|
-
* - Allows custom filter logic via applyFilter functions
|
|
877
|
-
*
|
|
878
|
-
* Data flow:
|
|
879
|
-
* 1. Skips filters already handled by polymorphic/cross-table hooks
|
|
880
|
-
* 2. Qualifies all field names with table name (e.g., articles.title)
|
|
881
|
-
* 3. Applies appropriate WHERE clause based on filterOperator
|
|
882
|
-
* 4. For oneOf filters, creates OR conditions across specified fields
|
|
883
|
-
* 5. For custom filters, delegates to applyFilter function
|
|
884
|
-
* 6. Returns query with all basic filters applied
|
|
885
744
|
*/
|
|
886
745
|
export const basicFiltersHook = async (hookParams, dependencies) => {
|
|
887
746
|
const { log, scopes, knex } = dependencies
|
|
@@ -891,6 +750,7 @@ export const basicFiltersHook = async (hookParams, dependencies) => {
|
|
|
891
750
|
const scopeName = hookParams.context?.knexQuery?.scopeName
|
|
892
751
|
const filters = hookParams.context?.knexQuery?.filters
|
|
893
752
|
const query = hookParams.context?.knexQuery?.query
|
|
753
|
+
const db = hookParams.context?.knexQuery?.db || knex
|
|
894
754
|
if (!scopeName || !filters || !scopes[scopeName]) {
|
|
895
755
|
return
|
|
896
756
|
}
|
|
@@ -902,14 +762,18 @@ export const basicFiltersHook = async (hookParams, dependencies) => {
|
|
|
902
762
|
log.trace('[DEBUG basicFiltersHook] Called with:', {
|
|
903
763
|
scopeName,
|
|
904
764
|
hasFilters: !!filters,
|
|
905
|
-
filters,
|
|
906
|
-
searchSchemaKeys: Object.keys(schemaInfo.searchSchemaStructure || {}),
|
|
907
765
|
tableName
|
|
908
766
|
})
|
|
909
767
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
768
|
+
const references = new Map()
|
|
769
|
+
for (const filterKey of Object.keys(filters)) {
|
|
770
|
+
const fieldDef = schemaInfo.searchSchemaStructure[filterKey]
|
|
771
|
+
if (!fieldDef || fieldDef.polymorphicField || fieldDef.actualField?.includes('.') ||
|
|
772
|
+
fieldDef.oneOf?.some(field => field.includes('.')) || (fieldDef.applyFilter && !fieldDef.oneOf)) continue
|
|
773
|
+
for (const field of fieldDef.oneOf || [fieldDef.actualField || filterKey]) references.set(field, { scopeName, field, alias: tableAlias })
|
|
774
|
+
}
|
|
775
|
+
const columns = await resolveSearchColumns(references, { scopes, context: hookParams.context, adapterUtils, db })
|
|
776
|
+
const qualifyField = field => columns.get(field)
|
|
913
777
|
const normalizeValue = (field, value) => {
|
|
914
778
|
if (Array.isArray(value)) {
|
|
915
779
|
return value.map((entry) => adapterUtils.translateFilterValue(scopeName, field, entry))
|
|
@@ -925,7 +789,7 @@ export const basicFiltersHook = async (hookParams, dependencies) => {
|
|
|
925
789
|
log.trace(`[DEBUG basicFiltersHook] No field definition for filter key: ${filterKey}`)
|
|
926
790
|
continue
|
|
927
791
|
}
|
|
928
|
-
log.trace(`[DEBUG basicFiltersHook] Processing filter: ${filterKey}
|
|
792
|
+
log.trace(`[DEBUG basicFiltersHook] Processing filter: ${filterKey}`)
|
|
929
793
|
|
|
930
794
|
// Skip if this is a cross-table filter
|
|
931
795
|
if (fieldDef.actualField?.includes('.') ||
|
|
@@ -948,47 +812,33 @@ export const basicFiltersHook = async (hookParams, dependencies) => {
|
|
|
948
812
|
}
|
|
949
813
|
|
|
950
814
|
this.where(function () {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
columnRef,
|
|
963
|
-
operator,
|
|
964
|
-
value: perFieldValue,
|
|
965
|
-
knex,
|
|
966
|
-
or: index !== 0,
|
|
967
|
-
})
|
|
815
|
+
searchTerms.forEach((term, termIndex) => {
|
|
816
|
+
const join = fieldDef.matchAll || termIndex === 0 ? 'where' : 'orWhere'
|
|
817
|
+
this[join](function () {
|
|
818
|
+
fieldDef.oneOf.forEach((field, index) => {
|
|
819
|
+
applyWhereForOperator({
|
|
820
|
+
builder: this,
|
|
821
|
+
columnRef: qualifyField(field),
|
|
822
|
+
operator,
|
|
823
|
+
value: normalizeValue(field, term),
|
|
824
|
+
knex,
|
|
825
|
+
or: index !== 0,
|
|
968
826
|
})
|
|
969
827
|
})
|
|
970
828
|
})
|
|
971
|
-
}
|
|
972
|
-
fieldDef.oneOf.forEach((field, index) => {
|
|
973
|
-
const columnRef = qualifyField(field)
|
|
974
|
-
const normalizedValue = normalizeValue(field, filterValue)
|
|
975
|
-
applyWhereForOperator({
|
|
976
|
-
builder: this,
|
|
977
|
-
columnRef,
|
|
978
|
-
operator,
|
|
979
|
-
value: normalizedValue,
|
|
980
|
-
knex,
|
|
981
|
-
or: index !== 0,
|
|
982
|
-
})
|
|
983
|
-
})
|
|
984
|
-
}
|
|
829
|
+
})
|
|
985
830
|
})
|
|
986
831
|
break
|
|
987
832
|
}
|
|
988
833
|
|
|
989
834
|
case fieldDef.applyFilter && typeof fieldDef.applyFilter === 'function':
|
|
990
835
|
// Custom filter
|
|
991
|
-
fieldDef.applyFilter.call(this, this, filterValue
|
|
836
|
+
fieldDef.applyFilter.call(this, this, filterValue, {
|
|
837
|
+
column: field => adapterUtils.translateColumn(scopeName, field, tableAlias),
|
|
838
|
+
value: normalizeValue,
|
|
839
|
+
context: hookParams.context,
|
|
840
|
+
scopeName
|
|
841
|
+
})
|
|
992
842
|
break
|
|
993
843
|
|
|
994
844
|
default: {
|
|
@@ -999,7 +849,7 @@ export const basicFiltersHook = async (hookParams, dependencies) => {
|
|
|
999
849
|
|
|
1000
850
|
const operator = resolveSearchOperator(fieldDef)
|
|
1001
851
|
const normalized = normalizeValue(actualField, filterValue)
|
|
1002
|
-
log.trace(`[DEBUG basicFiltersHook] Applying filter: ${dbField} ${operator}
|
|
852
|
+
log.trace(`[DEBUG basicFiltersHook] Applying filter: ${dbField} ${operator}`)
|
|
1003
853
|
applyWhereForOperator({ builder: this, columnRef: dbField, operator, value: normalized, knex })
|
|
1004
854
|
break
|
|
1005
855
|
}
|