json-rest-api 1.0.22 → 1.0.24
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/README.md +22 -1
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
- package/docs/API.md +196 -38
- package/docs/COMPARISON.md +2 -2
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +111 -5
- 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 +370 -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/lib/enhanced-logger.js +1 -1
- package/package.json +6 -2
- package/plugins/core/bulk-operations-plugin.js +26 -34
- package/plugins/core/connectors/express-plugin.js +97 -190
- package/plugins/core/connectors/fastify-plugin.js +206 -0
- package/plugins/core/connectors/lib/busboy-detector.js +4 -4
- 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/anyapi/schema-utils.js +43 -4
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +4 -1
- package/plugins/core/lib/dbIntrospection.js +1078 -0
- package/plugins/core/lib/dbTablesOperations.js +1111 -160
- package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -3
- package/plugins/core/lib/querying/knex-process-includes.js +3 -4
- package/plugins/core/lib/querying/knex-query-helpers.js +21 -76
- package/plugins/core/lib/querying/knex-relationship-includes.js +468 -215
- 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 +20 -5
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -14
- 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/querying-writing/resource-id-normalization.js +131 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +0 -2
- package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
- package/plugins/core/lib/storage/canonical-storage-mapping.js +133 -0
- package/plugins/core/lib/storage/storage-adapter.js +82 -4
- package/plugins/core/lib/storage/storage-mapping.js +274 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -2
- package/plugins/core/rest-api-anyapi-knex-plugin.js +531 -312
- package/plugins/core/rest-api-autofilter-plugin.js +414 -0
- package/plugins/core/rest-api-knex-plugin.js +271 -226
- 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-hooks/turn-scope-init-into-vars.js +1 -0
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
- package/plugins/core/rest-api-plugin-methods/common.js +148 -92
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +11 -2
- package/plugins/core/rest-api-plugin-methods/delete.js +6 -1
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +7 -2
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +7 -2
- package/plugins/core/rest-api-plugin-methods/get.js +29 -14
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +10 -3
- package/plugins/core/rest-api-plugin-methods/patch.js +56 -12
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +11 -2
- package/plugins/core/rest-api-plugin-methods/post.js +21 -8
- package/plugins/core/rest-api-plugin-methods/put.js +73 -21
- package/plugins/core/rest-api-plugin-methods/query.js +15 -53
- package/plugins/core/rest-api-plugin-methods/release.js +0 -2
- package/plugins/core/rest-api-plugin.js +6 -0
- package/plugins/core/rest-api-positioning-plugin.js +2 -5
- package/plugins/core/rest-api-query-projections-plugin.js +130 -0
- package/plugins/core/socketio-plugin.js +1 -4
- package/plugins/storage/local-storage.js +1 -1
- package/plugins/storage/s3-storage.js +5 -2
- package/quickTest.js +1 -0
- package/scripts/run-docs.js +80 -84
- package/test-debug.js +1 -1
- package/tests/README.md +7 -0
- package/tests/anyapi-basic.test.js +3 -0
- package/tests/autofilter.test.js +463 -0
- package/tests/custom-id-mysql-returning-fallback.test.js +128 -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 +521 -0
- package/tests/db-table-operations.test.js +786 -0
- package/tests/fastify-plugin.test.js +156 -0
- package/tests/field-getters.test.js +1 -2
- package/tests/field-setters.test.js +0 -4
- package/tests/fixtures/api-configs.js +278 -176
- package/tests/helpers/fake-fastify.js +99 -0
- package/tests/http-connectors-parity.test.js +235 -0
- package/tests/id-normalization.test.js +444 -0
- package/tests/logical-id-write-validation.test.js +151 -0
- package/tests/pagination-cursor-multifield.test.js +11 -11
- 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 +259 -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,108 @@
|
|
|
74
74
|
* 6. Return array of unique included resources
|
|
75
75
|
*/
|
|
76
76
|
|
|
77
|
-
import { buildFieldSelection } from '../querying-writing/knex-field-helpers.js'
|
|
78
|
-
import { getForeignKeyFields } from '../querying-writing/field-utils.js'
|
|
77
|
+
import { applyFieldSelectionToQuery, buildFieldSelection } from '../querying-writing/knex-field-helpers.js'
|
|
79
78
|
import { toJsonApiRecord } from './knex-json-api-transformers-querying.js'
|
|
80
79
|
import { buildWindowedIncludeQuery, applyStandardIncludeConfig, buildOrderByClause } from './knex-window-queries.js'
|
|
80
|
+
import { unwrapQueryBuilderState } from './query-builder-utils.js'
|
|
81
81
|
import { RELATIONSHIPS_KEY, RELATIONSHIP_METADATA_KEY, ROW_NUMBER_KEY, COMPUTED_DEPENDENCIES_KEY, DEFAULT_QUERY_LIMIT } from '../querying-writing/knex-constants.js'
|
|
82
82
|
import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
|
|
83
|
+
import {
|
|
84
|
+
getFieldValue as getStorageFieldValue,
|
|
85
|
+
getIdColumn as getStorageIdColumn,
|
|
86
|
+
getStorageColumn as getMappedStorageColumn,
|
|
87
|
+
} from '../storage/storage-mapping.js'
|
|
88
|
+
|
|
89
|
+
const getScopeStorageAdapter = (scopes, scopeName) => scopes[scopeName]?.vars?.storageAdapter || null
|
|
90
|
+
|
|
91
|
+
const getIdColumnForScope = (scopes, scopeName) => {
|
|
92
|
+
const adapter = getScopeStorageAdapter(scopes, scopeName)
|
|
93
|
+
if (adapter?.getIdColumn) {
|
|
94
|
+
return adapter.getIdColumn()
|
|
95
|
+
}
|
|
83
96
|
|
|
84
|
-
|
|
85
|
-
|
|
97
|
+
return getStorageIdColumn(scopes[scopeName]?.vars?.schemaInfo || {})
|
|
98
|
+
}
|
|
86
99
|
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
const getFieldValueForScope = (scopes, scopeName, record, fieldName) => {
|
|
101
|
+
const adapter = getScopeStorageAdapter(scopes, scopeName)
|
|
102
|
+
if (adapter?.getFieldValue) {
|
|
103
|
+
return adapter.getFieldValue(record, fieldName)
|
|
104
|
+
}
|
|
89
105
|
|
|
90
|
-
|
|
106
|
+
return getStorageFieldValue(record, scopes[scopeName]?.vars?.schemaInfo || {}, fieldName)
|
|
107
|
+
}
|
|
91
108
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
const translateColumnForScope = (scopes, scopeName, fieldName) => {
|
|
110
|
+
const adapter = getScopeStorageAdapter(scopes, scopeName)
|
|
111
|
+
if (adapter?.translateColumn) {
|
|
112
|
+
return adapter.translateColumn(fieldName)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return getMappedStorageColumn(scopes[scopeName]?.vars?.schemaInfo || {}, fieldName)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const buildRelationshipLinks = (scopes, scopeName, record, relationshipName) => {
|
|
119
|
+
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
120
|
+
const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
self: `${urlPrefix}/${scopeName}/${recordId}/relationships/${relationshipName}`,
|
|
124
|
+
related: `${urlPrefix}/${scopeName}/${recordId}/${relationshipName}`
|
|
125
|
+
}
|
|
126
|
+
}
|
|
98
127
|
|
|
99
|
-
|
|
100
|
-
|
|
128
|
+
const buildScopedIncludeContext = ({ requestContext, scopeName, scopeObject, storageAdapter }) => {
|
|
129
|
+
const queryParams = requestContext?.queryParams
|
|
130
|
+
? { ...requestContext.queryParams }
|
|
131
|
+
: {}
|
|
132
|
+
|
|
133
|
+
if ('filters' in queryParams) {
|
|
134
|
+
delete queryParams.filters
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
...(requestContext || {}),
|
|
139
|
+
scopeName,
|
|
140
|
+
schemaInfo: scopeObject.vars.schemaInfo,
|
|
141
|
+
storageAdapter,
|
|
142
|
+
queryParams
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const applyScopeFiltersToIncludeQuery = async ({
|
|
147
|
+
query,
|
|
148
|
+
scopes,
|
|
149
|
+
scopeName,
|
|
150
|
+
requestContext,
|
|
151
|
+
db,
|
|
152
|
+
tableName
|
|
153
|
+
}) => {
|
|
154
|
+
const scopeObject = scopes[scopeName]
|
|
155
|
+
if (!scopeObject?.applyQueryFilters) {
|
|
156
|
+
return { query }
|
|
101
157
|
}
|
|
102
158
|
|
|
103
|
-
|
|
159
|
+
const storageAdapter = scopeObject.vars.storageAdapter
|
|
160
|
+
|
|
161
|
+
const queryState = await scopeObject.applyQueryFilters({
|
|
162
|
+
query,
|
|
163
|
+
filters: undefined,
|
|
164
|
+
scopeName,
|
|
165
|
+
tableName: tableName || scopeObject.vars.schemaInfo.tableName,
|
|
166
|
+
db,
|
|
167
|
+
schemaInfo: scopeObject.vars.schemaInfo,
|
|
168
|
+
storageAdapter
|
|
169
|
+
}, buildScopedIncludeContext({
|
|
170
|
+
requestContext,
|
|
171
|
+
scopeName,
|
|
172
|
+
scopeObject,
|
|
173
|
+
storageAdapter
|
|
174
|
+
}))
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
query: unwrapQueryBuilderState(queryState, query)
|
|
178
|
+
}
|
|
104
179
|
}
|
|
105
180
|
|
|
106
181
|
/**
|
|
@@ -162,12 +237,12 @@ const translateSelectFieldsForAdapter = (fields, adapter) => {
|
|
|
162
237
|
* 3. Collects unique IDs for each type
|
|
163
238
|
* 4. Returns map used for batch WHERE IN queries
|
|
164
239
|
*/
|
|
165
|
-
export const groupByPolymorphicType = (records, typeField, idField) => {
|
|
240
|
+
export const groupByPolymorphicType = (records, typeField, idField, getFieldValue = (record, fieldName) => record[fieldName]) => {
|
|
166
241
|
const grouped = {}
|
|
167
242
|
|
|
168
243
|
records.forEach(record => {
|
|
169
|
-
const type = record
|
|
170
|
-
const id = record
|
|
244
|
+
const type = getFieldValue(record, typeField)
|
|
245
|
+
const id = getFieldValue(record, idField)
|
|
171
246
|
|
|
172
247
|
// Skip if either type or id is missing
|
|
173
248
|
if (!type || !id) return
|
|
@@ -295,7 +370,7 @@ const loadRelationshipMetadata = async (scopes, records, scopeName) => {
|
|
|
295
370
|
// Process belongsTo relationships from schema
|
|
296
371
|
for (const [fieldName, fieldDef] of Object.entries(schema)) {
|
|
297
372
|
if (fieldDef.belongsTo && fieldDef.as) {
|
|
298
|
-
const foreignKeyValue = record
|
|
373
|
+
const foreignKeyValue = getFieldValueForScope(scopes, scopeName, record, fieldName)
|
|
299
374
|
if (foreignKeyValue != null) {
|
|
300
375
|
record[RELATIONSHIP_METADATA_KEY][fieldDef.as] = {
|
|
301
376
|
data: {
|
|
@@ -320,8 +395,8 @@ const loadRelationshipMetadata = async (scopes, records, scopeName) => {
|
|
|
320
395
|
} else if (relDef.belongsToPolymorphic) {
|
|
321
396
|
// Process polymorphic relationships defined in relationships section
|
|
322
397
|
const { typeField, idField } = relDef.belongsToPolymorphic
|
|
323
|
-
const type = record
|
|
324
|
-
const id = record
|
|
398
|
+
const type = getFieldValueForScope(scopes, scopeName, record, typeField)
|
|
399
|
+
const id = getFieldValueForScope(scopes, scopeName, record, idField)
|
|
325
400
|
|
|
326
401
|
if (type && id) {
|
|
327
402
|
record[RELATIONSHIP_METADATA_KEY][relName] = {
|
|
@@ -361,7 +436,6 @@ const loadRelationshipMetadata = async (scopes, records, scopeName) => {
|
|
|
361
436
|
* - processedPaths: Set - Set of already processed paths
|
|
362
437
|
* - currentPath: string - Current include path for tracking
|
|
363
438
|
* - fields: Object - Sparse fieldsets configuration
|
|
364
|
-
* - idProperty: string - The ID property name
|
|
365
439
|
* @param {Object} deps - Dependencies object containing:
|
|
366
440
|
* - context.scopes: Object - The hooked-api scopes object
|
|
367
441
|
* - context.log: Object - Logger instance
|
|
@@ -369,8 +443,8 @@ const loadRelationshipMetadata = async (scopes, records, scopeName) => {
|
|
|
369
443
|
* @returns {Promise<void>}
|
|
370
444
|
*/
|
|
371
445
|
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
|
|
446
|
+
const { records, scopeName, fieldName, fieldDef, includeName, subIncludes, included, processedPaths, currentPath, fields } = scope
|
|
447
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
374
448
|
try {
|
|
375
449
|
log.trace('[INCLUDE] Loading belongsTo:', {
|
|
376
450
|
fieldName,
|
|
@@ -387,7 +461,7 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
387
461
|
|
|
388
462
|
// Collect all foreign key values
|
|
389
463
|
const foreignKeyValues = records
|
|
390
|
-
.map(
|
|
464
|
+
.map(record => getFieldValueForScope(scopes, scopeName, record, fieldName))
|
|
391
465
|
.filter(val => val != null) // Filter out null/undefined
|
|
392
466
|
|
|
393
467
|
const uniqueIds = [...new Set(foreignKeyValues)]
|
|
@@ -404,12 +478,8 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
404
478
|
const relationshipObject = { data: null }
|
|
405
479
|
|
|
406
480
|
// Add links if urlPrefix is configured
|
|
407
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
408
481
|
if (scopeName) {
|
|
409
|
-
relationshipObject.links =
|
|
410
|
-
self: `${urlPrefix}/${scopeName}/${record[idProperty]}/relationships/${includeName}`,
|
|
411
|
-
related: `${urlPrefix}/${scopeName}/${record[idProperty]}/${includeName}`
|
|
412
|
-
}
|
|
482
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
413
483
|
}
|
|
414
484
|
|
|
415
485
|
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
@@ -419,15 +489,14 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
419
489
|
|
|
420
490
|
// Get target table info
|
|
421
491
|
const targetTableName = scopes[targetScope].vars.schemaInfo.tableName
|
|
422
|
-
const
|
|
423
|
-
const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance
|
|
424
|
-
|
|
492
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
425
493
|
// Build field selection for sparse fieldsets
|
|
426
494
|
const targetScopeObject = scopes[targetScope]
|
|
427
495
|
const storageAdapter = targetScopeObject.vars.storageAdapter
|
|
428
496
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
429
497
|
? await buildFieldSelection(targetScopeObject, {
|
|
430
498
|
context: {
|
|
499
|
+
...(requestContext || {}),
|
|
431
500
|
scopeName: targetScope,
|
|
432
501
|
queryParams: { fields: { [targetScope]: fields[targetScope] } },
|
|
433
502
|
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
@@ -437,14 +506,38 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
437
506
|
: null
|
|
438
507
|
|
|
439
508
|
// Load the target records
|
|
440
|
-
let query = knex(targetTableName).whereIn(
|
|
509
|
+
let query = knex(targetTableName).whereIn(targetIdColumn, uniqueIds)
|
|
441
510
|
if (fieldSelectionInfo) {
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
511
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
512
|
+
query,
|
|
513
|
+
scope: targetScopeObject,
|
|
514
|
+
fieldSelectionInfo,
|
|
515
|
+
tableName: targetTableName,
|
|
516
|
+
useTablePrefix: false,
|
|
517
|
+
storageAdapter,
|
|
518
|
+
db: knex,
|
|
519
|
+
context: {
|
|
520
|
+
...(requestContext || {}),
|
|
521
|
+
scopeName: targetScope,
|
|
522
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
523
|
+
storageAdapter,
|
|
524
|
+
},
|
|
525
|
+
scopeName: targetScope
|
|
526
|
+
})
|
|
527
|
+
query = selectionState.query
|
|
528
|
+
} else if (targetIdColumn !== 'id') {
|
|
529
|
+
// If no field selection but the storage ID column is not `id`, alias it for response shaping.
|
|
530
|
+
query = query.select('*', `${targetIdColumn} as id`)
|
|
447
531
|
}
|
|
532
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
533
|
+
query,
|
|
534
|
+
scopes,
|
|
535
|
+
scopeName: targetScope,
|
|
536
|
+
requestContext,
|
|
537
|
+
db: knex,
|
|
538
|
+
tableName: targetTableName
|
|
539
|
+
})
|
|
540
|
+
query = scopedQueryState.query
|
|
448
541
|
const targetRecords = await query
|
|
449
542
|
|
|
450
543
|
// Load relationship metadata for all target records
|
|
@@ -453,7 +546,7 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
453
546
|
// Create lookup map
|
|
454
547
|
const targetById = {}
|
|
455
548
|
targetRecords.forEach(record => {
|
|
456
|
-
targetById[
|
|
549
|
+
targetById[getFieldValueForScope(scopes, targetScope, record, 'id')] = record
|
|
457
550
|
})
|
|
458
551
|
|
|
459
552
|
// Set relationships on original records
|
|
@@ -462,7 +555,7 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
462
555
|
records.forEach(record => {
|
|
463
556
|
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
464
557
|
|
|
465
|
-
const targetId = record
|
|
558
|
+
const targetId = getFieldValueForScope(scopes, scopeName, record, fieldName)
|
|
466
559
|
const targetRecord = targetById[targetId]
|
|
467
560
|
|
|
468
561
|
if (targetRecord) {
|
|
@@ -488,12 +581,8 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
488
581
|
const relationshipObject = { data: { type: targetScope, id: String(targetId) } }
|
|
489
582
|
|
|
490
583
|
// Add links if urlPrefix is configured
|
|
491
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
492
584
|
if (scopeName) {
|
|
493
|
-
relationshipObject.links =
|
|
494
|
-
self: `${urlPrefix}/${scopeName}/${record[idProperty]}/relationships/${includeName}`,
|
|
495
|
-
related: `${urlPrefix}/${scopeName}/${record[idProperty]}/${includeName}`
|
|
496
|
-
}
|
|
585
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
497
586
|
}
|
|
498
587
|
|
|
499
588
|
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
@@ -512,12 +601,8 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
512
601
|
const relationshipObject = { data: null }
|
|
513
602
|
|
|
514
603
|
// Add links if urlPrefix is configured
|
|
515
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
516
604
|
if (scopeName) {
|
|
517
|
-
relationshipObject.links =
|
|
518
|
-
self: `${urlPrefix}/${scopeName}/${record[idProperty]}/relationships/${includeName}`,
|
|
519
|
-
related: `${urlPrefix}/${scopeName}/${record[idProperty]}/${includeName}`
|
|
520
|
-
}
|
|
605
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
521
606
|
}
|
|
522
607
|
|
|
523
608
|
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
@@ -529,8 +614,8 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
529
614
|
const nextPath = `${currentPath}.${includeName}`
|
|
530
615
|
if (!processedPaths.has(nextPath)) {
|
|
531
616
|
await processIncludes(
|
|
532
|
-
{ records: targetRecordsToProcess, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields
|
|
533
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
617
|
+
{ records: targetRecordsToProcess, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
|
|
618
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
534
619
|
)
|
|
535
620
|
}
|
|
536
621
|
}
|
|
@@ -580,7 +665,7 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
580
665
|
*/
|
|
581
666
|
export const loadHasMany = async (scope, deps) => {
|
|
582
667
|
const { records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
|
|
583
|
-
const { scopes, log, knex, capabilities } = deps.context
|
|
668
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
584
669
|
try {
|
|
585
670
|
log.trace('[INCLUDE] Loading hasMany/manyToMany relationship:', {
|
|
586
671
|
scopeName,
|
|
@@ -592,7 +677,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
592
677
|
})
|
|
593
678
|
|
|
594
679
|
// Collect all parent IDs
|
|
595
|
-
const mainIds = records.map(
|
|
680
|
+
const mainIds = records.map(record => getFieldValueForScope(scopes, scopeName, record, 'id')).filter(Boolean)
|
|
596
681
|
|
|
597
682
|
if (mainIds.length === 0) {
|
|
598
683
|
log.trace('[INCLUDE] No parent IDs found, skipping hasMany load')
|
|
@@ -607,7 +692,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
607
692
|
const otherKey = relDef.otherKey
|
|
608
693
|
|
|
609
694
|
if (!foreignKey || !otherKey) {
|
|
610
|
-
throw new Error(`Missing foreignKey or otherKey in many-to-many relationship '${
|
|
695
|
+
throw new Error(`Missing foreignKey or otherKey in many-to-many relationship '${includeName}' for scope '${scopeName}'`)
|
|
611
696
|
}
|
|
612
697
|
|
|
613
698
|
// For manyToMany, the target scope is the relationship name itself
|
|
@@ -631,12 +716,8 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
631
716
|
const relationshipObject = { data: [] }
|
|
632
717
|
|
|
633
718
|
// Add links if urlPrefix is configured
|
|
634
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
635
719
|
if (scopeName) {
|
|
636
|
-
relationshipObject.links =
|
|
637
|
-
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${includeName}`,
|
|
638
|
-
related: `${urlPrefix}/${scopeName}/${record.id}/${includeName}`
|
|
639
|
-
}
|
|
720
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
640
721
|
}
|
|
641
722
|
|
|
642
723
|
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
@@ -653,13 +734,13 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
653
734
|
})
|
|
654
735
|
|
|
655
736
|
// Step 3: Build field selection for sparse fieldsets
|
|
656
|
-
const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance
|
|
657
737
|
const targetScopeObject = scopes[targetScope]
|
|
658
738
|
const storageAdapter = targetScopeObject.vars.storageAdapter
|
|
659
|
-
const
|
|
739
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
660
740
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
661
741
|
? await buildFieldSelection(targetScopeObject, {
|
|
662
742
|
context: {
|
|
743
|
+
...(requestContext || {}),
|
|
663
744
|
scopeName: targetScope,
|
|
664
745
|
queryParams: { fields: { [targetScope]: fields[targetScope] } },
|
|
665
746
|
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
@@ -671,7 +752,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
671
752
|
// Step 4: For many-to-many with limits, we need a different approach
|
|
672
753
|
let targetRecords
|
|
673
754
|
|
|
674
|
-
if (relDef.include?.strategy === 'window') {
|
|
755
|
+
if (relDef.include?.strategy === 'window' && !fieldSelectionInfo?.queryFieldsToSelect?.length) {
|
|
675
756
|
// For many-to-many with window functions, we need to limit per parent
|
|
676
757
|
// This requires joining back through the pivot table
|
|
677
758
|
|
|
@@ -690,27 +771,36 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
690
771
|
})
|
|
691
772
|
}
|
|
692
773
|
|
|
693
|
-
|
|
694
|
-
const windowQuery = knex
|
|
774
|
+
let windowQuery = knex
|
|
695
775
|
.select(`${targetTable}.*`)
|
|
696
776
|
|
|
697
777
|
// Add id alias if needed
|
|
698
|
-
if (
|
|
699
|
-
windowQuery.select(`${targetTable}.${
|
|
778
|
+
if (targetIdColumn !== 'id') {
|
|
779
|
+
windowQuery.select(`${targetTable}.${targetIdColumn} as id`)
|
|
700
780
|
}
|
|
701
781
|
|
|
702
782
|
windowQuery.select(
|
|
703
783
|
knex.raw(
|
|
704
784
|
'ROW_NUMBER() OVER (PARTITION BY pivot.?? ORDER BY ' +
|
|
705
|
-
buildOrderByClause(relDef.include?.orderBy || [
|
|
785
|
+
buildOrderByClause(relDef.include?.orderBy || [targetIdColumn], targetTable) +
|
|
706
786
|
') as ' + ROW_NUMBER_KEY,
|
|
707
787
|
[foreignKey]
|
|
708
788
|
)
|
|
709
789
|
)
|
|
710
790
|
.from(targetTable)
|
|
711
|
-
.join(`${pivotTable} as pivot`, `${targetTable}.${
|
|
791
|
+
.join(`${pivotTable} as pivot`, `${targetTable}.${targetIdColumn}`, 'pivot.' + otherKey)
|
|
712
792
|
.whereIn(`pivot.${foreignKey}`, mainIds)
|
|
713
793
|
|
|
794
|
+
const scopedWindowQueryState = await applyScopeFiltersToIncludeQuery({
|
|
795
|
+
query: windowQuery,
|
|
796
|
+
scopes,
|
|
797
|
+
scopeName: targetScope,
|
|
798
|
+
requestContext,
|
|
799
|
+
db: knex,
|
|
800
|
+
tableName: targetTable
|
|
801
|
+
})
|
|
802
|
+
windowQuery = scopedWindowQueryState.query
|
|
803
|
+
|
|
714
804
|
// Wrap to filter by row number
|
|
715
805
|
const limitedQuery = knex
|
|
716
806
|
.select('*')
|
|
@@ -723,17 +813,41 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
723
813
|
targetRecords.forEach(record => delete record[ROW_NUMBER_KEY])
|
|
724
814
|
} else {
|
|
725
815
|
// Standard query without per-parent limits
|
|
726
|
-
|
|
727
|
-
let query = knex(targetTable).whereIn(targetIdProperty, targetIds)
|
|
816
|
+
let query = knex(targetTable).whereIn(targetIdColumn, targetIds)
|
|
728
817
|
|
|
729
818
|
if (fieldSelectionInfo) {
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
819
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
820
|
+
query,
|
|
821
|
+
scope: targetScopeObject,
|
|
822
|
+
fieldSelectionInfo,
|
|
823
|
+
tableName: targetTable,
|
|
824
|
+
useTablePrefix: false,
|
|
825
|
+
storageAdapter,
|
|
826
|
+
db: knex,
|
|
827
|
+
context: {
|
|
828
|
+
...(requestContext || {}),
|
|
829
|
+
scopeName: targetScope,
|
|
830
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
831
|
+
storageAdapter,
|
|
832
|
+
},
|
|
833
|
+
scopeName: targetScope
|
|
834
|
+
})
|
|
835
|
+
query = selectionState.query
|
|
836
|
+
} else if (targetIdColumn !== 'id') {
|
|
837
|
+
// If no field selection but the storage ID column is not `id`, alias it for response shaping.
|
|
838
|
+
query = query.select('*', `${targetIdColumn} as id`)
|
|
735
839
|
}
|
|
736
840
|
|
|
841
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
842
|
+
query,
|
|
843
|
+
scopes,
|
|
844
|
+
scopeName: targetScope,
|
|
845
|
+
requestContext,
|
|
846
|
+
db: knex,
|
|
847
|
+
tableName: targetTable
|
|
848
|
+
})
|
|
849
|
+
query = scopedQueryState.query
|
|
850
|
+
|
|
737
851
|
// Apply standard include config (global limits)
|
|
738
852
|
if (relDef.include) {
|
|
739
853
|
const targetVars = scopes[targetScope].vars
|
|
@@ -751,7 +865,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
751
865
|
// Step 6: Create lookup map for target records
|
|
752
866
|
const targetById = {}
|
|
753
867
|
targetRecords.forEach(record => {
|
|
754
|
-
targetById[record
|
|
868
|
+
targetById[getFieldValueForScope(scopes, targetScope, record, 'id')] = record
|
|
755
869
|
})
|
|
756
870
|
|
|
757
871
|
// Step 7: Group pivot records by parent ID
|
|
@@ -769,14 +883,16 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
769
883
|
records.forEach(record => {
|
|
770
884
|
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
771
885
|
|
|
772
|
-
const
|
|
886
|
+
const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
|
|
887
|
+
const childIds = pivotsByParent[recordId] || []
|
|
773
888
|
|
|
774
889
|
const relData = childIds
|
|
775
890
|
.map(childId => targetById[childId])
|
|
776
891
|
.filter(Boolean)
|
|
777
892
|
.map(childRecord => {
|
|
778
893
|
// Add to included
|
|
779
|
-
const
|
|
894
|
+
const childRecordId = getFieldValueForScope(scopes, targetScope, childRecord, 'id')
|
|
895
|
+
const resourceKey = `${targetScope}:${childRecordId}`
|
|
780
896
|
if (!included.has(resourceKey)) {
|
|
781
897
|
const jsonApiRecord = toJsonApiRecord(
|
|
782
898
|
scopes[targetScope],
|
|
@@ -798,24 +914,20 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
798
914
|
|
|
799
915
|
included.set(resourceKey, jsonApiRecord)
|
|
800
916
|
}
|
|
801
|
-
return { type: targetScope, id: String(
|
|
917
|
+
return { type: targetScope, id: String(childRecordId) }
|
|
802
918
|
})
|
|
803
919
|
|
|
804
920
|
const relationshipObject = { data: relData }
|
|
805
921
|
|
|
806
922
|
// Add links if urlPrefix is configured
|
|
807
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
808
923
|
if (scopeName) {
|
|
809
|
-
relationshipObject.links =
|
|
810
|
-
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${includeName}`,
|
|
811
|
-
related: `${urlPrefix}/${scopeName}/${record.id}/${includeName}`
|
|
812
|
-
}
|
|
924
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
813
925
|
}
|
|
814
926
|
|
|
815
927
|
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
816
928
|
|
|
817
929
|
// Update the record in the included Map if it exists
|
|
818
|
-
const recordKey = `${scopeName}:${
|
|
930
|
+
const recordKey = `${scopeName}:${recordId}`
|
|
819
931
|
if (included.has(recordKey)) {
|
|
820
932
|
const existingRecord = included.get(recordKey)
|
|
821
933
|
if (!existingRecord.relationships) {
|
|
@@ -830,8 +942,8 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
830
942
|
const nextPath = `${currentPath}.${includeName}`
|
|
831
943
|
if (!processedPaths.has(nextPath)) {
|
|
832
944
|
await processIncludes(
|
|
833
|
-
{ records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields
|
|
834
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
945
|
+
{ records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
|
|
946
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
835
947
|
)
|
|
836
948
|
}
|
|
837
949
|
}
|
|
@@ -842,7 +954,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
842
954
|
const foreignKey = relDef.foreignKey
|
|
843
955
|
|
|
844
956
|
if (!foreignKey) {
|
|
845
|
-
throw new Error(`Missing foreignKey in hasMany relationship '${
|
|
957
|
+
throw new Error(`Missing foreignKey in hasMany relationship '${includeName}' for scope '${scopeName}'`)
|
|
846
958
|
}
|
|
847
959
|
|
|
848
960
|
log.debug(`[INCLUDE] Loading ${targetScope} records with foreign key ${foreignKey}:`, {
|
|
@@ -851,10 +963,8 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
851
963
|
})
|
|
852
964
|
|
|
853
965
|
// Build field selection for sparse fieldsets
|
|
854
|
-
const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance
|
|
855
966
|
const targetScopeObject = scopes[targetScope]
|
|
856
967
|
const storageAdapter = targetScopeObject.vars.storageAdapter
|
|
857
|
-
const targetIdProperty = targetScopeObject.vars.schemaInfo.idProperty
|
|
858
968
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
859
969
|
? await buildFieldSelection(targetScopeObject, {
|
|
860
970
|
context: {
|
|
@@ -873,9 +983,11 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
873
983
|
if (relDef.include?.strategy === 'window') {
|
|
874
984
|
try {
|
|
875
985
|
// Try to build window function query
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
986
|
+
if (fieldSelectionInfo?.queryFieldsToSelect?.length) {
|
|
987
|
+
throw new Error('Query projection fields require the standard include query path.')
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const selectFields = fieldSelectionInfo?.fieldsToSelect || null
|
|
879
991
|
|
|
880
992
|
query = buildWindowedIncludeQuery(
|
|
881
993
|
knex,
|
|
@@ -887,6 +999,15 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
887
999
|
capabilities,
|
|
888
1000
|
targetScopeObject.vars // Pass target scope vars
|
|
889
1001
|
)
|
|
1002
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
1003
|
+
query,
|
|
1004
|
+
scopes,
|
|
1005
|
+
scopeName: targetScope,
|
|
1006
|
+
requestContext,
|
|
1007
|
+
db: knex,
|
|
1008
|
+
tableName: targetTable
|
|
1009
|
+
})
|
|
1010
|
+
query = scopedQueryState.query
|
|
890
1011
|
usingWindowFunction = true
|
|
891
1012
|
log.debug('[INCLUDE] Using window function strategy with limits')
|
|
892
1013
|
} catch (error) {
|
|
@@ -905,13 +1026,39 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
905
1026
|
|
|
906
1027
|
// Build standard query if not using window functions
|
|
907
1028
|
if (!usingWindowFunction) {
|
|
908
|
-
|
|
1029
|
+
const foreignKeyColumn = translateColumnForScope(scopes, targetScope, foreignKey)
|
|
1030
|
+
query = knex(targetTable).whereIn(foreignKeyColumn, mainIds)
|
|
909
1031
|
|
|
910
1032
|
if (fieldSelectionInfo) {
|
|
911
|
-
const
|
|
912
|
-
|
|
1033
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
1034
|
+
query,
|
|
1035
|
+
scope: targetScopeObject,
|
|
1036
|
+
fieldSelectionInfo,
|
|
1037
|
+
tableName: targetTable,
|
|
1038
|
+
useTablePrefix: false,
|
|
1039
|
+
storageAdapter,
|
|
1040
|
+
db: knex,
|
|
1041
|
+
context: {
|
|
1042
|
+
...(requestContext || {}),
|
|
1043
|
+
scopeName: targetScope,
|
|
1044
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
1045
|
+
storageAdapter,
|
|
1046
|
+
},
|
|
1047
|
+
scopeName: targetScope
|
|
1048
|
+
})
|
|
1049
|
+
query = selectionState.query
|
|
913
1050
|
}
|
|
914
1051
|
|
|
1052
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
1053
|
+
query,
|
|
1054
|
+
scopes,
|
|
1055
|
+
scopeName: targetScope,
|
|
1056
|
+
requestContext,
|
|
1057
|
+
db: knex,
|
|
1058
|
+
tableName: targetTable
|
|
1059
|
+
})
|
|
1060
|
+
query = scopedQueryState.query
|
|
1061
|
+
|
|
915
1062
|
// Apply standard config with defaults
|
|
916
1063
|
const targetVars = scopes[targetScope].vars
|
|
917
1064
|
query = applyStandardIncludeConfig(
|
|
@@ -941,7 +1088,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
941
1088
|
// Group by parent ID
|
|
942
1089
|
const childrenByParent = {}
|
|
943
1090
|
targetRecords.forEach(record => {
|
|
944
|
-
const parentId = record
|
|
1091
|
+
const parentId = getFieldValueForScope(scopes, targetScope, record, foreignKey)
|
|
945
1092
|
if (!childrenByParent[parentId]) {
|
|
946
1093
|
childrenByParent[parentId] = []
|
|
947
1094
|
}
|
|
@@ -952,10 +1099,12 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
952
1099
|
records.forEach(record => {
|
|
953
1100
|
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
954
1101
|
|
|
955
|
-
const
|
|
1102
|
+
const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
|
|
1103
|
+
const children = childrenByParent[recordId] || []
|
|
956
1104
|
const relData = children.map(childRecord => {
|
|
957
1105
|
// Add to included
|
|
958
|
-
const
|
|
1106
|
+
const childRecordId = getFieldValueForScope(scopes, targetScope, childRecord, 'id')
|
|
1107
|
+
const resourceKey = `${targetScope}:${childRecordId}`
|
|
959
1108
|
if (!included.has(resourceKey)) {
|
|
960
1109
|
const jsonApiRecord = toJsonApiRecord(
|
|
961
1110
|
scopes[targetScope],
|
|
@@ -977,24 +1126,20 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
977
1126
|
|
|
978
1127
|
included.set(resourceKey, jsonApiRecord)
|
|
979
1128
|
}
|
|
980
|
-
return { type: targetScope, id: String(
|
|
1129
|
+
return { type: targetScope, id: String(childRecordId) }
|
|
981
1130
|
})
|
|
982
1131
|
|
|
983
1132
|
const relationshipObject = { data: relData }
|
|
984
1133
|
|
|
985
1134
|
// Add links if urlPrefix is configured
|
|
986
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
987
1135
|
if (scopeName) {
|
|
988
|
-
relationshipObject.links =
|
|
989
|
-
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${includeName}`,
|
|
990
|
-
related: `${urlPrefix}/${scopeName}/${record.id}/${includeName}`
|
|
991
|
-
}
|
|
1136
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
992
1137
|
}
|
|
993
1138
|
|
|
994
1139
|
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
995
1140
|
|
|
996
1141
|
// Update the record in the included Map if it exists
|
|
997
|
-
const recordKey = `${scopeName}:${
|
|
1142
|
+
const recordKey = `${scopeName}:${recordId}`
|
|
998
1143
|
if (included.has(recordKey)) {
|
|
999
1144
|
const existingRecord = included.get(recordKey)
|
|
1000
1145
|
if (!existingRecord.relationships) {
|
|
@@ -1009,8 +1154,8 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
1009
1154
|
const nextPath = `${currentPath}.${includeName}`
|
|
1010
1155
|
if (!processedPaths.has(nextPath)) {
|
|
1011
1156
|
await processIncludes(
|
|
1012
|
-
{ records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields
|
|
1013
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1157
|
+
{ records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
|
|
1158
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1014
1159
|
)
|
|
1015
1160
|
}
|
|
1016
1161
|
}
|
|
@@ -1047,21 +1192,20 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
1047
1192
|
*/
|
|
1048
1193
|
export const loadHasOne = async (scope, deps) => {
|
|
1049
1194
|
const { records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
|
|
1050
|
-
const { context: { scopes, log, knex, capabilities } } = deps
|
|
1195
|
+
const { context: { scopes, log, knex, capabilities, requestContext } } = deps
|
|
1051
1196
|
|
|
1052
|
-
|
|
1197
|
+
const childLog = log?.child ? log.child({ method: 'loadHasOne' }) : console
|
|
1053
1198
|
|
|
1054
1199
|
if (!records || records.length === 0) {
|
|
1055
|
-
|
|
1200
|
+
childLog.trace('[INCLUDE] No records to load hasOne for')
|
|
1056
1201
|
return
|
|
1057
1202
|
}
|
|
1058
1203
|
|
|
1059
1204
|
// Get parent IDs
|
|
1060
|
-
const
|
|
1061
|
-
const mainIds = [...new Set(records.map(r => r[idProperty]).filter(Boolean))]
|
|
1205
|
+
const mainIds = [...new Set(records.map(record => getFieldValueForScope(scopes, scopeName, record, 'id')).filter(Boolean))]
|
|
1062
1206
|
|
|
1063
1207
|
if (mainIds.length === 0) {
|
|
1064
|
-
|
|
1208
|
+
childLog.trace('[INCLUDE] No parent IDs found, skipping hasOne load')
|
|
1065
1209
|
return
|
|
1066
1210
|
}
|
|
1067
1211
|
|
|
@@ -1073,20 +1217,19 @@ export const loadHasOne = async (scope, deps) => {
|
|
|
1073
1217
|
throw new Error(`Missing foreignKey in hasOne relationship '${includeName}' for scope '${scopeName}'`)
|
|
1074
1218
|
}
|
|
1075
1219
|
|
|
1076
|
-
|
|
1220
|
+
childLog.debug(`[INCLUDE] Loading ${targetScope} records with foreign key ${foreignKey}:`, {
|
|
1077
1221
|
whereIn: mainIds
|
|
1078
1222
|
})
|
|
1079
1223
|
|
|
1080
1224
|
// Build query for hasOne - expects single result per parent
|
|
1081
|
-
const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance
|
|
1082
1225
|
const targetScopeObject = scopes[targetScope]
|
|
1083
1226
|
const storageAdapter = targetScopeObject.vars.storageAdapter
|
|
1084
|
-
const targetIdProperty = targetScopeObject.vars.schemaInfo.idProperty
|
|
1085
1227
|
|
|
1086
1228
|
// Build field selection for sparse fieldsets
|
|
1087
1229
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
1088
1230
|
? await buildFieldSelection(targetScopeObject, {
|
|
1089
1231
|
context: {
|
|
1232
|
+
...(requestContext || {}),
|
|
1090
1233
|
scopeName: targetScope,
|
|
1091
1234
|
queryParams: { fields: { [targetScope]: fields[targetScope] } },
|
|
1092
1235
|
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
@@ -1096,19 +1239,45 @@ export const loadHasOne = async (scope, deps) => {
|
|
|
1096
1239
|
: null
|
|
1097
1240
|
|
|
1098
1241
|
// Query related records
|
|
1099
|
-
|
|
1242
|
+
const foreignKeyColumn = translateColumnForScope(scopes, targetScope, foreignKey)
|
|
1243
|
+
let query = knex(targetTable).whereIn(foreignKeyColumn, mainIds)
|
|
1100
1244
|
|
|
1101
|
-
if (fieldSelectionInfo
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1245
|
+
if (fieldSelectionInfo) {
|
|
1246
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
1247
|
+
query,
|
|
1248
|
+
scope: targetScopeObject,
|
|
1249
|
+
fieldSelectionInfo,
|
|
1250
|
+
tableName: targetTable,
|
|
1251
|
+
useTablePrefix: false,
|
|
1252
|
+
storageAdapter,
|
|
1253
|
+
db: knex,
|
|
1254
|
+
context: {
|
|
1255
|
+
...(requestContext || {}),
|
|
1256
|
+
scopeName: targetScope,
|
|
1257
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
1258
|
+
storageAdapter,
|
|
1259
|
+
},
|
|
1260
|
+
scopeName: targetScope
|
|
1261
|
+
})
|
|
1262
|
+
query = selectionState.query
|
|
1104
1263
|
}
|
|
1105
1264
|
|
|
1265
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
1266
|
+
query,
|
|
1267
|
+
scopes,
|
|
1268
|
+
scopeName: targetScope,
|
|
1269
|
+
requestContext,
|
|
1270
|
+
db: knex,
|
|
1271
|
+
tableName: targetTable
|
|
1272
|
+
})
|
|
1273
|
+
query = scopedQueryState.query
|
|
1274
|
+
|
|
1106
1275
|
const relatedRecords = await query
|
|
1107
1276
|
|
|
1108
1277
|
// Create a map of parent ID to related record (should be one-to-one)
|
|
1109
1278
|
const relatedByParentId = {}
|
|
1110
1279
|
for (const related of relatedRecords) {
|
|
1111
|
-
const parentId = related
|
|
1280
|
+
const parentId = getFieldValueForScope(scopes, targetScope, related, foreignKey)
|
|
1112
1281
|
if (relatedByParentId[parentId]) {
|
|
1113
1282
|
log.warn(`[INCLUDE] Multiple records found for hasOne relationship '${includeName}' with ${foreignKey}=${parentId}`)
|
|
1114
1283
|
}
|
|
@@ -1117,7 +1286,7 @@ export const loadHasOne = async (scope, deps) => {
|
|
|
1117
1286
|
|
|
1118
1287
|
// Process each parent record
|
|
1119
1288
|
for (const record of records) {
|
|
1120
|
-
const recordId = record
|
|
1289
|
+
const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
|
|
1121
1290
|
const relatedRecord = relatedByParentId[recordId]
|
|
1122
1291
|
|
|
1123
1292
|
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
@@ -1136,12 +1305,13 @@ export const loadHasOne = async (scope, deps) => {
|
|
|
1136
1305
|
)
|
|
1137
1306
|
|
|
1138
1307
|
// Set single object (not array) for hasOne
|
|
1308
|
+
const relatedRecordId = getFieldValueForScope(scopes, targetScope, relatedRecord, 'id')
|
|
1139
1309
|
record[RELATIONSHIPS_KEY][includeName] = {
|
|
1140
|
-
data: { type: targetScope, id: String(
|
|
1310
|
+
data: { type: targetScope, id: String(relatedRecordId) }
|
|
1141
1311
|
}
|
|
1142
1312
|
|
|
1143
1313
|
// Add to included if not already there
|
|
1144
|
-
const key = `${targetScope}:${
|
|
1314
|
+
const key = `${targetScope}:${relatedRecordId}`
|
|
1145
1315
|
if (!included.has(key)) {
|
|
1146
1316
|
included.set(key, transformed)
|
|
1147
1317
|
}
|
|
@@ -1153,7 +1323,7 @@ export const loadHasOne = async (scope, deps) => {
|
|
|
1153
1323
|
processedPaths.add(newPath)
|
|
1154
1324
|
await processIncludes(
|
|
1155
1325
|
{ records: [relatedRecord], scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: newPath, fields },
|
|
1156
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1326
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1157
1327
|
)
|
|
1158
1328
|
}
|
|
1159
1329
|
}
|
|
@@ -1187,7 +1357,7 @@ export const loadHasOne = async (scope, deps) => {
|
|
|
1187
1357
|
*/
|
|
1188
1358
|
export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
1189
1359
|
const { records, scopeName, relName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
|
|
1190
|
-
const { scopes, log, knex, capabilities } = deps.context
|
|
1360
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
1191
1361
|
try {
|
|
1192
1362
|
log.trace('[INCLUDE] Loading polymorphic belongsTo:', {
|
|
1193
1363
|
relName,
|
|
@@ -1200,7 +1370,8 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1200
1370
|
const grouped = groupByPolymorphicType(
|
|
1201
1371
|
records,
|
|
1202
1372
|
typeField,
|
|
1203
|
-
idField
|
|
1373
|
+
idField,
|
|
1374
|
+
(record, fieldName) => getFieldValueForScope(scopes, scopeName, record, fieldName)
|
|
1204
1375
|
)
|
|
1205
1376
|
|
|
1206
1377
|
log.trace('[INCLUDE] Grouped by type:', {
|
|
@@ -1224,11 +1395,11 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1224
1395
|
|
|
1225
1396
|
// Build field selection for sparse fieldsets
|
|
1226
1397
|
const targetScopeObject = scopes[targetType]
|
|
1227
|
-
// const targetIdProperty = targetScopeObject.vars.schemaInfo.idProperty;
|
|
1228
1398
|
const storageAdapter = targetScopeObject.vars.storageAdapter
|
|
1229
1399
|
const fieldSelectionInfo = fields?.[targetType]
|
|
1230
1400
|
? await buildFieldSelection(targetScopeObject, {
|
|
1231
1401
|
context: {
|
|
1402
|
+
...(requestContext || {}),
|
|
1232
1403
|
scopeName: targetType,
|
|
1233
1404
|
queryParams: { fields: { [targetType]: fields[targetType] } },
|
|
1234
1405
|
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
@@ -1243,15 +1414,39 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1243
1414
|
})
|
|
1244
1415
|
|
|
1245
1416
|
// Query for this type
|
|
1246
|
-
const
|
|
1247
|
-
let query = knex(targetTable).whereIn(
|
|
1417
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetType)
|
|
1418
|
+
let query = knex(targetTable).whereIn(targetIdColumn, targetIds)
|
|
1248
1419
|
if (fieldSelectionInfo) {
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1420
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
1421
|
+
query,
|
|
1422
|
+
scope: targetScopeObject,
|
|
1423
|
+
fieldSelectionInfo,
|
|
1424
|
+
tableName: targetTable,
|
|
1425
|
+
useTablePrefix: false,
|
|
1426
|
+
storageAdapter,
|
|
1427
|
+
db: knex,
|
|
1428
|
+
context: {
|
|
1429
|
+
...(requestContext || {}),
|
|
1430
|
+
scopeName: targetType,
|
|
1431
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
1432
|
+
storageAdapter,
|
|
1433
|
+
},
|
|
1434
|
+
scopeName: targetType
|
|
1435
|
+
})
|
|
1436
|
+
query = selectionState.query
|
|
1437
|
+
} else if (targetIdColumn !== 'id') {
|
|
1438
|
+
// If no field selection but the storage ID column is not `id`, alias it for response shaping.
|
|
1439
|
+
query = query.select('*', `${targetIdColumn} as id`)
|
|
1254
1440
|
}
|
|
1441
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
1442
|
+
query,
|
|
1443
|
+
scopes,
|
|
1444
|
+
scopeName: targetType,
|
|
1445
|
+
requestContext,
|
|
1446
|
+
db: knex,
|
|
1447
|
+
tableName: targetTable
|
|
1448
|
+
})
|
|
1449
|
+
query = scopedQueryState.query
|
|
1255
1450
|
const targetRecords = await query
|
|
1256
1451
|
|
|
1257
1452
|
// Load relationship metadata for all target records
|
|
@@ -1260,13 +1455,13 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1260
1455
|
// Create lookup map
|
|
1261
1456
|
const targetById = {}
|
|
1262
1457
|
targetRecords.forEach(record => {
|
|
1263
|
-
targetById[record
|
|
1458
|
+
targetById[getFieldValueForScope(scopes, targetType, record, 'id')] = record
|
|
1264
1459
|
})
|
|
1265
1460
|
|
|
1266
1461
|
// Add to included and set relationships
|
|
1267
1462
|
records.forEach(record => {
|
|
1268
|
-
if (record
|
|
1269
|
-
const targetId = record
|
|
1463
|
+
if (getFieldValueForScope(scopes, scopeName, record, typeField) === targetType) {
|
|
1464
|
+
const targetId = getFieldValueForScope(scopes, scopeName, record, idField)
|
|
1270
1465
|
const targetRecord = targetById[targetId]
|
|
1271
1466
|
|
|
1272
1467
|
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
@@ -1301,12 +1496,8 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1301
1496
|
}
|
|
1302
1497
|
|
|
1303
1498
|
// Add links if urlPrefix is configured
|
|
1304
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
1305
1499
|
if (scopeName) {
|
|
1306
|
-
relationshipObject.links =
|
|
1307
|
-
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${relName}`,
|
|
1308
|
-
related: `${urlPrefix}/${scopeName}/${record.id}/${relName}`
|
|
1309
|
-
}
|
|
1500
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, relName)
|
|
1310
1501
|
}
|
|
1311
1502
|
|
|
1312
1503
|
record[RELATIONSHIPS_KEY][relName] = relationshipObject
|
|
@@ -1314,12 +1505,8 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1314
1505
|
const relationshipObject = { data: null }
|
|
1315
1506
|
|
|
1316
1507
|
// Add links if urlPrefix is configured
|
|
1317
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
1318
1508
|
if (scopeName) {
|
|
1319
|
-
relationshipObject.links =
|
|
1320
|
-
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${relName}`,
|
|
1321
|
-
related: `${urlPrefix}/${scopeName}/${record.id}/${relName}`
|
|
1322
|
-
}
|
|
1509
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, relName)
|
|
1323
1510
|
}
|
|
1324
1511
|
|
|
1325
1512
|
record[RELATIONSHIPS_KEY][relName] = relationshipObject
|
|
@@ -1332,8 +1519,8 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1332
1519
|
const nextPath = `${currentPath}.${relName}`
|
|
1333
1520
|
if (!processedPaths.has(nextPath)) {
|
|
1334
1521
|
await processIncludes(
|
|
1335
|
-
{ records: targetRecords, scopeName: targetType, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields
|
|
1336
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1522
|
+
{ records: targetRecords, scopeName: targetType, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
|
|
1523
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1337
1524
|
)
|
|
1338
1525
|
}
|
|
1339
1526
|
}
|
|
@@ -1346,12 +1533,8 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1346
1533
|
const relationshipObject = { data: null }
|
|
1347
1534
|
|
|
1348
1535
|
// Add links if urlPrefix is configured
|
|
1349
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
1350
1536
|
if (scopeName) {
|
|
1351
|
-
relationshipObject.links =
|
|
1352
|
-
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${relName}`,
|
|
1353
|
-
related: `${urlPrefix}/${scopeName}/${record.id}/${relName}`
|
|
1354
|
-
}
|
|
1537
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, relName)
|
|
1355
1538
|
}
|
|
1356
1539
|
|
|
1357
1540
|
record[RELATIONSHIPS_KEY][relName] = relationshipObject
|
|
@@ -1401,7 +1584,7 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1401
1584
|
*/
|
|
1402
1585
|
export const loadReversePolymorphic = async (scope, deps) => {
|
|
1403
1586
|
const { records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
|
|
1404
|
-
const { scopes, log, knex, capabilities } = deps.context
|
|
1587
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
1405
1588
|
try {
|
|
1406
1589
|
log.trace('[INCLUDE] Loading reverse polymorphic (via):', {
|
|
1407
1590
|
scopeName,
|
|
@@ -1429,7 +1612,7 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1429
1612
|
const targetTable = scopes[targetScope].vars.schemaInfo.tableName
|
|
1430
1613
|
|
|
1431
1614
|
// Collect parent IDs
|
|
1432
|
-
const parentIds = records.map(
|
|
1615
|
+
const parentIds = records.map(record => getFieldValueForScope(scopes, scopeName, record, 'id')).filter(Boolean)
|
|
1433
1616
|
if (parentIds.length === 0) return
|
|
1434
1617
|
|
|
1435
1618
|
log.debug(`[INCLUDE] Loading ${targetScope} records via ${viaRelName}:`, {
|
|
@@ -1440,13 +1623,13 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1440
1623
|
})
|
|
1441
1624
|
|
|
1442
1625
|
// Build field selection for sparse fieldsets
|
|
1443
|
-
const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance
|
|
1444
1626
|
const targetScopeObject = scopes[targetScope]
|
|
1445
1627
|
const storageAdapter = targetScopeObject.vars.storageAdapter
|
|
1446
|
-
const
|
|
1628
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
1447
1629
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
1448
1630
|
? await buildFieldSelection(targetScopeObject, {
|
|
1449
1631
|
context: {
|
|
1632
|
+
...(requestContext || {}),
|
|
1450
1633
|
scopeName: targetScope,
|
|
1451
1634
|
queryParams: { fields: { [targetScope]: fields[targetScope] } },
|
|
1452
1635
|
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
@@ -1456,18 +1639,45 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1456
1639
|
: null
|
|
1457
1640
|
|
|
1458
1641
|
// Query for records pointing back to our scope
|
|
1642
|
+
const typeColumn = translateColumnForScope(scopes, targetScope, typeField)
|
|
1643
|
+
const idColumn = translateColumnForScope(scopes, targetScope, idField)
|
|
1459
1644
|
let query = knex(targetTable)
|
|
1460
|
-
.where(
|
|
1461
|
-
.whereIn(
|
|
1645
|
+
.where(typeColumn, scopeName)
|
|
1646
|
+
.whereIn(idColumn, parentIds)
|
|
1462
1647
|
|
|
1463
1648
|
if (fieldSelectionInfo) {
|
|
1464
|
-
const
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1649
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
1650
|
+
query,
|
|
1651
|
+
scope: targetScopeObject,
|
|
1652
|
+
fieldSelectionInfo,
|
|
1653
|
+
tableName: targetTable,
|
|
1654
|
+
useTablePrefix: false,
|
|
1655
|
+
storageAdapter,
|
|
1656
|
+
db: knex,
|
|
1657
|
+
context: {
|
|
1658
|
+
...(requestContext || {}),
|
|
1659
|
+
scopeName: targetScope,
|
|
1660
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
1661
|
+
storageAdapter,
|
|
1662
|
+
},
|
|
1663
|
+
scopeName: targetScope
|
|
1664
|
+
})
|
|
1665
|
+
query = selectionState.query
|
|
1666
|
+
} else if (targetIdColumn !== 'id') {
|
|
1667
|
+
// If no field selection but the storage ID column is not `id`, alias it for response shaping.
|
|
1668
|
+
query = query.select('*', `${targetIdColumn} as id`)
|
|
1469
1669
|
}
|
|
1470
1670
|
|
|
1671
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
1672
|
+
query,
|
|
1673
|
+
scopes,
|
|
1674
|
+
scopeName: targetScope,
|
|
1675
|
+
requestContext,
|
|
1676
|
+
db: knex,
|
|
1677
|
+
tableName: targetTable
|
|
1678
|
+
})
|
|
1679
|
+
query = scopedQueryState.query
|
|
1680
|
+
|
|
1471
1681
|
// Apply include configuration (limits, ordering, etc.)
|
|
1472
1682
|
const targetVars = scopes[targetScope].vars
|
|
1473
1683
|
query = applyStandardIncludeConfig(
|
|
@@ -1489,7 +1699,7 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1489
1699
|
// Group by parent ID
|
|
1490
1700
|
const childrenByParent = {}
|
|
1491
1701
|
targetRecords.forEach(record => {
|
|
1492
|
-
const parentId = record
|
|
1702
|
+
const parentId = getFieldValueForScope(scopes, targetScope, record, idField)
|
|
1493
1703
|
if (!childrenByParent[parentId]) {
|
|
1494
1704
|
childrenByParent[parentId] = []
|
|
1495
1705
|
}
|
|
@@ -1500,10 +1710,12 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1500
1710
|
records.forEach(record => {
|
|
1501
1711
|
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
1502
1712
|
|
|
1503
|
-
const
|
|
1713
|
+
const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
|
|
1714
|
+
const children = childrenByParent[recordId] || []
|
|
1504
1715
|
const relData = children.map(childRecord => {
|
|
1505
1716
|
// Add to included
|
|
1506
|
-
const
|
|
1717
|
+
const childRecordId = getFieldValueForScope(scopes, targetScope, childRecord, 'id')
|
|
1718
|
+
const resourceKey = `${targetScope}:${childRecordId}`
|
|
1507
1719
|
if (!included.has(resourceKey)) {
|
|
1508
1720
|
const jsonApiRecord = toJsonApiRecord(
|
|
1509
1721
|
scopes[targetScope],
|
|
@@ -1525,18 +1737,14 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1525
1737
|
|
|
1526
1738
|
included.set(resourceKey, jsonApiRecord)
|
|
1527
1739
|
}
|
|
1528
|
-
return { type: targetScope, id: String(
|
|
1740
|
+
return { type: targetScope, id: String(childRecordId) }
|
|
1529
1741
|
})
|
|
1530
1742
|
|
|
1531
1743
|
const relationshipObject = { data: relData }
|
|
1532
1744
|
|
|
1533
1745
|
// Add links if urlPrefix is configured
|
|
1534
|
-
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
1535
1746
|
if (scopeName) {
|
|
1536
|
-
relationshipObject.links =
|
|
1537
|
-
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${includeName}`,
|
|
1538
|
-
related: `${urlPrefix}/${scopeName}/${record.id}/${includeName}`
|
|
1539
|
-
}
|
|
1747
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
1540
1748
|
}
|
|
1541
1749
|
|
|
1542
1750
|
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
@@ -1547,8 +1755,8 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1547
1755
|
const nextPath = `${currentPath}.${includeName}`
|
|
1548
1756
|
if (!processedPaths.has(nextPath)) {
|
|
1549
1757
|
await processIncludes(
|
|
1550
|
-
{ records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields
|
|
1551
|
-
{ context: { scopes, log, knex } }
|
|
1758
|
+
{ records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
|
|
1759
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1552
1760
|
)
|
|
1553
1761
|
}
|
|
1554
1762
|
}
|
|
@@ -1588,7 +1796,6 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1588
1796
|
* - processedPaths: Set - Set tracking processed paths to prevent cycles
|
|
1589
1797
|
* - currentPath: string - Current path in the include tree (default '')
|
|
1590
1798
|
* - fields: Object - Sparse fieldsets configuration (default {})
|
|
1591
|
-
* - idProperty: string - The ID property name
|
|
1592
1799
|
* @param {Object} deps - Dependencies object containing:
|
|
1593
1800
|
* - context.scopes: Object - The hooked-api scopes object
|
|
1594
1801
|
* - context.log: Object - Logger instance
|
|
@@ -1596,8 +1803,8 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1596
1803
|
* @returns {Promise<void>}
|
|
1597
1804
|
*/
|
|
1598
1805
|
export const processIncludes = async (scope, deps) => {
|
|
1599
|
-
const { records, scopeName, includeTree, included, processedPaths, currentPath = '', fields = {}
|
|
1600
|
-
const { scopes, log, knex, capabilities } = deps.context
|
|
1806
|
+
const { records, scopeName, includeTree, included, processedPaths, currentPath = '', fields = {} } = scope
|
|
1807
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
1601
1808
|
try {
|
|
1602
1809
|
log.trace('[INCLUDE] Processing includes:', {
|
|
1603
1810
|
scopeName,
|
|
@@ -1636,8 +1843,8 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1636
1843
|
for (const [fieldName, fieldDef] of Object.entries(schemaInstance.structure || {})) {
|
|
1637
1844
|
if (fieldDef.as === includeName && fieldDef.belongsTo) {
|
|
1638
1845
|
await loadBelongsTo(
|
|
1639
|
-
{ records, scopeName, fieldName, fieldDef, includeName, subIncludes, included, processedPaths, currentPath, fields
|
|
1640
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1846
|
+
{ records, scopeName, fieldName, fieldDef, includeName, subIncludes, included, processedPaths, currentPath, fields },
|
|
1847
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1641
1848
|
)
|
|
1642
1849
|
handled = true
|
|
1643
1850
|
break
|
|
@@ -1658,7 +1865,7 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1658
1865
|
})
|
|
1659
1866
|
await loadHasOne(
|
|
1660
1867
|
{ records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields },
|
|
1661
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1868
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1662
1869
|
)
|
|
1663
1870
|
handled = true
|
|
1664
1871
|
} else if (relDef.type === 'hasMany' || relDef.type === 'manyToMany') {
|
|
@@ -1666,7 +1873,7 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1666
1873
|
if (relDef.via) {
|
|
1667
1874
|
await loadReversePolymorphic(
|
|
1668
1875
|
{ records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields },
|
|
1669
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1876
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1670
1877
|
)
|
|
1671
1878
|
} else {
|
|
1672
1879
|
log.debug('[INCLUDE] About to call loadHasMany:', {
|
|
@@ -1678,14 +1885,14 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1678
1885
|
})
|
|
1679
1886
|
await loadHasMany(
|
|
1680
1887
|
{ records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields },
|
|
1681
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1888
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1682
1889
|
)
|
|
1683
1890
|
}
|
|
1684
1891
|
handled = true
|
|
1685
1892
|
} else if (relDef.belongsToPolymorphic) {
|
|
1686
1893
|
await loadPolymorphicBelongsTo(
|
|
1687
1894
|
{ records, scopeName, relName: includeName, relDef, subIncludes, included, processedPaths, currentPath, fields },
|
|
1688
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
1895
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1689
1896
|
)
|
|
1690
1897
|
handled = true
|
|
1691
1898
|
}
|
|
@@ -1737,7 +1944,6 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1737
1944
|
* - scopeName: string - The scope name of the main resources
|
|
1738
1945
|
* - includeParam: string - The include parameter value (e.g., "author,comments.author")
|
|
1739
1946
|
* - fields: Object - Sparse fieldsets configuration
|
|
1740
|
-
* - idProperty: string - The ID property name
|
|
1741
1947
|
* @param {Object} deps - Dependencies object containing:
|
|
1742
1948
|
* - context.scopes: Object - The hooked-api scopes object
|
|
1743
1949
|
* - context.log: Object - Logger instance
|
|
@@ -1750,8 +1956,7 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1750
1956
|
* records: articleRecords,
|
|
1751
1957
|
* scopeName: 'articles',
|
|
1752
1958
|
* includeParam: 'author,comments.author',
|
|
1753
|
-
* fields: { articles: 'title,body', people: 'name' }
|
|
1754
|
-
* idProperty: 'id'
|
|
1959
|
+
* fields: { articles: 'title,body', people: 'name' }
|
|
1755
1960
|
* },
|
|
1756
1961
|
* { context: { scopes, log, knex } }
|
|
1757
1962
|
* );
|
|
@@ -1765,8 +1970,8 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1765
1970
|
* // result.recordsWithRelationships = original records with _relationships added
|
|
1766
1971
|
*/
|
|
1767
1972
|
export const buildIncludedResources = async (scope, deps) => {
|
|
1768
|
-
const { records, scopeName, includeParam, fields
|
|
1769
|
-
const { scopes, log, knex, capabilities } = deps.context
|
|
1973
|
+
const { records, scopeName, includeParam, fields } = scope
|
|
1974
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
1770
1975
|
try {
|
|
1771
1976
|
log.trace('[INCLUDE] Building included resources:', { scopeName, includeParam, recordCount: records.length })
|
|
1772
1977
|
|
|
@@ -1799,8 +2004,8 @@ export const buildIncludedResources = async (scope, deps) => {
|
|
|
1799
2004
|
|
|
1800
2005
|
// Process all includes
|
|
1801
2006
|
await processIncludes(
|
|
1802
|
-
{ records, scopeName, includeTree, included, processedPaths, currentPath: '', fields
|
|
1803
|
-
{ context: { scopes, log, knex, capabilities } }
|
|
2007
|
+
{ records, scopeName, includeTree, included, processedPaths, currentPath: '', fields },
|
|
2008
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
1804
2009
|
)
|
|
1805
2010
|
|
|
1806
2011
|
// Convert Map to array for JSON:API format
|
|
@@ -1873,7 +2078,7 @@ export const buildIncludedResources = async (scope, deps) => {
|
|
|
1873
2078
|
* @param {Object} knex - Knex instance for database queries
|
|
1874
2079
|
* @returns {Promise<void>} Modifies records in place by adding relationship data
|
|
1875
2080
|
*/
|
|
1876
|
-
export const loadRelationshipIdentifiers = async (records, scopeName, scopes, knex) => {
|
|
2081
|
+
export const loadRelationshipIdentifiers = async (records, scopeName, scopes, knex, requestContext = null) => {
|
|
1877
2082
|
if (!records.length) return
|
|
1878
2083
|
|
|
1879
2084
|
const schemaInfo = scopes[scopeName]?.vars?.schemaInfo
|
|
@@ -1896,14 +2101,27 @@ export const loadRelationshipIdentifiers = async (records, scopeName, scopes, kn
|
|
|
1896
2101
|
}
|
|
1897
2102
|
const targetScope = scopes[relDef.target]
|
|
1898
2103
|
const targetTable = targetScope?.vars?.schemaInfo?.tableName || relDef.target
|
|
1899
|
-
const
|
|
2104
|
+
const targetIdColumn = getIdColumnForScope(scopes, relDef.target)
|
|
2105
|
+
const foreignKeyColumn = translateColumnForScope(scopes, relDef.target, foreignKey)
|
|
2106
|
+
|
|
2107
|
+
let query = knex(targetTable)
|
|
2108
|
+
.whereIn(foreignKeyColumn, recordIds)
|
|
2109
|
+
.select(targetIdColumn !== 'id' ? `${targetIdColumn} as id` : 'id', foreignKeyColumn)
|
|
2110
|
+
|
|
2111
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
2112
|
+
query,
|
|
2113
|
+
scopes,
|
|
2114
|
+
scopeName: relDef.target,
|
|
2115
|
+
requestContext,
|
|
2116
|
+
db: knex,
|
|
2117
|
+
tableName: targetTable
|
|
2118
|
+
})
|
|
2119
|
+
query = scopedQueryState.query
|
|
1900
2120
|
|
|
1901
|
-
const results = await
|
|
1902
|
-
.whereIn(foreignKey, recordIds)
|
|
1903
|
-
.select(targetIdProperty !== 'id' ? `${targetIdProperty} as id` : 'id', foreignKey)
|
|
2121
|
+
const results = await query
|
|
1904
2122
|
|
|
1905
2123
|
results.forEach(row => {
|
|
1906
|
-
const parentId = String(row[
|
|
2124
|
+
const parentId = String(row[foreignKeyColumn])
|
|
1907
2125
|
if (!idsMap[parentId]) idsMap[parentId] = []
|
|
1908
2126
|
idsMap[parentId].push(String(row.id))
|
|
1909
2127
|
})
|
|
@@ -1922,13 +2140,34 @@ export const loadRelationshipIdentifiers = async (records, scopeName, scopes, kn
|
|
|
1922
2140
|
const pivotScope = scopes[through]
|
|
1923
2141
|
const pivotTable = pivotScope?.vars?.schemaInfo?.tableName || through
|
|
1924
2142
|
|
|
1925
|
-
const
|
|
1926
|
-
|
|
1927
|
-
|
|
2143
|
+
const targetScopeName = relDef.target || relName
|
|
2144
|
+
const targetTable = scopes[targetScopeName]?.vars?.schemaInfo?.tableName || targetScopeName
|
|
2145
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetScopeName)
|
|
2146
|
+
let query = knex(targetTable)
|
|
2147
|
+
.join(`${pivotTable} as pivot`, `${targetTable}.${targetIdColumn}`, `pivot.${ok}`)
|
|
2148
|
+
.whereIn(`pivot.${fk}`, recordIds)
|
|
2149
|
+
.select(
|
|
2150
|
+
`pivot.${fk} as __parent_id`,
|
|
2151
|
+
targetIdColumn !== 'id'
|
|
2152
|
+
? `${targetTable}.${targetIdColumn} as id`
|
|
2153
|
+
: `${targetTable}.id as id`
|
|
2154
|
+
)
|
|
2155
|
+
|
|
2156
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
2157
|
+
query,
|
|
2158
|
+
scopes,
|
|
2159
|
+
scopeName: targetScopeName,
|
|
2160
|
+
requestContext,
|
|
2161
|
+
db: knex,
|
|
2162
|
+
tableName: targetTable
|
|
2163
|
+
})
|
|
2164
|
+
query = scopedQueryState.query
|
|
2165
|
+
|
|
2166
|
+
const results = await query
|
|
1928
2167
|
|
|
1929
2168
|
results.forEach(row => {
|
|
1930
|
-
const parentId = String(row
|
|
1931
|
-
const childId = String(row
|
|
2169
|
+
const parentId = String(row.__parent_id)
|
|
2170
|
+
const childId = String(row.id)
|
|
1932
2171
|
if (!idsMap[parentId]) idsMap[parentId] = []
|
|
1933
2172
|
idsMap[parentId].push(childId)
|
|
1934
2173
|
})
|
|
@@ -1944,15 +2183,29 @@ export const loadRelationshipIdentifiers = async (records, scopeName, scopes, kn
|
|
|
1944
2183
|
|
|
1945
2184
|
// Get the actual table name
|
|
1946
2185
|
const targetTable = scopes[targetScope]?.vars?.schemaInfo?.tableName || targetScope
|
|
1947
|
-
const
|
|
2186
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
2187
|
+
const typeColumn = translateColumnForScope(scopes, targetScope, typeField)
|
|
2188
|
+
const idColumn = translateColumnForScope(scopes, targetScope, idField)
|
|
2189
|
+
|
|
2190
|
+
let query = knex(targetTable)
|
|
2191
|
+
.where(typeColumn, scopeName)
|
|
2192
|
+
.whereIn(idColumn, recordIds)
|
|
2193
|
+
.select(targetIdColumn !== 'id' ? `${targetIdColumn} as id` : 'id', idColumn)
|
|
2194
|
+
|
|
2195
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
2196
|
+
query,
|
|
2197
|
+
scopes,
|
|
2198
|
+
scopeName: targetScope,
|
|
2199
|
+
requestContext,
|
|
2200
|
+
db: knex,
|
|
2201
|
+
tableName: targetTable
|
|
2202
|
+
})
|
|
2203
|
+
query = scopedQueryState.query
|
|
1948
2204
|
|
|
1949
|
-
const results = await
|
|
1950
|
-
.where(typeField, scopeName)
|
|
1951
|
-
.whereIn(idField, recordIds)
|
|
1952
|
-
.select(targetIdProperty !== 'id' ? `${targetIdProperty} as id` : 'id', idField)
|
|
2205
|
+
const results = await query
|
|
1953
2206
|
|
|
1954
2207
|
results.forEach(row => {
|
|
1955
|
-
const parentId = String(row[
|
|
2208
|
+
const parentId = String(row[idColumn])
|
|
1956
2209
|
if (!idsMap[parentId]) idsMap[parentId] = []
|
|
1957
2210
|
idsMap[parentId].push(String(row.id))
|
|
1958
2211
|
})
|
|
@@ -1966,7 +2219,7 @@ export const loadRelationshipIdentifiers = async (records, scopeName, scopes, kn
|
|
|
1966
2219
|
record[RELATIONSHIPS_KEY] = {}
|
|
1967
2220
|
}
|
|
1968
2221
|
|
|
1969
|
-
const ids = idsMap[String(record
|
|
2222
|
+
const ids = idsMap[String(getFieldValueForScope(scopes, scopeName, record, 'id'))] || []
|
|
1970
2223
|
// For manyToMany, the target type is the relationship name itself
|
|
1971
2224
|
// For hasMany/hasOne, the target type is specified in the target property
|
|
1972
2225
|
const targetType = (relDef.type === 'manyToMany') ? relName : relDef.target
|