json-rest-api 1.0.22 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/CLAUDE.md +1 -2
  2. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
  3. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
  4. package/docs/API.md +51 -14
  5. package/docs/COMPARISON.md +2 -2
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
  7. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
  8. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
  9. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
  10. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
  11. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
  12. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
  13. package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
  14. package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
  15. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
  16. package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
  17. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
  18. package/docs/GUIDE/index.md +18 -9
  19. package/docs/QUICKSTART.md +5 -1
  20. package/docs/index.md +1 -1
  21. package/index.js +3 -2
  22. package/package.json +2 -2
  23. package/plugins/core/connectors/express-plugin.js +97 -184
  24. package/plugins/core/connectors/fastify-plugin.js +206 -0
  25. package/plugins/core/connectors/lib/connector-core.js +195 -0
  26. package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
  27. package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
  28. package/plugins/core/lib/dbIntrospection.js +1078 -0
  29. package/plugins/core/lib/dbTablesOperations.js +1104 -160
  30. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
  31. package/plugins/core/lib/querying/knex-process-includes.js +3 -4
  32. package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
  33. package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
  34. package/plugins/core/lib/querying/query-builder-utils.js +19 -0
  35. package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
  36. package/plugins/core/lib/querying/sort-helpers.js +33 -0
  37. package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
  38. package/plugins/core/lib/querying/url-helpers.js +8 -2
  39. package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
  40. package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
  41. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
  42. package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
  43. package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
  44. package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
  45. package/plugins/core/lib/storage/storage-adapter.js +79 -2
  46. package/plugins/core/lib/storage/storage-mapping.js +204 -0
  47. package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
  48. package/plugins/core/rest-api-autofilter-plugin.js +414 -0
  49. package/plugins/core/rest-api-knex-plugin.js +249 -211
  50. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
  51. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
  52. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
  53. package/plugins/core/rest-api-plugin-methods/common.js +121 -83
  54. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
  55. package/plugins/core/rest-api-plugin-methods/get.js +23 -14
  56. package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
  57. package/plugins/core/rest-api-plugin-methods/post.js +13 -8
  58. package/plugins/core/rest-api-plugin-methods/put.js +45 -21
  59. package/plugins/core/rest-api-plugin-methods/query.js +15 -53
  60. package/plugins/core/rest-api-plugin.js +2 -0
  61. package/plugins/core/rest-api-positioning-plugin.js +1 -1
  62. package/plugins/core/rest-api-query-projections-plugin.js +130 -0
  63. package/plugins/core/socketio-plugin.js +1 -3
  64. package/tests/autofilter.test.js +463 -0
  65. package/tests/custom-id-simplified-post.test.js +75 -0
  66. package/tests/custom-idproperty-relationships.test.js +33 -0
  67. package/tests/db-introspection.test.js +518 -0
  68. package/tests/db-table-operations.test.js +781 -0
  69. package/tests/fastify-plugin.test.js +141 -0
  70. package/tests/field-getters.test.js +1 -2
  71. package/tests/field-setters.test.js +0 -4
  72. package/tests/fixtures/api-configs.js +188 -219
  73. package/tests/helpers/fake-fastify.js +99 -0
  74. package/tests/http-connectors-parity.test.js +235 -0
  75. package/tests/projected-fields.test.js +319 -0
  76. package/tests/relationships.test.js +3 -3
  77. package/tests/searchschema-knex-behavior.test.js +187 -0
  78. package/tests/searchschema-merge.test.js +21 -69
  79. package/tests/storage-mapping.test.js +210 -0
  80. package/audit/00-charter.md +0 -103
  81. package/audit/01-scope-map.md +0 -84
  82. package/audit/02-coverage-matrix.csv +0 -174
  83. package/audit/03-invariants.md +0 -123
  84. package/audit/04-standards-rubric.md +0 -56
  85. package/audit/05-competitor-rubric.md +0 -77
  86. package/audit/06-findings.md +0 -160
  87. package/audit/07-risk-register.csv +0 -10
  88. package/audit/08-remediation-roadmap.md +0 -177
  89. package/audit/09-signoff-check.md +0 -23
  90. package/audit/10-final-synthesis.md +0 -29
  91. package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
  92. package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
  93. package/audit/evidence/code/boundary-leak-checks.txt +0 -72
  94. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
  95. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
  96. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
  97. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
  98. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
  99. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
  100. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
  101. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
  102. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
  103. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
  104. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
  105. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
  106. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
  107. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
  108. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
  109. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
  110. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
  111. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
  112. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
  113. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
  114. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
  115. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
  116. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
  117. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
  118. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
  119. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
  120. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
  121. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
  122. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
  123. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
  124. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
  125. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
  126. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
  127. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
  128. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
  129. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
  130. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
  131. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
  132. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
  133. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
  134. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
  135. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
  136. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
  137. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
  138. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
  139. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
  140. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
  141. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
  142. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
  143. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
  144. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
  145. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
  146. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
  147. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
  148. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
  149. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
  150. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
  151. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
  152. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
  153. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
  154. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
  155. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
  156. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
  157. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
  158. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
  159. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
  160. package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
  161. package/audit/evidence/code/critical-pass1-summary.csv +0 -67
  162. package/audit/evidence/code/critical-pass2-summary.csv +0 -67
  163. package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
  164. package/audit/evidence/code/error-model-signals.txt +0 -98
  165. package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
  166. package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
  167. package/audit/evidence/code/hook-flow-signals.txt +0 -282
  168. package/audit/evidence/code/idproperty-signals.txt +0 -157
  169. package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
  170. package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
  171. package/audit/evidence/code/imports-json-rest-api.txt +0 -354
  172. package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
  173. package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
  174. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
  175. package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
  176. package/audit/evidence/code/permission-signals.txt +0 -193
  177. package/audit/evidence/code/query-include-signals.txt +0 -687
  178. package/audit/evidence/code/transaction-signals.txt +0 -28
  179. package/audit/evidence/code/validation-signals.txt +0 -650
  180. package/audit/evidence/code/write-path-signals.txt +0 -61
  181. package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
  182. package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
  183. package/audit/evidence/external/corpus-freeze.md +0 -21
  184. package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
  185. package/audit/evidence/external/npm-download-signals.txt +0 -23
  186. package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
  187. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
  188. package/audit/evidence/external/readme-fortune-signals.txt +0 -26
  189. package/audit/evidence/external/readme-fortune.md +0 -165
  190. package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
  191. package/audit/evidence/external/readme-json-api-serializer.md +0 -690
  192. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  193. package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
  194. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
  195. package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
  196. package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
  197. package/audit/evidence/external/readme-jsonapi-server.md +0 -104
  198. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
  199. package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
  200. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
  201. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
  202. package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
  203. package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
  204. package/audit/evidence/external/standards-sources.txt +0 -6
  205. package/audit/evidence/runtime/baseline-commits.txt +0 -2
  206. package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
  207. package/audit/evidence/runtime/environment.txt +0 -4
  208. package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
  209. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
  210. package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
  211. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
  212. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
  213. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
  214. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
  215. package/audit/evidence/runtime/test-exitcodes.txt +0 -4
  216. package/audit/evidence/runtime/test-summary.md +0 -107
  217. package/audit/evidence/runtime/trace-anchors.txt +0 -61
  218. package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
  219. package/audit/evidence/tests/failing-tests-summary.txt +0 -25
  220. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
  221. package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
  222. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
  223. package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
  224. package/audit/evidence/tests/query-include-test-signals.txt +0 -948
  225. package/audit/evidence/tests/relationship-test-signals.txt +0 -223
  226. package/audit/evidence/tests/skipped-tests.txt +0 -10
  227. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
  228. package/audit/evidence/tests/test-suite-list.txt +0 -32
  229. package/audit/subsystems/docs-drift.md +0 -49
  230. package/audit/subsystems/pass2-cross-review.md +0 -131
  231. package/audit/subsystems/plugins-access.md +0 -51
  232. package/audit/subsystems/plugins-bulk.md +0 -50
  233. package/audit/subsystems/plugins-cors.md +0 -48
  234. package/audit/subsystems/plugins-file.md +0 -53
  235. package/audit/subsystems/plugins-multihome.md +0 -50
  236. package/audit/subsystems/plugins-positioning.md +0 -54
  237. package/audit/subsystems/plugins-socketio.md +0 -50
  238. package/audit/subsystems/query-engine.md +0 -61
  239. package/audit/subsystems/relationships.md +0 -55
  240. package/audit/subsystems/rest-orchestration.md +0 -66
  241. package/audit/subsystems/schema-core.md +0 -64
  242. package/audit/subsystems/storage-knex.md +0 -57
  243. package/audit/subsystems/tests.md +0 -48
  244. package/audit/subsystems/transport-express.md +0 -55
  245. package/audit/traces/trace-read-get.md +0 -22
  246. package/audit/traces/trace-read-query.md +0 -25
  247. package/audit/traces/trace-relationship-endpoints.md +0 -31
  248. package/audit/traces/trace-write-delete.md +0 -17
  249. package/audit/traces/trace-write-patch.md +0 -26
  250. package/audit/traces/trace-write-post.md +0 -22
  251. package/audit/traces/trace-write-put.md +0 -23
  252. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
  253. package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
  254. package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
  255. package/plugins/core/rest-api-access.js +0 -459
  256. package/plugins/core/rest-api-multihome-plugin.js +0 -314
  257. package/tests/multihome.test.js +0 -553
  258. package/tests/multihome.test.js.bak +0 -550
  259. package/tests/ownership-optout.test.js +0 -76
  260. package/tests/rest-api-access.test.js +0 -206
@@ -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
- const translateSelectFieldsForAdapter = (fields, adapter) => {
85
- if (!adapter || !fields) return fields
98
+ return getStorageIdColumn(scopes[scopeName]?.vars?.schemaInfo || {})
99
+ }
86
100
 
87
- const translateField = (field) => {
88
- if (typeof field !== 'string') return field
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
- if (field === '*') return '*'
107
+ return getStorageFieldValue(record, scopes[scopeName]?.vars?.schemaInfo || {}, fieldName)
108
+ }
91
109
 
92
- const aliasMatch = field.match(/\s+as\s+/i)
93
- if (aliasMatch) {
94
- const [source, alias] = field.split(/\s+as\s+/i)
95
- const translatedSource = adapter.translateColumn(source.trim()) || source.trim()
96
- return `${translatedSource} as ${alias.trim()}`
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
- const translated = adapter.translateColumn(field)
100
- return translated || field
134
+ if ('filters' in queryParams) {
135
+ delete queryParams.filters
101
136
  }
102
137
 
103
- return fields.map(translateField)
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[typeField]
170
- const id = record[idField]
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[fieldName]
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[typeField]
324
- const id = record[idField]
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, idProperty } = scope
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(r => r[fieldName])
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 targetIdProperty = scopes[targetScope].vars.schemaInfo.idProperty
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(targetIdProperty, uniqueIds)
512
+ let query = knex(targetTableName).whereIn(targetIdColumn, uniqueIds)
441
513
  if (fieldSelectionInfo) {
442
- const selectFields = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter)
443
- query = query.select(selectFields)
444
- } else if (targetIdProperty !== 'id') {
445
- // If no field selection but custom idProperty, we need to alias it
446
- query = query.select('*', `${targetIdProperty} as id`)
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
- targetRecords.forEach(record => {
456
- targetById[record.id || record[targetIdProperty]] = record
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[fieldName]
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, idProperty: targetIdProperty },
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(r => r.id).filter(Boolean)
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 targetIdProperty = targetScopeObject.vars.schemaInfo.idProperty
660
- const fieldSelectionInfo = fields?.[targetScope]
661
- ? await buildFieldSelection(targetScopeObject, {
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
- const targetIdProperty = scopes[targetScope]?.vars?.schemaInfo?.idProperty || 'id'
694
- const windowQuery = knex
778
+ let windowQuery = knex
695
779
  .select(`${targetTable}.*`)
696
780
 
697
781
  // Add id alias if needed
698
- if (targetIdProperty !== 'id') {
699
- windowQuery.select(`${targetTable}.${targetIdProperty} as id`)
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 || [targetIdProperty], targetTable) +
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}.${targetIdProperty}`, 'pivot.' + otherKey)
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
- const targetIdProperty = scopes[targetScope]?.vars?.schemaInfo?.idProperty || 'id'
727
- let query = knex(targetTable).whereIn(targetIdProperty, targetIds)
820
+ let query = knex(targetTable).whereIn(targetIdColumn, targetIds)
728
821
 
729
822
  if (fieldSelectionInfo) {
730
- const selectFields = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter)
731
- query = query.select(selectFields)
732
- } else if (targetIdProperty !== 'id') {
733
- // If no field selection but custom idProperty, we need to alias it
734
- query = query.select('*', `${targetIdProperty} as id`)
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.id] = 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 childIds = pivotsByParent[record.id] || []
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 resourceKey = `${targetScope}:${childRecord.id}`
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(childRecord.id) }
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}:${record.id}`
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, idProperty: targetIdProperty },
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
- const selectFields = fieldSelectionInfo
877
- ? translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter)
878
- : null
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
- query = knex(targetTable).whereIn(foreignKey, mainIds)
1034
+ const foreignKeyColumn = translateColumnForScope(scopes, targetScope, foreignKey)
1035
+ query = knex(targetTable).whereIn(foreignKeyColumn, mainIds)
909
1036
 
910
1037
  if (fieldSelectionInfo) {
911
- const selectFieldsStd = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter)
912
- query = query.select(selectFieldsStd)
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[foreignKey]
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 children = childrenByParent[record.id] || []
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 resourceKey = `${targetScope}:${childRecord.id}`
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(childRecord.id) }
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}:${record.id}`
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, idProperty: targetIdProperty },
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 idProperty = scopes[scopeName]?.vars?.schemaInfo?.idProperty || 'id'
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
- let query = knex(targetTable).whereIn(foreignKey, mainIds)
1248
+ const foreignKeyColumn = translateColumnForScope(scopes, targetScope, foreignKey)
1249
+ let query = knex(targetTable).whereIn(foreignKeyColumn, mainIds)
1100
1250
 
1101
- if (fieldSelectionInfo?.fieldsToSelect) {
1102
- const selectFields = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter)
1103
- query = query.select(selectFields)
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[foreignKey]
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[idProperty]
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(relatedRecord[targetIdProperty]) }
1316
+ data: { type: targetScope, id: String(relatedRecordId) }
1141
1317
  }
1142
1318
 
1143
1319
  // Add to included if not already there
1144
- const key = `${targetScope}:${relatedRecord[targetIdProperty]}`
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 targetIdProperty = scopes[targetType]?.vars?.schemaInfo?.idProperty || 'id'
1247
- let query = knex(targetTable).whereIn(targetIdProperty, targetIds)
1423
+ const targetIdColumn = getIdColumnForScope(scopes, targetType)
1424
+ let query = knex(targetTable).whereIn(targetIdColumn, targetIds)
1248
1425
  if (fieldSelectionInfo) {
1249
- const selectFields = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter)
1250
- query = query.select(selectFields)
1251
- } else if (targetIdProperty !== 'id') {
1252
- // If no field selection but custom idProperty, we need to alias it
1253
- query = query.select('*', `${targetIdProperty} as id`)
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.id] = 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[typeField] === targetType) {
1269
- const targetId = record[idField]
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, idProperty: targetIdProperty },
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(r => r.id).filter(Boolean)
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 targetIdProperty = targetScopeObject.vars.schemaInfo.idProperty
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(typeField, scopeName)
1461
- .whereIn(idField, parentIds)
1652
+ .where(typeColumn, scopeName)
1653
+ .whereIn(idColumn, parentIds)
1462
1654
 
1463
1655
  if (fieldSelectionInfo) {
1464
- const selectFields = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter)
1465
- query = query.select(selectFields)
1466
- } else if (targetIdProperty !== 'id') {
1467
- // If no field selection but custom idProperty, we need to alias it
1468
- query = query.select('*', `${targetIdProperty} as id`)
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[idField]
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 children = childrenByParent[record.id] || []
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 resourceKey = `${targetScope}:${childRecord.id}`
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(childRecord.id) }
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, idProperty: targetIdProperty },
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 = {}, idProperty } = scope
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, idProperty },
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, idProperty } = scope
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, idProperty },
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 targetIdProperty = targetScope?.vars?.schemaInfo?.idProperty || 'id'
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 knex(targetTable)
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[foreignKey])
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 results = await knex(pivotTable)
1926
- .whereIn(fk, recordIds)
1927
- .select(fk, ok)
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[fk])
1931
- const childId = String(row[ok])
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 targetIdProperty = scopes[targetScope]?.vars?.schemaInfo?.idProperty || 'id'
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 knex(targetTable)
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[idField])
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.id)] || []
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