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.
Files changed (290) hide show
  1. package/CLAUDE.md +1 -2
  2. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
  3. package/README.md +22 -1
  4. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
  5. package/docs/API.md +196 -38
  6. package/docs/COMPARISON.md +2 -2
  7. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +111 -5
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
  9. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
  10. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
  11. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
  12. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
  13. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
  14. package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
  15. package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
  16. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +370 -0
  17. package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
  18. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
  19. package/docs/GUIDE/index.md +18 -9
  20. package/docs/QUICKSTART.md +5 -1
  21. package/docs/index.md +1 -1
  22. package/index.js +3 -2
  23. package/lib/enhanced-logger.js +1 -1
  24. package/package.json +6 -2
  25. package/plugins/core/bulk-operations-plugin.js +26 -34
  26. package/plugins/core/connectors/express-plugin.js +97 -190
  27. package/plugins/core/connectors/fastify-plugin.js +206 -0
  28. package/plugins/core/connectors/lib/busboy-detector.js +4 -4
  29. package/plugins/core/connectors/lib/connector-core.js +195 -0
  30. package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
  31. package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
  32. package/plugins/core/lib/anyapi/schema-utils.js +43 -4
  33. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +4 -1
  34. package/plugins/core/lib/dbIntrospection.js +1078 -0
  35. package/plugins/core/lib/dbTablesOperations.js +1111 -160
  36. package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
  37. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -3
  38. package/plugins/core/lib/querying/knex-process-includes.js +3 -4
  39. package/plugins/core/lib/querying/knex-query-helpers.js +21 -76
  40. package/plugins/core/lib/querying/knex-relationship-includes.js +468 -215
  41. package/plugins/core/lib/querying/query-builder-utils.js +19 -0
  42. package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
  43. package/plugins/core/lib/querying/sort-helpers.js +33 -0
  44. package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
  45. package/plugins/core/lib/querying/url-helpers.js +8 -2
  46. package/plugins/core/lib/querying-writing/compile-schemas.js +20 -5
  47. package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -14
  48. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
  49. package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
  50. package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
  51. package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
  52. package/plugins/core/lib/querying-writing/schema-helpers.js +0 -2
  53. package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
  54. package/plugins/core/lib/storage/canonical-storage-mapping.js +133 -0
  55. package/plugins/core/lib/storage/storage-adapter.js +82 -4
  56. package/plugins/core/lib/storage/storage-mapping.js +274 -0
  57. package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -2
  58. package/plugins/core/rest-api-anyapi-knex-plugin.js +531 -312
  59. package/plugins/core/rest-api-autofilter-plugin.js +414 -0
  60. package/plugins/core/rest-api-knex-plugin.js +271 -226
  61. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
  62. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
  63. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +1 -0
  64. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
  65. package/plugins/core/rest-api-plugin-methods/common.js +148 -92
  66. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +11 -2
  67. package/plugins/core/rest-api-plugin-methods/delete.js +6 -1
  68. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
  69. package/plugins/core/rest-api-plugin-methods/get-related.js +7 -2
  70. package/plugins/core/rest-api-plugin-methods/get-relationship.js +7 -2
  71. package/plugins/core/rest-api-plugin-methods/get.js +29 -14
  72. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +10 -3
  73. package/plugins/core/rest-api-plugin-methods/patch.js +56 -12
  74. package/plugins/core/rest-api-plugin-methods/post-relationship.js +11 -2
  75. package/plugins/core/rest-api-plugin-methods/post.js +21 -8
  76. package/plugins/core/rest-api-plugin-methods/put.js +73 -21
  77. package/plugins/core/rest-api-plugin-methods/query.js +15 -53
  78. package/plugins/core/rest-api-plugin-methods/release.js +0 -2
  79. package/plugins/core/rest-api-plugin.js +6 -0
  80. package/plugins/core/rest-api-positioning-plugin.js +2 -5
  81. package/plugins/core/rest-api-query-projections-plugin.js +130 -0
  82. package/plugins/core/socketio-plugin.js +1 -4
  83. package/plugins/storage/local-storage.js +1 -1
  84. package/plugins/storage/s3-storage.js +5 -2
  85. package/quickTest.js +1 -0
  86. package/scripts/run-docs.js +80 -84
  87. package/test-debug.js +1 -1
  88. package/tests/README.md +7 -0
  89. package/tests/anyapi-basic.test.js +3 -0
  90. package/tests/autofilter.test.js +463 -0
  91. package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
  92. package/tests/custom-id-simplified-post.test.js +75 -0
  93. package/tests/custom-idproperty-relationships.test.js +33 -0
  94. package/tests/db-introspection.test.js +521 -0
  95. package/tests/db-table-operations.test.js +786 -0
  96. package/tests/fastify-plugin.test.js +156 -0
  97. package/tests/field-getters.test.js +1 -2
  98. package/tests/field-setters.test.js +0 -4
  99. package/tests/fixtures/api-configs.js +278 -176
  100. package/tests/helpers/fake-fastify.js +99 -0
  101. package/tests/http-connectors-parity.test.js +235 -0
  102. package/tests/id-normalization.test.js +444 -0
  103. package/tests/logical-id-write-validation.test.js +151 -0
  104. package/tests/pagination-cursor-multifield.test.js +11 -11
  105. package/tests/projected-fields.test.js +319 -0
  106. package/tests/relationships.test.js +3 -3
  107. package/tests/searchschema-knex-behavior.test.js +187 -0
  108. package/tests/searchschema-merge.test.js +21 -69
  109. package/tests/storage-mapping.test.js +259 -0
  110. package/audit/00-charter.md +0 -103
  111. package/audit/01-scope-map.md +0 -84
  112. package/audit/02-coverage-matrix.csv +0 -174
  113. package/audit/03-invariants.md +0 -123
  114. package/audit/04-standards-rubric.md +0 -56
  115. package/audit/05-competitor-rubric.md +0 -77
  116. package/audit/06-findings.md +0 -160
  117. package/audit/07-risk-register.csv +0 -10
  118. package/audit/08-remediation-roadmap.md +0 -177
  119. package/audit/09-signoff-check.md +0 -23
  120. package/audit/10-final-synthesis.md +0 -29
  121. package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
  122. package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
  123. package/audit/evidence/code/boundary-leak-checks.txt +0 -72
  124. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
  125. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
  126. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
  127. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
  128. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
  129. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
  130. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
  131. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
  132. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
  133. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
  134. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
  135. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
  136. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
  137. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
  138. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
  139. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
  140. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
  141. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
  142. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
  143. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
  144. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
  145. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
  146. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
  147. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
  148. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
  149. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
  150. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
  151. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
  152. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
  153. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
  154. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
  155. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
  156. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
  157. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
  158. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
  159. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
  160. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
  161. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
  162. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
  163. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
  164. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
  165. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
  166. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
  167. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
  168. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
  169. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
  170. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
  171. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
  172. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
  173. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
  174. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
  175. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
  176. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
  177. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
  178. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
  179. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
  180. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
  181. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
  182. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
  183. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
  184. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
  185. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
  186. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
  187. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
  188. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
  189. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
  190. package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
  191. package/audit/evidence/code/critical-pass1-summary.csv +0 -67
  192. package/audit/evidence/code/critical-pass2-summary.csv +0 -67
  193. package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
  194. package/audit/evidence/code/error-model-signals.txt +0 -98
  195. package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
  196. package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
  197. package/audit/evidence/code/hook-flow-signals.txt +0 -282
  198. package/audit/evidence/code/idproperty-signals.txt +0 -157
  199. package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
  200. package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
  201. package/audit/evidence/code/imports-json-rest-api.txt +0 -354
  202. package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
  203. package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
  204. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
  205. package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
  206. package/audit/evidence/code/permission-signals.txt +0 -193
  207. package/audit/evidence/code/query-include-signals.txt +0 -687
  208. package/audit/evidence/code/transaction-signals.txt +0 -28
  209. package/audit/evidence/code/validation-signals.txt +0 -650
  210. package/audit/evidence/code/write-path-signals.txt +0 -61
  211. package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
  212. package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
  213. package/audit/evidence/external/corpus-freeze.md +0 -21
  214. package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
  215. package/audit/evidence/external/npm-download-signals.txt +0 -23
  216. package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
  217. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
  218. package/audit/evidence/external/readme-fortune-signals.txt +0 -26
  219. package/audit/evidence/external/readme-fortune.md +0 -165
  220. package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
  221. package/audit/evidence/external/readme-json-api-serializer.md +0 -690
  222. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  223. package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
  224. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
  225. package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
  226. package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
  227. package/audit/evidence/external/readme-jsonapi-server.md +0 -104
  228. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
  229. package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
  230. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
  231. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
  232. package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
  233. package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
  234. package/audit/evidence/external/standards-sources.txt +0 -6
  235. package/audit/evidence/runtime/baseline-commits.txt +0 -2
  236. package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
  237. package/audit/evidence/runtime/environment.txt +0 -4
  238. package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
  239. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
  240. package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
  241. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
  242. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
  243. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
  244. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
  245. package/audit/evidence/runtime/test-exitcodes.txt +0 -4
  246. package/audit/evidence/runtime/test-summary.md +0 -107
  247. package/audit/evidence/runtime/trace-anchors.txt +0 -61
  248. package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
  249. package/audit/evidence/tests/failing-tests-summary.txt +0 -25
  250. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
  251. package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
  252. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
  253. package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
  254. package/audit/evidence/tests/query-include-test-signals.txt +0 -948
  255. package/audit/evidence/tests/relationship-test-signals.txt +0 -223
  256. package/audit/evidence/tests/skipped-tests.txt +0 -10
  257. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
  258. package/audit/evidence/tests/test-suite-list.txt +0 -32
  259. package/audit/subsystems/docs-drift.md +0 -49
  260. package/audit/subsystems/pass2-cross-review.md +0 -131
  261. package/audit/subsystems/plugins-access.md +0 -51
  262. package/audit/subsystems/plugins-bulk.md +0 -50
  263. package/audit/subsystems/plugins-cors.md +0 -48
  264. package/audit/subsystems/plugins-file.md +0 -53
  265. package/audit/subsystems/plugins-multihome.md +0 -50
  266. package/audit/subsystems/plugins-positioning.md +0 -54
  267. package/audit/subsystems/plugins-socketio.md +0 -50
  268. package/audit/subsystems/query-engine.md +0 -61
  269. package/audit/subsystems/relationships.md +0 -55
  270. package/audit/subsystems/rest-orchestration.md +0 -66
  271. package/audit/subsystems/schema-core.md +0 -64
  272. package/audit/subsystems/storage-knex.md +0 -57
  273. package/audit/subsystems/tests.md +0 -48
  274. package/audit/subsystems/transport-express.md +0 -55
  275. package/audit/traces/trace-read-get.md +0 -22
  276. package/audit/traces/trace-read-query.md +0 -25
  277. package/audit/traces/trace-relationship-endpoints.md +0 -31
  278. package/audit/traces/trace-write-delete.md +0 -17
  279. package/audit/traces/trace-write-patch.md +0 -26
  280. package/audit/traces/trace-write-post.md +0 -22
  281. package/audit/traces/trace-write-put.md +0 -23
  282. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
  283. package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
  284. package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
  285. package/plugins/core/rest-api-access.js +0 -459
  286. package/plugins/core/rest-api-multihome-plugin.js +0 -314
  287. package/tests/multihome.test.js +0 -553
  288. package/tests/multihome.test.js.bak +0 -550
  289. package/tests/ownership-optout.test.js +0 -76
  290. 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
- const translateSelectFieldsForAdapter = (fields, adapter) => {
85
- if (!adapter || !fields) return fields
97
+ return getStorageIdColumn(scopes[scopeName]?.vars?.schemaInfo || {})
98
+ }
86
99
 
87
- const translateField = (field) => {
88
- if (typeof field !== 'string') return field
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
- if (field === '*') return '*'
106
+ return getStorageFieldValue(record, scopes[scopeName]?.vars?.schemaInfo || {}, fieldName)
107
+ }
91
108
 
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
- }
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
- const translated = adapter.translateColumn(field)
100
- return translated || field
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
- return fields.map(translateField)
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[typeField]
170
- const id = record[idField]
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[fieldName]
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[typeField]
324
- const id = record[idField]
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, idProperty } = scope
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(r => r[fieldName])
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 targetIdProperty = scopes[targetScope].vars.schemaInfo.idProperty
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(targetIdProperty, uniqueIds)
509
+ let query = knex(targetTableName).whereIn(targetIdColumn, uniqueIds)
441
510
  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`)
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[record.id || record[targetIdProperty]] = record
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[fieldName]
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, idProperty: targetIdProperty },
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(r => r.id).filter(Boolean)
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 '${relName}' for scope '${scopeName}'`)
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 targetIdProperty = targetScopeObject.vars.schemaInfo.idProperty
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
- const targetIdProperty = scopes[targetScope]?.vars?.schemaInfo?.idProperty || 'id'
694
- const windowQuery = knex
774
+ let windowQuery = knex
695
775
  .select(`${targetTable}.*`)
696
776
 
697
777
  // Add id alias if needed
698
- if (targetIdProperty !== 'id') {
699
- windowQuery.select(`${targetTable}.${targetIdProperty} as id`)
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 || [targetIdProperty], targetTable) +
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}.${targetIdProperty}`, 'pivot.' + otherKey)
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
- const targetIdProperty = scopes[targetScope]?.vars?.schemaInfo?.idProperty || 'id'
727
- let query = knex(targetTable).whereIn(targetIdProperty, targetIds)
816
+ let query = knex(targetTable).whereIn(targetIdColumn, targetIds)
728
817
 
729
818
  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`)
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.id] = 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 childIds = pivotsByParent[record.id] || []
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 resourceKey = `${targetScope}:${childRecord.id}`
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(childRecord.id) }
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}:${record.id}`
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, idProperty: targetIdProperty },
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 '${relName}' for scope '${scopeName}'`)
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
- const selectFields = fieldSelectionInfo
877
- ? translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter)
878
- : null
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
- query = knex(targetTable).whereIn(foreignKey, mainIds)
1029
+ const foreignKeyColumn = translateColumnForScope(scopes, targetScope, foreignKey)
1030
+ query = knex(targetTable).whereIn(foreignKeyColumn, mainIds)
909
1031
 
910
1032
  if (fieldSelectionInfo) {
911
- const selectFieldsStd = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter)
912
- query = query.select(selectFieldsStd)
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[foreignKey]
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 children = childrenByParent[record.id] || []
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 resourceKey = `${targetScope}:${childRecord.id}`
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(childRecord.id) }
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}:${record.id}`
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, idProperty: targetIdProperty },
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
- log = log?.child ? log.child({ method: 'loadHasOne' }) : console
1197
+ const childLog = log?.child ? log.child({ method: 'loadHasOne' }) : console
1053
1198
 
1054
1199
  if (!records || records.length === 0) {
1055
- log.trace('[INCLUDE] No records to load hasOne for')
1200
+ childLog.trace('[INCLUDE] No records to load hasOne for')
1056
1201
  return
1057
1202
  }
1058
1203
 
1059
1204
  // Get parent IDs
1060
- const idProperty = scopes[scopeName]?.vars?.schemaInfo?.idProperty || 'id'
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
- log.trace('[INCLUDE] No parent IDs found, skipping hasOne load')
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
- log.debug(`[INCLUDE] Loading ${targetScope} records with foreign key ${foreignKey}:`, {
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
- let query = knex(targetTable).whereIn(foreignKey, mainIds)
1242
+ const foreignKeyColumn = translateColumnForScope(scopes, targetScope, foreignKey)
1243
+ let query = knex(targetTable).whereIn(foreignKeyColumn, mainIds)
1100
1244
 
1101
- if (fieldSelectionInfo?.fieldsToSelect) {
1102
- const selectFields = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter)
1103
- query = query.select(selectFields)
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[foreignKey]
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[idProperty]
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(relatedRecord[targetIdProperty]) }
1310
+ data: { type: targetScope, id: String(relatedRecordId) }
1141
1311
  }
1142
1312
 
1143
1313
  // Add to included if not already there
1144
- const key = `${targetScope}:${relatedRecord[targetIdProperty]}`
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 targetIdProperty = scopes[targetType]?.vars?.schemaInfo?.idProperty || 'id'
1247
- let query = knex(targetTable).whereIn(targetIdProperty, targetIds)
1417
+ const targetIdColumn = getIdColumnForScope(scopes, targetType)
1418
+ let query = knex(targetTable).whereIn(targetIdColumn, targetIds)
1248
1419
  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`)
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.id] = 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[typeField] === targetType) {
1269
- const targetId = record[idField]
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, idProperty: targetIdProperty },
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(r => r.id).filter(Boolean)
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 targetIdProperty = targetScopeObject.vars.schemaInfo.idProperty
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(typeField, scopeName)
1461
- .whereIn(idField, parentIds)
1645
+ .where(typeColumn, scopeName)
1646
+ .whereIn(idColumn, parentIds)
1462
1647
 
1463
1648
  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`)
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[idField]
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 children = childrenByParent[record.id] || []
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 resourceKey = `${targetScope}:${childRecord.id}`
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(childRecord.id) }
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, idProperty: targetIdProperty },
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 = {}, idProperty } = scope
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, idProperty },
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, idProperty } = scope
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, idProperty },
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 targetIdProperty = targetScope?.vars?.schemaInfo?.idProperty || 'id'
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 knex(targetTable)
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[foreignKey])
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 results = await knex(pivotTable)
1926
- .whereIn(fk, recordIds)
1927
- .select(fk, ok)
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[fk])
1931
- const childId = String(row[ok])
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 targetIdProperty = scopes[targetScope]?.vars?.schemaInfo?.idProperty || 'id'
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 knex(targetTable)
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[idField])
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.id)] || []
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