json-rest-api 1.0.29 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +733 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +103 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +37 -0
  44. package/index.js +3 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +70 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. package/tests/virtual-fields.test.js +0 -404
@@ -0,0 +1,428 @@
1
+ // Collection loaders preserve per-parent filtering/order/limits; deps.loadNestedIncludes traverses child paths.
2
+ import { whereInIdentifiers } from './identifier-query.js'
3
+ import { toJsonApiRecord } from './knex-json-api-transformers-querying.js'
4
+ import {
5
+ RELATIONSHIPS_KEY,
6
+ RELATIONSHIP_METADATA_KEY,
7
+ COMPUTED_DEPENDENCIES_KEY,
8
+ } from '../querying-writing/knex-constants.js'
9
+ import { wrapUnexpectedError } from '../../../../lib/error-context.js'
10
+ import {
11
+ loadIncludedParentMap,
12
+ getIdColumnForScope,
13
+ getFieldValueForScope,
14
+ translateColumnForScope,
15
+ prepareCollectionInclude,
16
+ buildRelationshipLinks,
17
+ applyScopeFiltersToIncludeQuery,
18
+ loadRelationshipMetadata,
19
+ } from './include-query-helpers.js'
20
+ /**
21
+ * Loads hasMany relationships (one-to-many or many-to-many)
22
+ *
23
+ * This function loads the "child" side of a relationship. For example, if articles
24
+ * have many comments, this loads all comments for a set of articles. It also handles
25
+ * many-to-many relationships through a pivot table.
26
+ *
27
+ * @param {Object} scope - The resource object containing:
28
+ * - records: Array<Object> - Parent records to load relationships for
29
+ * - scopeName: string - The parent scope name
30
+ * - includeName: string - The relationship name to include
31
+ * - relDef: Object - The relationship definition
32
+ * - subIncludes: Object - Nested includes to process recursively
33
+ * - included: Map - Map of already included resources
34
+ * - processedPaths: Set - Set of already processed paths
35
+ * - currentPath: string - Current include path for tracking
36
+ * - fields: Object - Sparse fieldsets configuration
37
+ * @param {Object} deps - Dependencies object containing:
38
+ * - context.scopes: Object - The resources object
39
+ * - context.log: Object - Logger instance
40
+ * - context.knex: Object - Knex instance
41
+ * @returns {Promise<void>}
42
+ */
43
+ export const loadHasMany = async (scope, deps) => {
44
+ const { records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
45
+ const { scopes, log, knex, capabilities, requestContext } = deps.context
46
+ try {
47
+ log.trace('[INCLUDE] Loading hasMany/manyToMany relationship:', {
48
+ scopeName,
49
+ includeName,
50
+ recordCount: records.length,
51
+ isManyToMany: relDef.type === 'manyToMany',
52
+ hasMany: relDef.type === 'hasMany' ? relDef.target : undefined
53
+ })
54
+
55
+ // Collect all parent IDs
56
+ const mainIds = records.map(record => getFieldValueForScope(scopes, scopeName, record, 'id')).filter(id => id !== null && id !== undefined)
57
+
58
+ if (mainIds.length === 0) {
59
+ log.trace('[INCLUDE] No parent IDs found, skipping hasMany load')
60
+ return
61
+ }
62
+
63
+ // Check if this is a many-to-many relationship
64
+ if (relDef.type === 'manyToMany') {
65
+ // Handle many-to-many relationship
66
+ const pivotTable = scopes[relDef.through].vars.schemaInfo.tableName
67
+ const foreignKey = translateColumnForScope(scopes, relDef.through, relDef.foreignKey)
68
+ const otherKey = translateColumnForScope(scopes, relDef.through, relDef.otherKey)
69
+
70
+ if (!foreignKey || !otherKey) {
71
+ throw new Error(`Missing foreignKey or otherKey in many-to-many relationship '${includeName}' for scope '${scopeName}'`)
72
+ }
73
+
74
+ const targetScope = relDef.target || includeName
75
+ const targetTable = scopes[targetScope].vars.schemaInfo.tableName
76
+
77
+ log.debug(`[INCLUDE] Loading pivot records from ${pivotTable}:`, {
78
+ foreignKey,
79
+ parentCount: mainIds.length
80
+ })
81
+
82
+ const pivotQuery = knex(pivotTable).modify(whereInIdentifiers, foreignKey, mainIds)
83
+ .select(foreignKey, otherKey).distinct()
84
+ const pivotState = await applyScopeFiltersToIncludeQuery({
85
+ query: pivotQuery, scopes, scopeName: relDef.through, requestContext, db: knex, tableName: pivotTable
86
+ })
87
+ const targetIdColumn = getIdColumnForScope(scopes, targetScope)
88
+ const perParent = relDef.include?.strategy === 'window'
89
+ const query = knex(targetTable)
90
+ if (perParent) query.join(pivotState.query.as('pivot'), `${targetTable}.${targetIdColumn}`, `pivot.${otherKey}`)
91
+ else query.whereIn(`${targetTable}.${targetIdColumn}`, knex.select(otherKey).from(pivotState.query.clone().as('include_candidates')))
92
+ const limited = await prepareCollectionInclude({
93
+ query, targetScope, parentColumn: perParent ? `pivot.${foreignKey}` : knex.raw('NULL'), includeConfig: relDef.include, fields
94
+ }, { scopes, knex, requestContext, capabilities })
95
+ const fieldSelectionInfo = limited.fieldSelectionInfo
96
+ const targetRecords = await limited.query
97
+ const parentsByChild = perParent
98
+ ? null
99
+ : await loadIncludedParentMap({
100
+ query: knex(targetTable).join(pivotState.query.clone().as('pivot'), `${targetTable}.${targetIdColumn}`, `pivot.${otherKey}`)
101
+ .select({ parentId: `pivot.${foreignKey}`, childId: `${targetTable}.${targetIdColumn}` }),
102
+ parentIds: mainIds,
103
+ childIds: targetRecords.map(record => getFieldValueForScope(scopes, targetScope, record, 'id')),
104
+ knex
105
+ })
106
+ const childrenByParent = Object.create(null)
107
+ for (const record of targetRecords) {
108
+ const parents = perParent ? [record[limited.parentColumn]] : parentsByChild.get(String(getFieldValueForScope(scopes, targetScope, record, 'id'))) || []
109
+ for (const parentId of parents) (childrenByParent[parentId] ||= []).push(record)
110
+ for (const field of limited.temporaryFields) delete record[field]
111
+ }
112
+
113
+ log.trace('[INCLUDE] Loaded target records:', { count: targetRecords.length })
114
+
115
+ // Step 5: Load relationship metadata for all target records
116
+ await loadRelationshipMetadata(scopes, targetRecords, targetScope)
117
+
118
+ records.forEach(record => {
119
+ if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
120
+
121
+ const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
122
+ const relData = (childrenByParent[recordId] || []).map(childRecord => {
123
+ // Add to included
124
+ const childRecordId = getFieldValueForScope(scopes, targetScope, childRecord, 'id')
125
+ const resourceKey = `${targetScope}:${childRecordId}`
126
+ if (!included.has(resourceKey)) {
127
+ const jsonApiRecord = toJsonApiRecord(
128
+ scopes[targetScope],
129
+ childRecord,
130
+ targetScope
131
+ )
132
+
133
+ // Add relationships from metadata
134
+ if (childRecord[RELATIONSHIP_METADATA_KEY]) {
135
+ jsonApiRecord.relationships = childRecord[RELATIONSHIP_METADATA_KEY]
136
+ // Clean up the temporary property
137
+ delete childRecord[RELATIONSHIP_METADATA_KEY]
138
+ }
139
+
140
+ // Attach computed dependencies info if sparse fieldsets were used
141
+ if (fieldSelectionInfo?.computedDependencies) {
142
+ jsonApiRecord[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
143
+ }
144
+
145
+ included.set(resourceKey, jsonApiRecord)
146
+ }
147
+ return { type: targetScope, id: String(childRecordId) }
148
+ })
149
+
150
+ const relationshipObject = { data: relData }
151
+
152
+ // Add links if urlPrefix is configured
153
+ if (scopeName) {
154
+ relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
155
+ }
156
+
157
+ record[RELATIONSHIPS_KEY][includeName] = relationshipObject
158
+
159
+ // Update the record in the included Map if it exists
160
+ const recordKey = `${scopeName}:${recordId}`
161
+ if (included.has(recordKey)) {
162
+ const existingRecord = included.get(recordKey)
163
+ if (!existingRecord.relationships) {
164
+ existingRecord.relationships = {}
165
+ }
166
+ existingRecord.relationships[includeName] = relationshipObject
167
+ }
168
+ })
169
+
170
+ // Step 8: Process nested includes if any
171
+ if (Object.keys(subIncludes).length > 0 && targetRecords.length > 0) {
172
+ const nextPath = `${currentPath}.${includeName}`
173
+ await deps.loadNestedIncludes(
174
+ { records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
175
+ { context: { scopes, log, knex, capabilities, requestContext } }
176
+ )
177
+ }
178
+ } else if (relDef.type === 'hasMany') {
179
+ // Handle one-to-many relationship
180
+ const targetScope = relDef.target
181
+ const targetTable = scopes[targetScope].vars.schemaInfo.tableName
182
+ const foreignKey = relDef.foreignKey
183
+
184
+ if (!foreignKey) {
185
+ throw new Error(`Missing foreignKey in hasMany relationship '${includeName}' for scope '${scopeName}'`)
186
+ }
187
+
188
+ log.debug(`[INCLUDE] Loading ${targetScope} records with foreign key ${foreignKey}:`, {
189
+ parentCount: mainIds.length
190
+ })
191
+
192
+ const foreignKeyColumn = translateColumnForScope(scopes, targetScope, foreignKey)
193
+ const query = knex(targetTable).modify(whereInIdentifiers, foreignKeyColumn, mainIds)
194
+ const limited = await prepareCollectionInclude({
195
+ query, targetScope, parentColumn: `${targetTable}.${foreignKeyColumn}`, includeConfig: relDef.include, fields
196
+ }, { scopes, knex, requestContext, capabilities })
197
+ const fieldSelectionInfo = limited.fieldSelectionInfo
198
+ const targetRecords = await limited.query
199
+ for (const record of targetRecords) {
200
+ for (const field of limited.temporaryFields) delete record[field]
201
+ }
202
+
203
+ // Load relationship metadata for all target records
204
+ await loadRelationshipMetadata(scopes, targetRecords, targetScope)
205
+
206
+ // Group by parent ID
207
+ const childrenByParent = Object.create(null)
208
+ targetRecords.forEach(record => {
209
+ const parentId = getFieldValueForScope(scopes, targetScope, record, foreignKey)
210
+ if (!childrenByParent[parentId]) {
211
+ childrenByParent[parentId] = []
212
+ }
213
+ childrenByParent[parentId].push(record)
214
+ })
215
+
216
+ // Set relationships on parent records
217
+ records.forEach(record => {
218
+ if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
219
+
220
+ const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
221
+ const children = childrenByParent[recordId] || []
222
+ const relData = children.map(childRecord => {
223
+ // Add to included
224
+ const childRecordId = getFieldValueForScope(scopes, targetScope, childRecord, 'id')
225
+ const resourceKey = `${targetScope}:${childRecordId}`
226
+ if (!included.has(resourceKey)) {
227
+ const jsonApiRecord = toJsonApiRecord(
228
+ scopes[targetScope],
229
+ childRecord,
230
+ targetScope
231
+ )
232
+
233
+ // Add relationships from metadata
234
+ if (childRecord[RELATIONSHIP_METADATA_KEY]) {
235
+ jsonApiRecord.relationships = childRecord[RELATIONSHIP_METADATA_KEY]
236
+ // Clean up the temporary property
237
+ delete childRecord[RELATIONSHIP_METADATA_KEY]
238
+ }
239
+
240
+ // Attach computed dependencies info if sparse fieldsets were used
241
+ if (fieldSelectionInfo?.computedDependencies) {
242
+ jsonApiRecord[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
243
+ }
244
+
245
+ included.set(resourceKey, jsonApiRecord)
246
+ }
247
+ return { type: targetScope, id: String(childRecordId) }
248
+ })
249
+
250
+ const relationshipObject = { data: relData }
251
+
252
+ // Add links if urlPrefix is configured
253
+ if (scopeName) {
254
+ relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
255
+ }
256
+
257
+ record[RELATIONSHIPS_KEY][includeName] = relationshipObject
258
+
259
+ // Update the record in the included Map if it exists
260
+ const recordKey = `${scopeName}:${recordId}`
261
+ if (included.has(recordKey)) {
262
+ const existingRecord = included.get(recordKey)
263
+ if (!existingRecord.relationships) {
264
+ existingRecord.relationships = {}
265
+ }
266
+ existingRecord.relationships[includeName] = relationshipObject
267
+ }
268
+ })
269
+
270
+ // Process nested includes
271
+ if (Object.keys(subIncludes).length > 0 && targetRecords.length > 0) {
272
+ const nextPath = `${currentPath}.${includeName}`
273
+ await deps.loadNestedIncludes(
274
+ { records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
275
+ { context: { scopes, log, knex, capabilities, requestContext } }
276
+ )
277
+ }
278
+ }
279
+ } catch (error) {
280
+ throw wrapUnexpectedError(error, {
281
+ message: `Failed to load hasMany relationship '${includeName}' for scope '${scopeName}'`,
282
+ context: { phase: 'include', scopeName, includeName, hasThrough: !!relDef.through }
283
+ })
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Loads reverse polymorphic relationships (via)
289
+ *
290
+ * Handles loading "child" records that have a polymorphic relationship back to the parent.
291
+ * For example, loading all comments (which can belong to articles or videos) for a specific article.
292
+ *
293
+ * @param {Object} scope - The resource object containing:
294
+ * - records: Array<Object> - Parent records
295
+ * - scopeName: string - The parent scope name
296
+ * - includeName: string - The relationship name
297
+ * - relDef: Object - The relationship definition with 'via' property
298
+ * - subIncludes: Object - Nested includes to process recursively
299
+ * - included: Map - Map of already included resources
300
+ * - processedPaths: Set - Set of already processed paths
301
+ * - currentPath: string - Current include path for tracking
302
+ * - fields: Object - Sparse fieldsets configuration
303
+ * @param {Object} deps - Dependencies object containing:
304
+ * - context.scopes: Object - The resources object
305
+ * - context.log: Object - Logger instance
306
+ * - context.knex: Object - Knex instance
307
+ * @returns {Promise<void>}
308
+ */
309
+ export const loadReversePolymorphic = async (scope, deps) => {
310
+ const { records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
311
+ const { scopes, log, knex, capabilities, requestContext } = deps.context
312
+ try {
313
+ log.trace('[INCLUDE] Loading reverse polymorphic (via):', {
314
+ scopeName,
315
+ includeName,
316
+ via: relDef.via,
317
+ recordCount: records.length
318
+ })
319
+
320
+ const targetScope = relDef.target
321
+ const viaRelName = relDef.via
322
+
323
+ // Get the polymorphic field info from target scope
324
+ const targetRelationships = scopes[targetScope].vars.schemaInfo.schemaRelationships
325
+ const viaRel = targetRelationships?.[viaRelName]
326
+
327
+ if (!viaRel?.belongsToPolymorphic) {
328
+ throw new Error(`Reverse relationship '${scopeName}.${includeName}' requires polymorphic relationship '${targetScope}.${viaRelName}'`)
329
+ }
330
+
331
+ const { typeField, idField } = viaRel.belongsToPolymorphic
332
+ const targetTable = scopes[targetScope].vars.schemaInfo.tableName
333
+
334
+ // Collect parent IDs
335
+ const parentIds = records.map(record => getFieldValueForScope(scopes, scopeName, record, 'id')).filter(id => id !== null && id !== undefined)
336
+ if (parentIds.length === 0) return
337
+
338
+ log.debug(`[INCLUDE] Loading ${targetScope} records via ${viaRelName}:`, {
339
+ typeField,
340
+ idField,
341
+ scopeName,
342
+ parentCount: parentIds.length
343
+ })
344
+
345
+ const typeColumn = translateColumnForScope(scopes, targetScope, typeField)
346
+ const idColumn = translateColumnForScope(scopes, targetScope, idField)
347
+ const query = knex(targetTable).where(typeColumn, scopeName).modify(whereInIdentifiers, idColumn, parentIds)
348
+ const limited = await prepareCollectionInclude({
349
+ query, targetScope, parentColumn: `${targetTable}.${idColumn}`, includeConfig: relDef.include, fields
350
+ }, { scopes, knex, requestContext, capabilities })
351
+ const fieldSelectionInfo = limited.fieldSelectionInfo
352
+ const targetRecords = await limited.query
353
+ for (const record of targetRecords) {
354
+ for (const field of limited.temporaryFields) delete record[field]
355
+ }
356
+
357
+ // Load relationship metadata for all target records
358
+ await loadRelationshipMetadata(scopes, targetRecords, targetScope)
359
+
360
+ // Group by parent ID
361
+ const childrenByParent = Object.create(null)
362
+ targetRecords.forEach(record => {
363
+ const parentId = getFieldValueForScope(scopes, targetScope, record, idField)
364
+ if (!childrenByParent[parentId]) {
365
+ childrenByParent[parentId] = []
366
+ }
367
+ childrenByParent[parentId].push(record)
368
+ })
369
+
370
+ // Set relationships on parent records
371
+ records.forEach(record => {
372
+ if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
373
+
374
+ const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
375
+ const children = childrenByParent[recordId] || []
376
+ const relData = children.map(childRecord => {
377
+ // Add to included
378
+ const childRecordId = getFieldValueForScope(scopes, targetScope, childRecord, 'id')
379
+ const resourceKey = `${targetScope}:${childRecordId}`
380
+ if (!included.has(resourceKey)) {
381
+ const jsonApiRecord = toJsonApiRecord(
382
+ scopes[targetScope],
383
+ childRecord,
384
+ targetScope
385
+ )
386
+
387
+ // Add relationships from metadata
388
+ if (childRecord[RELATIONSHIP_METADATA_KEY]) {
389
+ jsonApiRecord.relationships = childRecord[RELATIONSHIP_METADATA_KEY]
390
+ // Clean up the temporary property
391
+ delete childRecord[RELATIONSHIP_METADATA_KEY]
392
+ }
393
+
394
+ // Attach computed dependencies info if sparse fieldsets were used
395
+ if (fieldSelectionInfo?.computedDependencies) {
396
+ jsonApiRecord[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
397
+ }
398
+
399
+ included.set(resourceKey, jsonApiRecord)
400
+ }
401
+ return { type: targetScope, id: String(childRecordId) }
402
+ })
403
+
404
+ const relationshipObject = { data: relData }
405
+
406
+ // Add links if urlPrefix is configured
407
+ if (scopeName) {
408
+ relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
409
+ }
410
+
411
+ record[RELATIONSHIPS_KEY][includeName] = relationshipObject
412
+ })
413
+
414
+ // Process nested includes
415
+ if (Object.keys(subIncludes).length > 0 && targetRecords.length > 0) {
416
+ const nextPath = `${currentPath}.${includeName}`
417
+ await deps.loadNestedIncludes(
418
+ { records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
419
+ { context: { scopes, log, knex, capabilities, requestContext } }
420
+ )
421
+ }
422
+ } catch (error) {
423
+ throw wrapUnexpectedError(error, {
424
+ message: `Failed to load reverse polymorphic relationship '${includeName}' via '${relDef?.via}' for scope '${scopeName}'`,
425
+ context: { phase: 'include', scopeName, includeName, via: relDef?.via, targetScope: relDef?.target }
426
+ })
427
+ }
428
+ }