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.
- package/CLAUDE.md +1 -2
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
- package/docs/API.md +51 -14
- package/docs/COMPARISON.md +2 -2
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
- package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
- package/docs/GUIDE/index.md +18 -9
- package/docs/QUICKSTART.md +5 -1
- package/docs/index.md +1 -1
- package/index.js +3 -2
- package/package.json +2 -2
- package/plugins/core/connectors/express-plugin.js +97 -184
- package/plugins/core/connectors/fastify-plugin.js +206 -0
- package/plugins/core/connectors/lib/connector-core.js +195 -0
- package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
- package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
- package/plugins/core/lib/dbIntrospection.js +1078 -0
- package/plugins/core/lib/dbTablesOperations.js +1104 -160
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
- package/plugins/core/lib/querying/knex-process-includes.js +3 -4
- package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
- package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
- package/plugins/core/lib/querying/query-builder-utils.js +19 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
- package/plugins/core/lib/querying/sort-helpers.js +33 -0
- package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
- package/plugins/core/lib/querying/url-helpers.js +8 -2
- package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
- package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
- package/plugins/core/lib/storage/storage-adapter.js +79 -2
- package/plugins/core/lib/storage/storage-mapping.js +204 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
- package/plugins/core/rest-api-autofilter-plugin.js +414 -0
- package/plugins/core/rest-api-knex-plugin.js +249 -211
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
- package/plugins/core/rest-api-plugin-methods/common.js +121 -83
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
- package/plugins/core/rest-api-plugin-methods/get.js +23 -14
- package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
- package/plugins/core/rest-api-plugin-methods/post.js +13 -8
- package/plugins/core/rest-api-plugin-methods/put.js +45 -21
- package/plugins/core/rest-api-plugin-methods/query.js +15 -53
- package/plugins/core/rest-api-plugin.js +2 -0
- package/plugins/core/rest-api-positioning-plugin.js +1 -1
- package/plugins/core/rest-api-query-projections-plugin.js +130 -0
- package/plugins/core/socketio-plugin.js +1 -3
- package/tests/autofilter.test.js +463 -0
- package/tests/custom-id-simplified-post.test.js +75 -0
- package/tests/custom-idproperty-relationships.test.js +33 -0
- package/tests/db-introspection.test.js +518 -0
- package/tests/db-table-operations.test.js +781 -0
- package/tests/fastify-plugin.test.js +141 -0
- package/tests/field-getters.test.js +1 -2
- package/tests/field-setters.test.js +0 -4
- package/tests/fixtures/api-configs.js +188 -219
- package/tests/helpers/fake-fastify.js +99 -0
- package/tests/http-connectors-parity.test.js +235 -0
- package/tests/projected-fields.test.js +319 -0
- package/tests/relationships.test.js +3 -3
- package/tests/searchschema-knex-behavior.test.js +187 -0
- package/tests/searchschema-merge.test.js +21 -69
- package/tests/storage-mapping.test.js +210 -0
- package/audit/00-charter.md +0 -103
- package/audit/01-scope-map.md +0 -84
- package/audit/02-coverage-matrix.csv +0 -174
- package/audit/03-invariants.md +0 -123
- package/audit/04-standards-rubric.md +0 -56
- package/audit/05-competitor-rubric.md +0 -77
- package/audit/06-findings.md +0 -160
- package/audit/07-risk-register.csv +0 -10
- package/audit/08-remediation-roadmap.md +0 -177
- package/audit/09-signoff-check.md +0 -23
- package/audit/10-final-synthesis.md +0 -29
- package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
- package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
- package/audit/evidence/code/boundary-leak-checks.txt +0 -72
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
- package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
- package/audit/evidence/code/critical-pass1-summary.csv +0 -67
- package/audit/evidence/code/critical-pass2-summary.csv +0 -67
- package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
- package/audit/evidence/code/error-model-signals.txt +0 -98
- package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
- package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
- package/audit/evidence/code/hook-flow-signals.txt +0 -282
- package/audit/evidence/code/idproperty-signals.txt +0 -157
- package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
- package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
- package/audit/evidence/code/imports-json-rest-api.txt +0 -354
- package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
- package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
- package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
- package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
- package/audit/evidence/code/permission-signals.txt +0 -193
- package/audit/evidence/code/query-include-signals.txt +0 -687
- package/audit/evidence/code/transaction-signals.txt +0 -28
- package/audit/evidence/code/validation-signals.txt +0 -650
- package/audit/evidence/code/write-path-signals.txt +0 -61
- package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
- package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
- package/audit/evidence/external/corpus-freeze.md +0 -21
- package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
- package/audit/evidence/external/npm-download-signals.txt +0 -23
- package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
- package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
- package/audit/evidence/external/readme-fortune-signals.txt +0 -26
- package/audit/evidence/external/readme-fortune.md +0 -165
- package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
- package/audit/evidence/external/readme-json-api-serializer.md +0 -690
- package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
- package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
- package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
- package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
- package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
- package/audit/evidence/external/readme-jsonapi-server.md +0 -104
- package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
- package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
- package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
- package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
- package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
- package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
- package/audit/evidence/external/standards-sources.txt +0 -6
- package/audit/evidence/runtime/baseline-commits.txt +0 -2
- package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
- package/audit/evidence/runtime/environment.txt +0 -4
- package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
- package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
- package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
- package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
- package/audit/evidence/runtime/test-exitcodes.txt +0 -4
- package/audit/evidence/runtime/test-summary.md +0 -107
- package/audit/evidence/runtime/trace-anchors.txt +0 -61
- package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
- package/audit/evidence/tests/failing-tests-summary.txt +0 -25
- package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
- package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
- package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
- package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
- package/audit/evidence/tests/query-include-test-signals.txt +0 -948
- package/audit/evidence/tests/relationship-test-signals.txt +0 -223
- package/audit/evidence/tests/skipped-tests.txt +0 -10
- package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
- package/audit/evidence/tests/test-suite-list.txt +0 -32
- package/audit/subsystems/docs-drift.md +0 -49
- package/audit/subsystems/pass2-cross-review.md +0 -131
- package/audit/subsystems/plugins-access.md +0 -51
- package/audit/subsystems/plugins-bulk.md +0 -50
- package/audit/subsystems/plugins-cors.md +0 -48
- package/audit/subsystems/plugins-file.md +0 -53
- package/audit/subsystems/plugins-multihome.md +0 -50
- package/audit/subsystems/plugins-positioning.md +0 -54
- package/audit/subsystems/plugins-socketio.md +0 -50
- package/audit/subsystems/query-engine.md +0 -61
- package/audit/subsystems/relationships.md +0 -55
- package/audit/subsystems/rest-orchestration.md +0 -66
- package/audit/subsystems/schema-core.md +0 -64
- package/audit/subsystems/storage-knex.md +0 -57
- package/audit/subsystems/tests.md +0 -48
- package/audit/subsystems/transport-express.md +0 -55
- package/audit/traces/trace-read-get.md +0 -22
- package/audit/traces/trace-read-query.md +0 -25
- package/audit/traces/trace-relationship-endpoints.md +0 -31
- package/audit/traces/trace-write-delete.md +0 -17
- package/audit/traces/trace-write-patch.md +0 -26
- package/audit/traces/trace-write-post.md +0 -22
- package/audit/traces/trace-write-put.md +0 -23
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
- package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
- package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
- package/plugins/core/rest-api-access.js +0 -459
- package/plugins/core/rest-api-multihome-plugin.js +0 -314
- package/tests/multihome.test.js +0 -553
- package/tests/multihome.test.js.bak +0 -550
- package/tests/ownership-optout.test.js +0 -76
- package/tests/rest-api-access.test.js +0 -206
|
@@ -74,33 +74,109 @@
|
|
|
74
74
|
* 6. Return array of unique included resources
|
|
75
75
|
*/
|
|
76
76
|
|
|
77
|
-
import { buildFieldSelection } from '../querying-writing/knex-field-helpers.js'
|
|
77
|
+
import { applyFieldSelectionToQuery, buildFieldSelection } from '../querying-writing/knex-field-helpers.js'
|
|
78
78
|
import { getForeignKeyFields } from '../querying-writing/field-utils.js'
|
|
79
79
|
import { toJsonApiRecord } from './knex-json-api-transformers-querying.js'
|
|
80
80
|
import { buildWindowedIncludeQuery, applyStandardIncludeConfig, buildOrderByClause } from './knex-window-queries.js'
|
|
81
|
+
import { unwrapQueryBuilderState } from './query-builder-utils.js'
|
|
81
82
|
import { RELATIONSHIPS_KEY, RELATIONSHIP_METADATA_KEY, ROW_NUMBER_KEY, COMPUTED_DEPENDENCIES_KEY, DEFAULT_QUERY_LIMIT } from '../querying-writing/knex-constants.js'
|
|
82
83
|
import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
|
|
84
|
+
import {
|
|
85
|
+
getFieldValue as getStorageFieldValue,
|
|
86
|
+
getIdColumn as getStorageIdColumn,
|
|
87
|
+
getStorageColumn as getMappedStorageColumn,
|
|
88
|
+
} from '../storage/storage-mapping.js'
|
|
89
|
+
|
|
90
|
+
const getScopeStorageAdapter = (scopes, scopeName) => scopes[scopeName]?.vars?.storageAdapter || null
|
|
91
|
+
|
|
92
|
+
const getIdColumnForScope = (scopes, scopeName) => {
|
|
93
|
+
const adapter = getScopeStorageAdapter(scopes, scopeName)
|
|
94
|
+
if (adapter?.getIdColumn) {
|
|
95
|
+
return adapter.getIdColumn()
|
|
96
|
+
}
|
|
83
97
|
|
|
84
|
-
|
|
85
|
-
|
|
98
|
+
return getStorageIdColumn(scopes[scopeName]?.vars?.schemaInfo || {})
|
|
99
|
+
}
|
|
86
100
|
|
|
87
|
-
|
|
88
|
-
|
|
101
|
+
const getFieldValueForScope = (scopes, scopeName, record, fieldName) => {
|
|
102
|
+
const adapter = getScopeStorageAdapter(scopes, scopeName)
|
|
103
|
+
if (adapter?.getFieldValue) {
|
|
104
|
+
return adapter.getFieldValue(record, fieldName)
|
|
105
|
+
}
|
|
89
106
|
|
|
90
|
-
|
|
107
|
+
return getStorageFieldValue(record, scopes[scopeName]?.vars?.schemaInfo || {}, fieldName)
|
|
108
|
+
}
|
|
91
109
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
const translateColumnForScope = (scopes, scopeName, fieldName) => {
|
|
111
|
+
const adapter = getScopeStorageAdapter(scopes, scopeName)
|
|
112
|
+
if (adapter?.translateColumn) {
|
|
113
|
+
return adapter.translateColumn(fieldName)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return getMappedStorageColumn(scopes[scopeName]?.vars?.schemaInfo || {}, fieldName)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const buildRelationshipLinks = (scopes, scopeName, record, relationshipName) => {
|
|
120
|
+
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
121
|
+
const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
self: `${urlPrefix}/${scopeName}/${recordId}/relationships/${relationshipName}`,
|
|
125
|
+
related: `${urlPrefix}/${scopeName}/${recordId}/${relationshipName}`
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const buildScopedIncludeContext = ({ requestContext, scopeName, scopeObject, storageAdapter }) => {
|
|
130
|
+
const queryParams = requestContext?.queryParams
|
|
131
|
+
? { ...requestContext.queryParams }
|
|
132
|
+
: {}
|
|
98
133
|
|
|
99
|
-
|
|
100
|
-
|
|
134
|
+
if ('filters' in queryParams) {
|
|
135
|
+
delete queryParams.filters
|
|
101
136
|
}
|
|
102
137
|
|
|
103
|
-
return
|
|
138
|
+
return {
|
|
139
|
+
...(requestContext || {}),
|
|
140
|
+
scopeName,
|
|
141
|
+
schemaInfo: scopeObject.vars.schemaInfo,
|
|
142
|
+
storageAdapter,
|
|
143
|
+
queryParams
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const applyScopeFiltersToIncludeQuery = async ({
|
|
148
|
+
query,
|
|
149
|
+
scopes,
|
|
150
|
+
scopeName,
|
|
151
|
+
requestContext,
|
|
152
|
+
db,
|
|
153
|
+
tableName
|
|
154
|
+
}) => {
|
|
155
|
+
const scopeObject = scopes[scopeName]
|
|
156
|
+
if (!scopeObject?.applyQueryFilters) {
|
|
157
|
+
return { query }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const storageAdapter = scopeObject.vars.storageAdapter
|
|
161
|
+
|
|
162
|
+
const queryState = await scopeObject.applyQueryFilters({
|
|
163
|
+
query,
|
|
164
|
+
filters: undefined,
|
|
165
|
+
scopeName,
|
|
166
|
+
tableName: tableName || scopeObject.vars.schemaInfo.tableName,
|
|
167
|
+
db,
|
|
168
|
+
schemaInfo: scopeObject.vars.schemaInfo,
|
|
169
|
+
storageAdapter
|
|
170
|
+
}, buildScopedIncludeContext({
|
|
171
|
+
requestContext,
|
|
172
|
+
scopeName,
|
|
173
|
+
scopeObject,
|
|
174
|
+
storageAdapter
|
|
175
|
+
}))
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
query: unwrapQueryBuilderState(queryState, query)
|
|
179
|
+
}
|
|
104
180
|
}
|
|
105
181
|
|
|
106
182
|
/**
|
|
@@ -162,12 +238,12 @@ const translateSelectFieldsForAdapter = (fields, adapter) => {
|
|
|
162
238
|
* 3. Collects unique IDs for each type
|
|
163
239
|
* 4. Returns map used for batch WHERE IN queries
|
|
164
240
|
*/
|
|
165
|
-
export const groupByPolymorphicType = (records, typeField, idField) => {
|
|
241
|
+
export const groupByPolymorphicType = (records, typeField, idField, getFieldValue = (record, fieldName) => record[fieldName]) => {
|
|
166
242
|
const grouped = {}
|
|
167
243
|
|
|
168
244
|
records.forEach(record => {
|
|
169
|
-
const type = record
|
|
170
|
-
const id = record
|
|
245
|
+
const type = getFieldValue(record, typeField)
|
|
246
|
+
const id = getFieldValue(record, idField)
|
|
171
247
|
|
|
172
248
|
// Skip if either type or id is missing
|
|
173
249
|
if (!type || !id) return
|
|
@@ -295,7 +371,7 @@ const loadRelationshipMetadata = async (scopes, records, scopeName) => {
|
|
|
295
371
|
// Process belongsTo relationships from schema
|
|
296
372
|
for (const [fieldName, fieldDef] of Object.entries(schema)) {
|
|
297
373
|
if (fieldDef.belongsTo && fieldDef.as) {
|
|
298
|
-
const foreignKeyValue = record
|
|
374
|
+
const foreignKeyValue = getFieldValueForScope(scopes, scopeName, record, fieldName)
|
|
299
375
|
if (foreignKeyValue != null) {
|
|
300
376
|
record[RELATIONSHIP_METADATA_KEY][fieldDef.as] = {
|
|
301
377
|
data: {
|
|
@@ -320,8 +396,8 @@ const loadRelationshipMetadata = async (scopes, records, scopeName) => {
|
|
|
320
396
|
} else if (relDef.belongsToPolymorphic) {
|
|
321
397
|
// Process polymorphic relationships defined in relationships section
|
|
322
398
|
const { typeField, idField } = relDef.belongsToPolymorphic
|
|
323
|
-
const type = record
|
|
324
|
-
const id = record
|
|
399
|
+
const type = getFieldValueForScope(scopes, scopeName, record, typeField)
|
|
400
|
+
const id = getFieldValueForScope(scopes, scopeName, record, idField)
|
|
325
401
|
|
|
326
402
|
if (type && id) {
|
|
327
403
|
record[RELATIONSHIP_METADATA_KEY][relName] = {
|
|
@@ -361,7 +437,6 @@ const loadRelationshipMetadata = async (scopes, records, scopeName) => {
|
|
|
361
437
|
* - processedPaths: Set - Set of already processed paths
|
|
362
438
|
* - currentPath: string - Current include path for tracking
|
|
363
439
|
* - fields: Object - Sparse fieldsets configuration
|
|
364
|
-
* - idProperty: string - The ID property name
|
|
365
440
|
* @param {Object} deps - Dependencies object containing:
|
|
366
441
|
* - context.scopes: Object - The hooked-api scopes object
|
|
367
442
|
* - context.log: Object - Logger instance
|
|
@@ -369,8 +444,8 @@ const loadRelationshipMetadata = async (scopes, records, scopeName) => {
|
|
|
369
444
|
* @returns {Promise<void>}
|
|
370
445
|
*/
|
|
371
446
|
export const loadBelongsTo = async (scope, deps) => {
|
|
372
|
-
const { records, scopeName, fieldName, fieldDef, includeName, subIncludes, included, processedPaths, currentPath, fields
|
|
373
|
-
const { scopes, log, knex, capabilities } = deps.context
|
|
447
|
+
const { records, scopeName, fieldName, fieldDef, includeName, subIncludes, included, processedPaths, currentPath, fields } = scope
|
|
448
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
374
449
|
try {
|
|
375
450
|
log.trace('[INCLUDE] Loading belongsTo:', {
|
|
376
451
|
fieldName,
|
|
@@ -387,7 +462,7 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
387
462
|
|
|
388
463
|
// Collect all foreign key values
|
|
389
464
|
const foreignKeyValues = records
|
|
390
|
-
.map(
|
|
465
|
+
.map(record => getFieldValueForScope(scopes, scopeName, record, fieldName))
|
|
391
466
|
.filter(val => val != null) // Filter out null/undefined
|
|
392
467
|
|
|
393
468
|
const uniqueIds = [...new Set(foreignKeyValues)]
|
|
@@ -404,12 +479,8 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
404
479
|
const relationshipObject = { data: null }
|
|
405
480
|
|
|
406
481
|
// Add links if urlPrefix is configured
|
|
407
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
408
482
|
if (scopeName) {
|
|
409
|
-
relationshipObject.links =
|
|
410
|
-
self: `${urlPrefix}/${scopeName}/${record[idProperty]}/relationships/${includeName}`,
|
|
411
|
-
related: `${urlPrefix}/${scopeName}/${record[idProperty]}/${includeName}`
|
|
412
|
-
}
|
|
483
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
413
484
|
}
|
|
414
485
|
|
|
415
486
|
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
@@ -419,7 +490,7 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
419
490
|
|
|
420
491
|
// Get target table info
|
|
421
492
|
const targetTableName = scopes[targetScope].vars.schemaInfo.tableName
|
|
422
|
-
const
|
|
493
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
423
494
|
const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance
|
|
424
495
|
|
|
425
496
|
// Build field selection for sparse fieldsets
|
|
@@ -428,6 +499,7 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
428
499
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
429
500
|
? await buildFieldSelection(targetScopeObject, {
|
|
430
501
|
context: {
|
|
502
|
+
...(requestContext || {}),
|
|
431
503
|
scopeName: targetScope,
|
|
432
504
|
queryParams: { fields: { [targetScope]: fields[targetScope] } },
|
|
433
505
|
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
@@ -437,14 +509,38 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
437
509
|
: null
|
|
438
510
|
|
|
439
511
|
// Load the target records
|
|
440
|
-
let query = knex(targetTableName).whereIn(
|
|
512
|
+
let query = knex(targetTableName).whereIn(targetIdColumn, uniqueIds)
|
|
441
513
|
if (fieldSelectionInfo) {
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
514
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
515
|
+
query,
|
|
516
|
+
scope: targetScopeObject,
|
|
517
|
+
fieldSelectionInfo,
|
|
518
|
+
tableName: targetTableName,
|
|
519
|
+
useTablePrefix: false,
|
|
520
|
+
storageAdapter,
|
|
521
|
+
db: knex,
|
|
522
|
+
context: {
|
|
523
|
+
...(requestContext || {}),
|
|
524
|
+
scopeName: targetScope,
|
|
525
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
526
|
+
storageAdapter,
|
|
527
|
+
},
|
|
528
|
+
scopeName: targetScope
|
|
529
|
+
})
|
|
530
|
+
query = selectionState.query
|
|
531
|
+
} else if (targetIdColumn !== 'id') {
|
|
532
|
+
// If no field selection but the storage ID column is not `id`, alias it for response shaping.
|
|
533
|
+
query = query.select('*', `${targetIdColumn} as id`)
|
|
447
534
|
}
|
|
535
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
536
|
+
query,
|
|
537
|
+
scopes,
|
|
538
|
+
scopeName: targetScope,
|
|
539
|
+
requestContext,
|
|
540
|
+
db: knex,
|
|
541
|
+
tableName: targetTableName
|
|
542
|
+
})
|
|
543
|
+
query = scopedQueryState.query
|
|
448
544
|
const targetRecords = await query
|
|
449
545
|
|
|
450
546
|
// Load relationship metadata for all target records
|
|
@@ -452,9 +548,9 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
452
548
|
|
|
453
549
|
// Create lookup map
|
|
454
550
|
const targetById = {}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
551
|
+
targetRecords.forEach(record => {
|
|
552
|
+
targetById[getFieldValueForScope(scopes, targetScope, record, 'id')] = record
|
|
553
|
+
})
|
|
458
554
|
|
|
459
555
|
// Set relationships on original records
|
|
460
556
|
const targetRecordsToProcess = []
|
|
@@ -462,7 +558,7 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
462
558
|
records.forEach(record => {
|
|
463
559
|
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
464
560
|
|
|
465
|
-
const targetId = record
|
|
561
|
+
const targetId = getFieldValueForScope(scopes, scopeName, record, fieldName)
|
|
466
562
|
const targetRecord = targetById[targetId]
|
|
467
563
|
|
|
468
564
|
if (targetRecord) {
|
|
@@ -488,12 +584,8 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
488
584
|
const relationshipObject = { data: { type: targetScope, id: String(targetId) } }
|
|
489
585
|
|
|
490
586
|
// Add links if urlPrefix is configured
|
|
491
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
492
587
|
if (scopeName) {
|
|
493
|
-
relationshipObject.links =
|
|
494
|
-
self: `${urlPrefix}/${scopeName}/${record[idProperty]}/relationships/${includeName}`,
|
|
495
|
-
related: `${urlPrefix}/${scopeName}/${record[idProperty]}/${includeName}`
|
|
496
|
-
}
|
|
588
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
497
589
|
}
|
|
498
590
|
|
|
499
591
|
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
@@ -512,12 +604,8 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
512
604
|
const relationshipObject = { data: null }
|
|
513
605
|
|
|
514
606
|
// Add links if urlPrefix is configured
|
|
515
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
516
607
|
if (scopeName) {
|
|
517
|
-
relationshipObject.links =
|
|
518
|
-
self: `${urlPrefix}/${scopeName}/${record[idProperty]}/relationships/${includeName}`,
|
|
519
|
-
related: `${urlPrefix}/${scopeName}/${record[idProperty]}/${includeName}`
|
|
520
|
-
}
|
|
608
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
521
609
|
}
|
|
522
610
|
|
|
523
611
|
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
@@ -529,8 +617,8 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
529
617
|
const nextPath = `${currentPath}.${includeName}`
|
|
530
618
|
if (!processedPaths.has(nextPath)) {
|
|
531
619
|
await processIncludes(
|
|
532
|
-
{ records: targetRecordsToProcess, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields
|
|
533
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
620
|
+
{ records: targetRecordsToProcess, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
|
|
621
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
534
622
|
)
|
|
535
623
|
}
|
|
536
624
|
}
|
|
@@ -580,7 +668,7 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
580
668
|
*/
|
|
581
669
|
export const loadHasMany = async (scope, deps) => {
|
|
582
670
|
const { records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
|
|
583
|
-
const { scopes, log, knex, capabilities } = deps.context
|
|
671
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
584
672
|
try {
|
|
585
673
|
log.trace('[INCLUDE] Loading hasMany/manyToMany relationship:', {
|
|
586
674
|
scopeName,
|
|
@@ -592,7 +680,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
592
680
|
})
|
|
593
681
|
|
|
594
682
|
// Collect all parent IDs
|
|
595
|
-
const mainIds = records.map(
|
|
683
|
+
const mainIds = records.map(record => getFieldValueForScope(scopes, scopeName, record, 'id')).filter(Boolean)
|
|
596
684
|
|
|
597
685
|
if (mainIds.length === 0) {
|
|
598
686
|
log.trace('[INCLUDE] No parent IDs found, skipping hasMany load')
|
|
@@ -631,12 +719,8 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
631
719
|
const relationshipObject = { data: [] }
|
|
632
720
|
|
|
633
721
|
// Add links if urlPrefix is configured
|
|
634
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
635
722
|
if (scopeName) {
|
|
636
|
-
relationshipObject.links =
|
|
637
|
-
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${includeName}`,
|
|
638
|
-
related: `${urlPrefix}/${scopeName}/${record.id}/${includeName}`
|
|
639
|
-
}
|
|
723
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
640
724
|
}
|
|
641
725
|
|
|
642
726
|
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
@@ -656,10 +740,11 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
656
740
|
const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance
|
|
657
741
|
const targetScopeObject = scopes[targetScope]
|
|
658
742
|
const storageAdapter = targetScopeObject.vars.storageAdapter
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
743
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
744
|
+
const fieldSelectionInfo = fields?.[targetScope]
|
|
745
|
+
? await buildFieldSelection(targetScopeObject, {
|
|
662
746
|
context: {
|
|
747
|
+
...(requestContext || {}),
|
|
663
748
|
scopeName: targetScope,
|
|
664
749
|
queryParams: { fields: { [targetScope]: fields[targetScope] } },
|
|
665
750
|
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
@@ -671,7 +756,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
671
756
|
// Step 4: For many-to-many with limits, we need a different approach
|
|
672
757
|
let targetRecords
|
|
673
758
|
|
|
674
|
-
if (relDef.include?.strategy === 'window') {
|
|
759
|
+
if (relDef.include?.strategy === 'window' && !fieldSelectionInfo?.queryFieldsToSelect?.length) {
|
|
675
760
|
// For many-to-many with window functions, we need to limit per parent
|
|
676
761
|
// This requires joining back through the pivot table
|
|
677
762
|
|
|
@@ -690,27 +775,36 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
690
775
|
})
|
|
691
776
|
}
|
|
692
777
|
|
|
693
|
-
|
|
694
|
-
const windowQuery = knex
|
|
778
|
+
let windowQuery = knex
|
|
695
779
|
.select(`${targetTable}.*`)
|
|
696
780
|
|
|
697
781
|
// Add id alias if needed
|
|
698
|
-
if (
|
|
699
|
-
windowQuery.select(`${targetTable}.${
|
|
782
|
+
if (targetIdColumn !== 'id') {
|
|
783
|
+
windowQuery.select(`${targetTable}.${targetIdColumn} as id`)
|
|
700
784
|
}
|
|
701
785
|
|
|
702
786
|
windowQuery.select(
|
|
703
787
|
knex.raw(
|
|
704
788
|
'ROW_NUMBER() OVER (PARTITION BY pivot.?? ORDER BY ' +
|
|
705
|
-
buildOrderByClause(relDef.include?.orderBy || [
|
|
789
|
+
buildOrderByClause(relDef.include?.orderBy || [targetIdColumn], targetTable) +
|
|
706
790
|
') as ' + ROW_NUMBER_KEY,
|
|
707
791
|
[foreignKey]
|
|
708
792
|
)
|
|
709
793
|
)
|
|
710
794
|
.from(targetTable)
|
|
711
|
-
.join(`${pivotTable} as pivot`, `${targetTable}.${
|
|
795
|
+
.join(`${pivotTable} as pivot`, `${targetTable}.${targetIdColumn}`, 'pivot.' + otherKey)
|
|
712
796
|
.whereIn(`pivot.${foreignKey}`, mainIds)
|
|
713
797
|
|
|
798
|
+
const scopedWindowQueryState = await applyScopeFiltersToIncludeQuery({
|
|
799
|
+
query: windowQuery,
|
|
800
|
+
scopes,
|
|
801
|
+
scopeName: targetScope,
|
|
802
|
+
requestContext,
|
|
803
|
+
db: knex,
|
|
804
|
+
tableName: targetTable
|
|
805
|
+
})
|
|
806
|
+
windowQuery = scopedWindowQueryState.query
|
|
807
|
+
|
|
714
808
|
// Wrap to filter by row number
|
|
715
809
|
const limitedQuery = knex
|
|
716
810
|
.select('*')
|
|
@@ -723,17 +817,41 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
723
817
|
targetRecords.forEach(record => delete record[ROW_NUMBER_KEY])
|
|
724
818
|
} else {
|
|
725
819
|
// Standard query without per-parent limits
|
|
726
|
-
|
|
727
|
-
let query = knex(targetTable).whereIn(targetIdProperty, targetIds)
|
|
820
|
+
let query = knex(targetTable).whereIn(targetIdColumn, targetIds)
|
|
728
821
|
|
|
729
822
|
if (fieldSelectionInfo) {
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
823
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
824
|
+
query,
|
|
825
|
+
scope: targetScopeObject,
|
|
826
|
+
fieldSelectionInfo,
|
|
827
|
+
tableName: targetTable,
|
|
828
|
+
useTablePrefix: false,
|
|
829
|
+
storageAdapter,
|
|
830
|
+
db: knex,
|
|
831
|
+
context: {
|
|
832
|
+
...(requestContext || {}),
|
|
833
|
+
scopeName: targetScope,
|
|
834
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
835
|
+
storageAdapter,
|
|
836
|
+
},
|
|
837
|
+
scopeName: targetScope
|
|
838
|
+
})
|
|
839
|
+
query = selectionState.query
|
|
840
|
+
} else if (targetIdColumn !== 'id') {
|
|
841
|
+
// If no field selection but the storage ID column is not `id`, alias it for response shaping.
|
|
842
|
+
query = query.select('*', `${targetIdColumn} as id`)
|
|
735
843
|
}
|
|
736
844
|
|
|
845
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
846
|
+
query,
|
|
847
|
+
scopes,
|
|
848
|
+
scopeName: targetScope,
|
|
849
|
+
requestContext,
|
|
850
|
+
db: knex,
|
|
851
|
+
tableName: targetTable
|
|
852
|
+
})
|
|
853
|
+
query = scopedQueryState.query
|
|
854
|
+
|
|
737
855
|
// Apply standard include config (global limits)
|
|
738
856
|
if (relDef.include) {
|
|
739
857
|
const targetVars = scopes[targetScope].vars
|
|
@@ -751,7 +869,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
751
869
|
// Step 6: Create lookup map for target records
|
|
752
870
|
const targetById = {}
|
|
753
871
|
targetRecords.forEach(record => {
|
|
754
|
-
targetById[record
|
|
872
|
+
targetById[getFieldValueForScope(scopes, targetScope, record, 'id')] = record
|
|
755
873
|
})
|
|
756
874
|
|
|
757
875
|
// Step 7: Group pivot records by parent ID
|
|
@@ -769,14 +887,16 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
769
887
|
records.forEach(record => {
|
|
770
888
|
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
771
889
|
|
|
772
|
-
const
|
|
890
|
+
const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
|
|
891
|
+
const childIds = pivotsByParent[recordId] || []
|
|
773
892
|
|
|
774
893
|
const relData = childIds
|
|
775
894
|
.map(childId => targetById[childId])
|
|
776
895
|
.filter(Boolean)
|
|
777
896
|
.map(childRecord => {
|
|
778
897
|
// Add to included
|
|
779
|
-
const
|
|
898
|
+
const childRecordId = getFieldValueForScope(scopes, targetScope, childRecord, 'id')
|
|
899
|
+
const resourceKey = `${targetScope}:${childRecordId}`
|
|
780
900
|
if (!included.has(resourceKey)) {
|
|
781
901
|
const jsonApiRecord = toJsonApiRecord(
|
|
782
902
|
scopes[targetScope],
|
|
@@ -798,24 +918,20 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
798
918
|
|
|
799
919
|
included.set(resourceKey, jsonApiRecord)
|
|
800
920
|
}
|
|
801
|
-
return { type: targetScope, id: String(
|
|
921
|
+
return { type: targetScope, id: String(childRecordId) }
|
|
802
922
|
})
|
|
803
923
|
|
|
804
924
|
const relationshipObject = { data: relData }
|
|
805
925
|
|
|
806
926
|
// Add links if urlPrefix is configured
|
|
807
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
808
927
|
if (scopeName) {
|
|
809
|
-
relationshipObject.links =
|
|
810
|
-
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${includeName}`,
|
|
811
|
-
related: `${urlPrefix}/${scopeName}/${record.id}/${includeName}`
|
|
812
|
-
}
|
|
928
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
813
929
|
}
|
|
814
930
|
|
|
815
931
|
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
816
932
|
|
|
817
933
|
// Update the record in the included Map if it exists
|
|
818
|
-
const recordKey = `${scopeName}:${
|
|
934
|
+
const recordKey = `${scopeName}:${recordId}`
|
|
819
935
|
if (included.has(recordKey)) {
|
|
820
936
|
const existingRecord = included.get(recordKey)
|
|
821
937
|
if (!existingRecord.relationships) {
|
|
@@ -830,8 +946,8 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
830
946
|
const nextPath = `${currentPath}.${includeName}`
|
|
831
947
|
if (!processedPaths.has(nextPath)) {
|
|
832
948
|
await processIncludes(
|
|
833
|
-
{ records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields
|
|
834
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
949
|
+
{ records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
|
|
950
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
835
951
|
)
|
|
836
952
|
}
|
|
837
953
|
}
|
|
@@ -854,7 +970,6 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
854
970
|
const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance
|
|
855
971
|
const targetScopeObject = scopes[targetScope]
|
|
856
972
|
const storageAdapter = targetScopeObject.vars.storageAdapter
|
|
857
|
-
const targetIdProperty = targetScopeObject.vars.schemaInfo.idProperty
|
|
858
973
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
859
974
|
? await buildFieldSelection(targetScopeObject, {
|
|
860
975
|
context: {
|
|
@@ -873,9 +988,11 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
873
988
|
if (relDef.include?.strategy === 'window') {
|
|
874
989
|
try {
|
|
875
990
|
// Try to build window function query
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
991
|
+
if (fieldSelectionInfo?.queryFieldsToSelect?.length) {
|
|
992
|
+
throw new Error('Query projection fields require the standard include query path.')
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const selectFields = fieldSelectionInfo?.fieldsToSelect || null
|
|
879
996
|
|
|
880
997
|
query = buildWindowedIncludeQuery(
|
|
881
998
|
knex,
|
|
@@ -887,6 +1004,15 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
887
1004
|
capabilities,
|
|
888
1005
|
targetScopeObject.vars // Pass target scope vars
|
|
889
1006
|
)
|
|
1007
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
1008
|
+
query,
|
|
1009
|
+
scopes,
|
|
1010
|
+
scopeName: targetScope,
|
|
1011
|
+
requestContext,
|
|
1012
|
+
db: knex,
|
|
1013
|
+
tableName: targetTable
|
|
1014
|
+
})
|
|
1015
|
+
query = scopedQueryState.query
|
|
890
1016
|
usingWindowFunction = true
|
|
891
1017
|
log.debug('[INCLUDE] Using window function strategy with limits')
|
|
892
1018
|
} catch (error) {
|
|
@@ -905,13 +1031,39 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
905
1031
|
|
|
906
1032
|
// Build standard query if not using window functions
|
|
907
1033
|
if (!usingWindowFunction) {
|
|
908
|
-
|
|
1034
|
+
const foreignKeyColumn = translateColumnForScope(scopes, targetScope, foreignKey)
|
|
1035
|
+
query = knex(targetTable).whereIn(foreignKeyColumn, mainIds)
|
|
909
1036
|
|
|
910
1037
|
if (fieldSelectionInfo) {
|
|
911
|
-
const
|
|
912
|
-
|
|
1038
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
1039
|
+
query,
|
|
1040
|
+
scope: targetScopeObject,
|
|
1041
|
+
fieldSelectionInfo,
|
|
1042
|
+
tableName: targetTable,
|
|
1043
|
+
useTablePrefix: false,
|
|
1044
|
+
storageAdapter,
|
|
1045
|
+
db: knex,
|
|
1046
|
+
context: {
|
|
1047
|
+
...(requestContext || {}),
|
|
1048
|
+
scopeName: targetScope,
|
|
1049
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
1050
|
+
storageAdapter,
|
|
1051
|
+
},
|
|
1052
|
+
scopeName: targetScope
|
|
1053
|
+
})
|
|
1054
|
+
query = selectionState.query
|
|
913
1055
|
}
|
|
914
1056
|
|
|
1057
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
1058
|
+
query,
|
|
1059
|
+
scopes,
|
|
1060
|
+
scopeName: targetScope,
|
|
1061
|
+
requestContext,
|
|
1062
|
+
db: knex,
|
|
1063
|
+
tableName: targetTable
|
|
1064
|
+
})
|
|
1065
|
+
query = scopedQueryState.query
|
|
1066
|
+
|
|
915
1067
|
// Apply standard config with defaults
|
|
916
1068
|
const targetVars = scopes[targetScope].vars
|
|
917
1069
|
query = applyStandardIncludeConfig(
|
|
@@ -941,7 +1093,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
941
1093
|
// Group by parent ID
|
|
942
1094
|
const childrenByParent = {}
|
|
943
1095
|
targetRecords.forEach(record => {
|
|
944
|
-
const parentId = record
|
|
1096
|
+
const parentId = getFieldValueForScope(scopes, targetScope, record, foreignKey)
|
|
945
1097
|
if (!childrenByParent[parentId]) {
|
|
946
1098
|
childrenByParent[parentId] = []
|
|
947
1099
|
}
|
|
@@ -952,10 +1104,12 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
952
1104
|
records.forEach(record => {
|
|
953
1105
|
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
954
1106
|
|
|
955
|
-
const
|
|
1107
|
+
const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
|
|
1108
|
+
const children = childrenByParent[recordId] || []
|
|
956
1109
|
const relData = children.map(childRecord => {
|
|
957
1110
|
// Add to included
|
|
958
|
-
const
|
|
1111
|
+
const childRecordId = getFieldValueForScope(scopes, targetScope, childRecord, 'id')
|
|
1112
|
+
const resourceKey = `${targetScope}:${childRecordId}`
|
|
959
1113
|
if (!included.has(resourceKey)) {
|
|
960
1114
|
const jsonApiRecord = toJsonApiRecord(
|
|
961
1115
|
scopes[targetScope],
|
|
@@ -977,24 +1131,20 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
977
1131
|
|
|
978
1132
|
included.set(resourceKey, jsonApiRecord)
|
|
979
1133
|
}
|
|
980
|
-
return { type: targetScope, id: String(
|
|
1134
|
+
return { type: targetScope, id: String(childRecordId) }
|
|
981
1135
|
})
|
|
982
1136
|
|
|
983
1137
|
const relationshipObject = { data: relData }
|
|
984
1138
|
|
|
985
1139
|
// Add links if urlPrefix is configured
|
|
986
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
987
1140
|
if (scopeName) {
|
|
988
|
-
relationshipObject.links =
|
|
989
|
-
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${includeName}`,
|
|
990
|
-
related: `${urlPrefix}/${scopeName}/${record.id}/${includeName}`
|
|
991
|
-
}
|
|
1141
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
992
1142
|
}
|
|
993
1143
|
|
|
994
1144
|
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
995
1145
|
|
|
996
1146
|
// Update the record in the included Map if it exists
|
|
997
|
-
const recordKey = `${scopeName}:${
|
|
1147
|
+
const recordKey = `${scopeName}:${recordId}`
|
|
998
1148
|
if (included.has(recordKey)) {
|
|
999
1149
|
const existingRecord = included.get(recordKey)
|
|
1000
1150
|
if (!existingRecord.relationships) {
|
|
@@ -1009,8 +1159,8 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
1009
1159
|
const nextPath = `${currentPath}.${includeName}`
|
|
1010
1160
|
if (!processedPaths.has(nextPath)) {
|
|
1011
1161
|
await processIncludes(
|
|
1012
|
-
{ records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields
|
|
1013
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1162
|
+
{ records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
|
|
1163
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1014
1164
|
)
|
|
1015
1165
|
}
|
|
1016
1166
|
}
|
|
@@ -1047,7 +1197,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
1047
1197
|
*/
|
|
1048
1198
|
export const loadHasOne = async (scope, deps) => {
|
|
1049
1199
|
const { records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
|
|
1050
|
-
const { context: { scopes, log, knex, capabilities } } = deps
|
|
1200
|
+
const { context: { scopes, log, knex, capabilities, requestContext } } = deps
|
|
1051
1201
|
|
|
1052
1202
|
log = log?.child ? log.child({ method: 'loadHasOne' }) : console
|
|
1053
1203
|
|
|
@@ -1057,8 +1207,7 @@ export const loadHasOne = async (scope, deps) => {
|
|
|
1057
1207
|
}
|
|
1058
1208
|
|
|
1059
1209
|
// Get parent IDs
|
|
1060
|
-
const
|
|
1061
|
-
const mainIds = [...new Set(records.map(r => r[idProperty]).filter(Boolean))]
|
|
1210
|
+
const mainIds = [...new Set(records.map(record => getFieldValueForScope(scopes, scopeName, record, 'id')).filter(Boolean))]
|
|
1062
1211
|
|
|
1063
1212
|
if (mainIds.length === 0) {
|
|
1064
1213
|
log.trace('[INCLUDE] No parent IDs found, skipping hasOne load')
|
|
@@ -1081,12 +1230,12 @@ export const loadHasOne = async (scope, deps) => {
|
|
|
1081
1230
|
const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance
|
|
1082
1231
|
const targetScopeObject = scopes[targetScope]
|
|
1083
1232
|
const storageAdapter = targetScopeObject.vars.storageAdapter
|
|
1084
|
-
const targetIdProperty = targetScopeObject.vars.schemaInfo.idProperty
|
|
1085
1233
|
|
|
1086
1234
|
// Build field selection for sparse fieldsets
|
|
1087
1235
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
1088
1236
|
? await buildFieldSelection(targetScopeObject, {
|
|
1089
1237
|
context: {
|
|
1238
|
+
...(requestContext || {}),
|
|
1090
1239
|
scopeName: targetScope,
|
|
1091
1240
|
queryParams: { fields: { [targetScope]: fields[targetScope] } },
|
|
1092
1241
|
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
@@ -1096,19 +1245,45 @@ export const loadHasOne = async (scope, deps) => {
|
|
|
1096
1245
|
: null
|
|
1097
1246
|
|
|
1098
1247
|
// Query related records
|
|
1099
|
-
|
|
1248
|
+
const foreignKeyColumn = translateColumnForScope(scopes, targetScope, foreignKey)
|
|
1249
|
+
let query = knex(targetTable).whereIn(foreignKeyColumn, mainIds)
|
|
1100
1250
|
|
|
1101
|
-
if (fieldSelectionInfo
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1251
|
+
if (fieldSelectionInfo) {
|
|
1252
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
1253
|
+
query,
|
|
1254
|
+
scope: targetScopeObject,
|
|
1255
|
+
fieldSelectionInfo,
|
|
1256
|
+
tableName: targetTable,
|
|
1257
|
+
useTablePrefix: false,
|
|
1258
|
+
storageAdapter,
|
|
1259
|
+
db: knex,
|
|
1260
|
+
context: {
|
|
1261
|
+
...(requestContext || {}),
|
|
1262
|
+
scopeName: targetScope,
|
|
1263
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
1264
|
+
storageAdapter,
|
|
1265
|
+
},
|
|
1266
|
+
scopeName: targetScope
|
|
1267
|
+
})
|
|
1268
|
+
query = selectionState.query
|
|
1104
1269
|
}
|
|
1105
1270
|
|
|
1271
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
1272
|
+
query,
|
|
1273
|
+
scopes,
|
|
1274
|
+
scopeName: targetScope,
|
|
1275
|
+
requestContext,
|
|
1276
|
+
db: knex,
|
|
1277
|
+
tableName: targetTable
|
|
1278
|
+
})
|
|
1279
|
+
query = scopedQueryState.query
|
|
1280
|
+
|
|
1106
1281
|
const relatedRecords = await query
|
|
1107
1282
|
|
|
1108
1283
|
// Create a map of parent ID to related record (should be one-to-one)
|
|
1109
1284
|
const relatedByParentId = {}
|
|
1110
1285
|
for (const related of relatedRecords) {
|
|
1111
|
-
const parentId = related
|
|
1286
|
+
const parentId = getFieldValueForScope(scopes, targetScope, related, foreignKey)
|
|
1112
1287
|
if (relatedByParentId[parentId]) {
|
|
1113
1288
|
log.warn(`[INCLUDE] Multiple records found for hasOne relationship '${includeName}' with ${foreignKey}=${parentId}`)
|
|
1114
1289
|
}
|
|
@@ -1117,7 +1292,7 @@ export const loadHasOne = async (scope, deps) => {
|
|
|
1117
1292
|
|
|
1118
1293
|
// Process each parent record
|
|
1119
1294
|
for (const record of records) {
|
|
1120
|
-
const recordId = record
|
|
1295
|
+
const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
|
|
1121
1296
|
const relatedRecord = relatedByParentId[recordId]
|
|
1122
1297
|
|
|
1123
1298
|
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
@@ -1136,12 +1311,13 @@ export const loadHasOne = async (scope, deps) => {
|
|
|
1136
1311
|
)
|
|
1137
1312
|
|
|
1138
1313
|
// Set single object (not array) for hasOne
|
|
1314
|
+
const relatedRecordId = getFieldValueForScope(scopes, targetScope, relatedRecord, 'id')
|
|
1139
1315
|
record[RELATIONSHIPS_KEY][includeName] = {
|
|
1140
|
-
data: { type: targetScope, id: String(
|
|
1316
|
+
data: { type: targetScope, id: String(relatedRecordId) }
|
|
1141
1317
|
}
|
|
1142
1318
|
|
|
1143
1319
|
// Add to included if not already there
|
|
1144
|
-
const key = `${targetScope}:${
|
|
1320
|
+
const key = `${targetScope}:${relatedRecordId}`
|
|
1145
1321
|
if (!included.has(key)) {
|
|
1146
1322
|
included.set(key, transformed)
|
|
1147
1323
|
}
|
|
@@ -1153,7 +1329,7 @@ export const loadHasOne = async (scope, deps) => {
|
|
|
1153
1329
|
processedPaths.add(newPath)
|
|
1154
1330
|
await processIncludes(
|
|
1155
1331
|
{ records: [relatedRecord], scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: newPath, fields },
|
|
1156
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1332
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1157
1333
|
)
|
|
1158
1334
|
}
|
|
1159
1335
|
}
|
|
@@ -1187,7 +1363,7 @@ export const loadHasOne = async (scope, deps) => {
|
|
|
1187
1363
|
*/
|
|
1188
1364
|
export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
1189
1365
|
const { records, scopeName, relName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
|
|
1190
|
-
const { scopes, log, knex, capabilities } = deps.context
|
|
1366
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
1191
1367
|
try {
|
|
1192
1368
|
log.trace('[INCLUDE] Loading polymorphic belongsTo:', {
|
|
1193
1369
|
relName,
|
|
@@ -1200,7 +1376,8 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1200
1376
|
const grouped = groupByPolymorphicType(
|
|
1201
1377
|
records,
|
|
1202
1378
|
typeField,
|
|
1203
|
-
idField
|
|
1379
|
+
idField,
|
|
1380
|
+
(record, fieldName) => getFieldValueForScope(scopes, scopeName, record, fieldName)
|
|
1204
1381
|
)
|
|
1205
1382
|
|
|
1206
1383
|
log.trace('[INCLUDE] Grouped by type:', {
|
|
@@ -1224,11 +1401,11 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1224
1401
|
|
|
1225
1402
|
// Build field selection for sparse fieldsets
|
|
1226
1403
|
const targetScopeObject = scopes[targetType]
|
|
1227
|
-
// const targetIdProperty = targetScopeObject.vars.schemaInfo.idProperty;
|
|
1228
1404
|
const storageAdapter = targetScopeObject.vars.storageAdapter
|
|
1229
1405
|
const fieldSelectionInfo = fields?.[targetType]
|
|
1230
1406
|
? await buildFieldSelection(targetScopeObject, {
|
|
1231
1407
|
context: {
|
|
1408
|
+
...(requestContext || {}),
|
|
1232
1409
|
scopeName: targetType,
|
|
1233
1410
|
queryParams: { fields: { [targetType]: fields[targetType] } },
|
|
1234
1411
|
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
@@ -1243,15 +1420,39 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1243
1420
|
})
|
|
1244
1421
|
|
|
1245
1422
|
// Query for this type
|
|
1246
|
-
const
|
|
1247
|
-
let query = knex(targetTable).whereIn(
|
|
1423
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetType)
|
|
1424
|
+
let query = knex(targetTable).whereIn(targetIdColumn, targetIds)
|
|
1248
1425
|
if (fieldSelectionInfo) {
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1426
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
1427
|
+
query,
|
|
1428
|
+
scope: targetScopeObject,
|
|
1429
|
+
fieldSelectionInfo,
|
|
1430
|
+
tableName: targetTable,
|
|
1431
|
+
useTablePrefix: false,
|
|
1432
|
+
storageAdapter,
|
|
1433
|
+
db: knex,
|
|
1434
|
+
context: {
|
|
1435
|
+
...(requestContext || {}),
|
|
1436
|
+
scopeName: targetType,
|
|
1437
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
1438
|
+
storageAdapter,
|
|
1439
|
+
},
|
|
1440
|
+
scopeName: targetType
|
|
1441
|
+
})
|
|
1442
|
+
query = selectionState.query
|
|
1443
|
+
} else if (targetIdColumn !== 'id') {
|
|
1444
|
+
// If no field selection but the storage ID column is not `id`, alias it for response shaping.
|
|
1445
|
+
query = query.select('*', `${targetIdColumn} as id`)
|
|
1254
1446
|
}
|
|
1447
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
1448
|
+
query,
|
|
1449
|
+
scopes,
|
|
1450
|
+
scopeName: targetType,
|
|
1451
|
+
requestContext,
|
|
1452
|
+
db: knex,
|
|
1453
|
+
tableName: targetTable
|
|
1454
|
+
})
|
|
1455
|
+
query = scopedQueryState.query
|
|
1255
1456
|
const targetRecords = await query
|
|
1256
1457
|
|
|
1257
1458
|
// Load relationship metadata for all target records
|
|
@@ -1260,13 +1461,13 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1260
1461
|
// Create lookup map
|
|
1261
1462
|
const targetById = {}
|
|
1262
1463
|
targetRecords.forEach(record => {
|
|
1263
|
-
targetById[record
|
|
1464
|
+
targetById[getFieldValueForScope(scopes, targetType, record, 'id')] = record
|
|
1264
1465
|
})
|
|
1265
1466
|
|
|
1266
1467
|
// Add to included and set relationships
|
|
1267
1468
|
records.forEach(record => {
|
|
1268
|
-
if (record
|
|
1269
|
-
const targetId = record
|
|
1469
|
+
if (getFieldValueForScope(scopes, scopeName, record, typeField) === targetType) {
|
|
1470
|
+
const targetId = getFieldValueForScope(scopes, scopeName, record, idField)
|
|
1270
1471
|
const targetRecord = targetById[targetId]
|
|
1271
1472
|
|
|
1272
1473
|
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
@@ -1301,12 +1502,8 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1301
1502
|
}
|
|
1302
1503
|
|
|
1303
1504
|
// Add links if urlPrefix is configured
|
|
1304
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
1305
1505
|
if (scopeName) {
|
|
1306
|
-
relationshipObject.links =
|
|
1307
|
-
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${relName}`,
|
|
1308
|
-
related: `${urlPrefix}/${scopeName}/${record.id}/${relName}`
|
|
1309
|
-
}
|
|
1506
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, relName)
|
|
1310
1507
|
}
|
|
1311
1508
|
|
|
1312
1509
|
record[RELATIONSHIPS_KEY][relName] = relationshipObject
|
|
@@ -1314,12 +1511,8 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1314
1511
|
const relationshipObject = { data: null }
|
|
1315
1512
|
|
|
1316
1513
|
// Add links if urlPrefix is configured
|
|
1317
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
1318
1514
|
if (scopeName) {
|
|
1319
|
-
relationshipObject.links =
|
|
1320
|
-
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${relName}`,
|
|
1321
|
-
related: `${urlPrefix}/${scopeName}/${record.id}/${relName}`
|
|
1322
|
-
}
|
|
1515
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, relName)
|
|
1323
1516
|
}
|
|
1324
1517
|
|
|
1325
1518
|
record[RELATIONSHIPS_KEY][relName] = relationshipObject
|
|
@@ -1332,8 +1525,8 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1332
1525
|
const nextPath = `${currentPath}.${relName}`
|
|
1333
1526
|
if (!processedPaths.has(nextPath)) {
|
|
1334
1527
|
await processIncludes(
|
|
1335
|
-
{ records: targetRecords, scopeName: targetType, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields
|
|
1336
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1528
|
+
{ records: targetRecords, scopeName: targetType, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
|
|
1529
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1337
1530
|
)
|
|
1338
1531
|
}
|
|
1339
1532
|
}
|
|
@@ -1346,12 +1539,8 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1346
1539
|
const relationshipObject = { data: null }
|
|
1347
1540
|
|
|
1348
1541
|
// Add links if urlPrefix is configured
|
|
1349
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
1350
1542
|
if (scopeName) {
|
|
1351
|
-
relationshipObject.links =
|
|
1352
|
-
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${relName}`,
|
|
1353
|
-
related: `${urlPrefix}/${scopeName}/${record.id}/${relName}`
|
|
1354
|
-
}
|
|
1543
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, relName)
|
|
1355
1544
|
}
|
|
1356
1545
|
|
|
1357
1546
|
record[RELATIONSHIPS_KEY][relName] = relationshipObject
|
|
@@ -1401,7 +1590,7 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1401
1590
|
*/
|
|
1402
1591
|
export const loadReversePolymorphic = async (scope, deps) => {
|
|
1403
1592
|
const { records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
|
|
1404
|
-
const { scopes, log, knex, capabilities } = deps.context
|
|
1593
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
1405
1594
|
try {
|
|
1406
1595
|
log.trace('[INCLUDE] Loading reverse polymorphic (via):', {
|
|
1407
1596
|
scopeName,
|
|
@@ -1429,7 +1618,7 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1429
1618
|
const targetTable = scopes[targetScope].vars.schemaInfo.tableName
|
|
1430
1619
|
|
|
1431
1620
|
// Collect parent IDs
|
|
1432
|
-
const parentIds = records.map(
|
|
1621
|
+
const parentIds = records.map(record => getFieldValueForScope(scopes, scopeName, record, 'id')).filter(Boolean)
|
|
1433
1622
|
if (parentIds.length === 0) return
|
|
1434
1623
|
|
|
1435
1624
|
log.debug(`[INCLUDE] Loading ${targetScope} records via ${viaRelName}:`, {
|
|
@@ -1443,10 +1632,11 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1443
1632
|
const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance
|
|
1444
1633
|
const targetScopeObject = scopes[targetScope]
|
|
1445
1634
|
const storageAdapter = targetScopeObject.vars.storageAdapter
|
|
1446
|
-
const
|
|
1635
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
1447
1636
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
1448
1637
|
? await buildFieldSelection(targetScopeObject, {
|
|
1449
1638
|
context: {
|
|
1639
|
+
...(requestContext || {}),
|
|
1450
1640
|
scopeName: targetScope,
|
|
1451
1641
|
queryParams: { fields: { [targetScope]: fields[targetScope] } },
|
|
1452
1642
|
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
@@ -1456,18 +1646,45 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1456
1646
|
: null
|
|
1457
1647
|
|
|
1458
1648
|
// Query for records pointing back to our scope
|
|
1649
|
+
const typeColumn = translateColumnForScope(scopes, targetScope, typeField)
|
|
1650
|
+
const idColumn = translateColumnForScope(scopes, targetScope, idField)
|
|
1459
1651
|
let query = knex(targetTable)
|
|
1460
|
-
.where(
|
|
1461
|
-
.whereIn(
|
|
1652
|
+
.where(typeColumn, scopeName)
|
|
1653
|
+
.whereIn(idColumn, parentIds)
|
|
1462
1654
|
|
|
1463
1655
|
if (fieldSelectionInfo) {
|
|
1464
|
-
const
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1656
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
1657
|
+
query,
|
|
1658
|
+
scope: targetScopeObject,
|
|
1659
|
+
fieldSelectionInfo,
|
|
1660
|
+
tableName: targetTable,
|
|
1661
|
+
useTablePrefix: false,
|
|
1662
|
+
storageAdapter,
|
|
1663
|
+
db: knex,
|
|
1664
|
+
context: {
|
|
1665
|
+
...(requestContext || {}),
|
|
1666
|
+
scopeName: targetScope,
|
|
1667
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
1668
|
+
storageAdapter,
|
|
1669
|
+
},
|
|
1670
|
+
scopeName: targetScope
|
|
1671
|
+
})
|
|
1672
|
+
query = selectionState.query
|
|
1673
|
+
} else if (targetIdColumn !== 'id') {
|
|
1674
|
+
// If no field selection but the storage ID column is not `id`, alias it for response shaping.
|
|
1675
|
+
query = query.select('*', `${targetIdColumn} as id`)
|
|
1469
1676
|
}
|
|
1470
1677
|
|
|
1678
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
1679
|
+
query,
|
|
1680
|
+
scopes,
|
|
1681
|
+
scopeName: targetScope,
|
|
1682
|
+
requestContext,
|
|
1683
|
+
db: knex,
|
|
1684
|
+
tableName: targetTable
|
|
1685
|
+
})
|
|
1686
|
+
query = scopedQueryState.query
|
|
1687
|
+
|
|
1471
1688
|
// Apply include configuration (limits, ordering, etc.)
|
|
1472
1689
|
const targetVars = scopes[targetScope].vars
|
|
1473
1690
|
query = applyStandardIncludeConfig(
|
|
@@ -1489,7 +1706,7 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1489
1706
|
// Group by parent ID
|
|
1490
1707
|
const childrenByParent = {}
|
|
1491
1708
|
targetRecords.forEach(record => {
|
|
1492
|
-
const parentId = record
|
|
1709
|
+
const parentId = getFieldValueForScope(scopes, targetScope, record, idField)
|
|
1493
1710
|
if (!childrenByParent[parentId]) {
|
|
1494
1711
|
childrenByParent[parentId] = []
|
|
1495
1712
|
}
|
|
@@ -1500,10 +1717,12 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1500
1717
|
records.forEach(record => {
|
|
1501
1718
|
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
1502
1719
|
|
|
1503
|
-
const
|
|
1720
|
+
const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
|
|
1721
|
+
const children = childrenByParent[recordId] || []
|
|
1504
1722
|
const relData = children.map(childRecord => {
|
|
1505
1723
|
// Add to included
|
|
1506
|
-
const
|
|
1724
|
+
const childRecordId = getFieldValueForScope(scopes, targetScope, childRecord, 'id')
|
|
1725
|
+
const resourceKey = `${targetScope}:${childRecordId}`
|
|
1507
1726
|
if (!included.has(resourceKey)) {
|
|
1508
1727
|
const jsonApiRecord = toJsonApiRecord(
|
|
1509
1728
|
scopes[targetScope],
|
|
@@ -1525,18 +1744,14 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1525
1744
|
|
|
1526
1745
|
included.set(resourceKey, jsonApiRecord)
|
|
1527
1746
|
}
|
|
1528
|
-
return { type: targetScope, id: String(
|
|
1747
|
+
return { type: targetScope, id: String(childRecordId) }
|
|
1529
1748
|
})
|
|
1530
1749
|
|
|
1531
1750
|
const relationshipObject = { data: relData }
|
|
1532
1751
|
|
|
1533
1752
|
// Add links if urlPrefix is configured
|
|
1534
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
1535
1753
|
if (scopeName) {
|
|
1536
|
-
relationshipObject.links =
|
|
1537
|
-
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${includeName}`,
|
|
1538
|
-
related: `${urlPrefix}/${scopeName}/${record.id}/${includeName}`
|
|
1539
|
-
}
|
|
1754
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
1540
1755
|
}
|
|
1541
1756
|
|
|
1542
1757
|
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
@@ -1547,8 +1762,8 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1547
1762
|
const nextPath = `${currentPath}.${includeName}`
|
|
1548
1763
|
if (!processedPaths.has(nextPath)) {
|
|
1549
1764
|
await processIncludes(
|
|
1550
|
-
{ records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields
|
|
1551
|
-
{ context: { scopes, log, knex } }
|
|
1765
|
+
{ records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
|
|
1766
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1552
1767
|
)
|
|
1553
1768
|
}
|
|
1554
1769
|
}
|
|
@@ -1588,7 +1803,6 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1588
1803
|
* - processedPaths: Set - Set tracking processed paths to prevent cycles
|
|
1589
1804
|
* - currentPath: string - Current path in the include tree (default '')
|
|
1590
1805
|
* - fields: Object - Sparse fieldsets configuration (default {})
|
|
1591
|
-
* - idProperty: string - The ID property name
|
|
1592
1806
|
* @param {Object} deps - Dependencies object containing:
|
|
1593
1807
|
* - context.scopes: Object - The hooked-api scopes object
|
|
1594
1808
|
* - context.log: Object - Logger instance
|
|
@@ -1596,8 +1810,8 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1596
1810
|
* @returns {Promise<void>}
|
|
1597
1811
|
*/
|
|
1598
1812
|
export const processIncludes = async (scope, deps) => {
|
|
1599
|
-
const { records, scopeName, includeTree, included, processedPaths, currentPath = '', fields = {}
|
|
1600
|
-
const { scopes, log, knex, capabilities } = deps.context
|
|
1813
|
+
const { records, scopeName, includeTree, included, processedPaths, currentPath = '', fields = {} } = scope
|
|
1814
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
1601
1815
|
try {
|
|
1602
1816
|
log.trace('[INCLUDE] Processing includes:', {
|
|
1603
1817
|
scopeName,
|
|
@@ -1636,8 +1850,8 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1636
1850
|
for (const [fieldName, fieldDef] of Object.entries(schemaInstance.structure || {})) {
|
|
1637
1851
|
if (fieldDef.as === includeName && fieldDef.belongsTo) {
|
|
1638
1852
|
await loadBelongsTo(
|
|
1639
|
-
{ records, scopeName, fieldName, fieldDef, includeName, subIncludes, included, processedPaths, currentPath, fields
|
|
1640
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1853
|
+
{ records, scopeName, fieldName, fieldDef, includeName, subIncludes, included, processedPaths, currentPath, fields },
|
|
1854
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1641
1855
|
)
|
|
1642
1856
|
handled = true
|
|
1643
1857
|
break
|
|
@@ -1658,7 +1872,7 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1658
1872
|
})
|
|
1659
1873
|
await loadHasOne(
|
|
1660
1874
|
{ records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields },
|
|
1661
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1875
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1662
1876
|
)
|
|
1663
1877
|
handled = true
|
|
1664
1878
|
} else if (relDef.type === 'hasMany' || relDef.type === 'manyToMany') {
|
|
@@ -1666,7 +1880,7 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1666
1880
|
if (relDef.via) {
|
|
1667
1881
|
await loadReversePolymorphic(
|
|
1668
1882
|
{ records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields },
|
|
1669
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1883
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1670
1884
|
)
|
|
1671
1885
|
} else {
|
|
1672
1886
|
log.debug('[INCLUDE] About to call loadHasMany:', {
|
|
@@ -1678,14 +1892,14 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1678
1892
|
})
|
|
1679
1893
|
await loadHasMany(
|
|
1680
1894
|
{ records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields },
|
|
1681
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1895
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1682
1896
|
)
|
|
1683
1897
|
}
|
|
1684
1898
|
handled = true
|
|
1685
1899
|
} else if (relDef.belongsToPolymorphic) {
|
|
1686
1900
|
await loadPolymorphicBelongsTo(
|
|
1687
1901
|
{ records, scopeName, relName: includeName, relDef, subIncludes, included, processedPaths, currentPath, fields },
|
|
1688
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1902
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1689
1903
|
)
|
|
1690
1904
|
handled = true
|
|
1691
1905
|
}
|
|
@@ -1737,7 +1951,6 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1737
1951
|
* - scopeName: string - The scope name of the main resources
|
|
1738
1952
|
* - includeParam: string - The include parameter value (e.g., "author,comments.author")
|
|
1739
1953
|
* - fields: Object - Sparse fieldsets configuration
|
|
1740
|
-
* - idProperty: string - The ID property name
|
|
1741
1954
|
* @param {Object} deps - Dependencies object containing:
|
|
1742
1955
|
* - context.scopes: Object - The hooked-api scopes object
|
|
1743
1956
|
* - context.log: Object - Logger instance
|
|
@@ -1750,8 +1963,7 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1750
1963
|
* records: articleRecords,
|
|
1751
1964
|
* scopeName: 'articles',
|
|
1752
1965
|
* includeParam: 'author,comments.author',
|
|
1753
|
-
* fields: { articles: 'title,body', people: 'name' }
|
|
1754
|
-
* idProperty: 'id'
|
|
1966
|
+
* fields: { articles: 'title,body', people: 'name' }
|
|
1755
1967
|
* },
|
|
1756
1968
|
* { context: { scopes, log, knex } }
|
|
1757
1969
|
* );
|
|
@@ -1765,8 +1977,8 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1765
1977
|
* // result.recordsWithRelationships = original records with _relationships added
|
|
1766
1978
|
*/
|
|
1767
1979
|
export const buildIncludedResources = async (scope, deps) => {
|
|
1768
|
-
const { records, scopeName, includeParam, fields
|
|
1769
|
-
const { scopes, log, knex, capabilities } = deps.context
|
|
1980
|
+
const { records, scopeName, includeParam, fields } = scope
|
|
1981
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
1770
1982
|
try {
|
|
1771
1983
|
log.trace('[INCLUDE] Building included resources:', { scopeName, includeParam, recordCount: records.length })
|
|
1772
1984
|
|
|
@@ -1799,8 +2011,8 @@ export const buildIncludedResources = async (scope, deps) => {
|
|
|
1799
2011
|
|
|
1800
2012
|
// Process all includes
|
|
1801
2013
|
await processIncludes(
|
|
1802
|
-
{ records, scopeName, includeTree, included, processedPaths, currentPath: '', fields
|
|
1803
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
2014
|
+
{ records, scopeName, includeTree, included, processedPaths, currentPath: '', fields },
|
|
2015
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1804
2016
|
)
|
|
1805
2017
|
|
|
1806
2018
|
// Convert Map to array for JSON:API format
|
|
@@ -1873,7 +2085,7 @@ export const buildIncludedResources = async (scope, deps) => {
|
|
|
1873
2085
|
* @param {Object} knex - Knex instance for database queries
|
|
1874
2086
|
* @returns {Promise<void>} Modifies records in place by adding relationship data
|
|
1875
2087
|
*/
|
|
1876
|
-
export const loadRelationshipIdentifiers = async (records, scopeName, scopes, knex) => {
|
|
2088
|
+
export const loadRelationshipIdentifiers = async (records, scopeName, scopes, knex, requestContext = null) => {
|
|
1877
2089
|
if (!records.length) return
|
|
1878
2090
|
|
|
1879
2091
|
const schemaInfo = scopes[scopeName]?.vars?.schemaInfo
|
|
@@ -1896,14 +2108,27 @@ export const loadRelationshipIdentifiers = async (records, scopeName, scopes, kn
|
|
|
1896
2108
|
}
|
|
1897
2109
|
const targetScope = scopes[relDef.target]
|
|
1898
2110
|
const targetTable = targetScope?.vars?.schemaInfo?.tableName || relDef.target
|
|
1899
|
-
const
|
|
2111
|
+
const targetIdColumn = getIdColumnForScope(scopes, relDef.target)
|
|
2112
|
+
const foreignKeyColumn = translateColumnForScope(scopes, relDef.target, foreignKey)
|
|
2113
|
+
|
|
2114
|
+
let query = knex(targetTable)
|
|
2115
|
+
.whereIn(foreignKeyColumn, recordIds)
|
|
2116
|
+
.select(targetIdColumn !== 'id' ? `${targetIdColumn} as id` : 'id', foreignKeyColumn)
|
|
2117
|
+
|
|
2118
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
2119
|
+
query,
|
|
2120
|
+
scopes,
|
|
2121
|
+
scopeName: relDef.target,
|
|
2122
|
+
requestContext,
|
|
2123
|
+
db: knex,
|
|
2124
|
+
tableName: targetTable
|
|
2125
|
+
})
|
|
2126
|
+
query = scopedQueryState.query
|
|
1900
2127
|
|
|
1901
|
-
const results = await
|
|
1902
|
-
.whereIn(foreignKey, recordIds)
|
|
1903
|
-
.select(targetIdProperty !== 'id' ? `${targetIdProperty} as id` : 'id', foreignKey)
|
|
2128
|
+
const results = await query
|
|
1904
2129
|
|
|
1905
2130
|
results.forEach(row => {
|
|
1906
|
-
const parentId = String(row[
|
|
2131
|
+
const parentId = String(row[foreignKeyColumn])
|
|
1907
2132
|
if (!idsMap[parentId]) idsMap[parentId] = []
|
|
1908
2133
|
idsMap[parentId].push(String(row.id))
|
|
1909
2134
|
})
|
|
@@ -1922,13 +2147,34 @@ export const loadRelationshipIdentifiers = async (records, scopeName, scopes, kn
|
|
|
1922
2147
|
const pivotScope = scopes[through]
|
|
1923
2148
|
const pivotTable = pivotScope?.vars?.schemaInfo?.tableName || through
|
|
1924
2149
|
|
|
1925
|
-
const
|
|
1926
|
-
|
|
1927
|
-
|
|
2150
|
+
const targetScopeName = relDef.target || relName
|
|
2151
|
+
const targetTable = scopes[targetScopeName]?.vars?.schemaInfo?.tableName || targetScopeName
|
|
2152
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetScopeName)
|
|
2153
|
+
let query = knex(targetTable)
|
|
2154
|
+
.join(`${pivotTable} as pivot`, `${targetTable}.${targetIdColumn}`, `pivot.${ok}`)
|
|
2155
|
+
.whereIn(`pivot.${fk}`, recordIds)
|
|
2156
|
+
.select(
|
|
2157
|
+
`pivot.${fk} as __parent_id`,
|
|
2158
|
+
targetIdColumn !== 'id'
|
|
2159
|
+
? `${targetTable}.${targetIdColumn} as id`
|
|
2160
|
+
: `${targetTable}.id as id`
|
|
2161
|
+
)
|
|
2162
|
+
|
|
2163
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
2164
|
+
query,
|
|
2165
|
+
scopes,
|
|
2166
|
+
scopeName: targetScopeName,
|
|
2167
|
+
requestContext,
|
|
2168
|
+
db: knex,
|
|
2169
|
+
tableName: targetTable
|
|
2170
|
+
})
|
|
2171
|
+
query = scopedQueryState.query
|
|
2172
|
+
|
|
2173
|
+
const results = await query
|
|
1928
2174
|
|
|
1929
2175
|
results.forEach(row => {
|
|
1930
|
-
const parentId = String(row
|
|
1931
|
-
const childId = String(row
|
|
2176
|
+
const parentId = String(row.__parent_id)
|
|
2177
|
+
const childId = String(row.id)
|
|
1932
2178
|
if (!idsMap[parentId]) idsMap[parentId] = []
|
|
1933
2179
|
idsMap[parentId].push(childId)
|
|
1934
2180
|
})
|
|
@@ -1944,15 +2190,29 @@ export const loadRelationshipIdentifiers = async (records, scopeName, scopes, kn
|
|
|
1944
2190
|
|
|
1945
2191
|
// Get the actual table name
|
|
1946
2192
|
const targetTable = scopes[targetScope]?.vars?.schemaInfo?.tableName || targetScope
|
|
1947
|
-
const
|
|
2193
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
2194
|
+
const typeColumn = translateColumnForScope(scopes, targetScope, typeField)
|
|
2195
|
+
const idColumn = translateColumnForScope(scopes, targetScope, idField)
|
|
2196
|
+
|
|
2197
|
+
let query = knex(targetTable)
|
|
2198
|
+
.where(typeColumn, scopeName)
|
|
2199
|
+
.whereIn(idColumn, recordIds)
|
|
2200
|
+
.select(targetIdColumn !== 'id' ? `${targetIdColumn} as id` : 'id', idColumn)
|
|
2201
|
+
|
|
2202
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
2203
|
+
query,
|
|
2204
|
+
scopes,
|
|
2205
|
+
scopeName: targetScope,
|
|
2206
|
+
requestContext,
|
|
2207
|
+
db: knex,
|
|
2208
|
+
tableName: targetTable
|
|
2209
|
+
})
|
|
2210
|
+
query = scopedQueryState.query
|
|
1948
2211
|
|
|
1949
|
-
const results = await
|
|
1950
|
-
.where(typeField, scopeName)
|
|
1951
|
-
.whereIn(idField, recordIds)
|
|
1952
|
-
.select(targetIdProperty !== 'id' ? `${targetIdProperty} as id` : 'id', idField)
|
|
2212
|
+
const results = await query
|
|
1953
2213
|
|
|
1954
2214
|
results.forEach(row => {
|
|
1955
|
-
const parentId = String(row[
|
|
2215
|
+
const parentId = String(row[idColumn])
|
|
1956
2216
|
if (!idsMap[parentId]) idsMap[parentId] = []
|
|
1957
2217
|
idsMap[parentId].push(String(row.id))
|
|
1958
2218
|
})
|
|
@@ -1966,7 +2226,7 @@ export const loadRelationshipIdentifiers = async (records, scopeName, scopes, kn
|
|
|
1966
2226
|
record[RELATIONSHIPS_KEY] = {}
|
|
1967
2227
|
}
|
|
1968
2228
|
|
|
1969
|
-
const ids = idsMap[String(record
|
|
2229
|
+
const ids = idsMap[String(getFieldValueForScope(scopes, scopeName, record, 'id'))] || []
|
|
1970
2230
|
// For manyToMany, the target type is the relationship name itself
|
|
1971
2231
|
// For hasMany/hasOne, the target type is specified in the target property
|
|
1972
2232
|
const targetType = (relDef.type === 'manyToMany') ? relName : relDef.target
|