json-rest-api 1.0.29 → 2.0.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 (292) 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 +634 -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 +100 -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 +3827 -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 +36 -0
  44. package/index.js +1 -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 +64 -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 +213 -164
  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 +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -148
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. package/tests/virtual-fields.test.js +0 -404
@@ -0,0 +1,579 @@
1
+ // To-one loaders attach linkage and selected records; deps.loadNestedIncludes traverses child paths.
2
+ import { whereInIdentifiers } from './identifier-query.js'
3
+ import { applyFieldSelectionToQuery, buildFieldSelection } from '../querying-writing/knex-field-helpers.js'
4
+ import { toJsonApiRecord } from './knex-json-api-transformers-querying.js'
5
+ import {
6
+ RELATIONSHIPS_KEY,
7
+ RELATIONSHIP_METADATA_KEY,
8
+ COMPUTED_DEPENDENCIES_KEY,
9
+ RELATIONSHIP_READ_BATCH_SIZE,
10
+ } from '../querying-writing/knex-constants.js'
11
+ import { getOperationDiagnosticContext, wrapUnexpectedError } from '../../../../lib/error-context.js'
12
+ import { getPolymorphicLinkage } from '../querying-writing/relationship-contracts.js'
13
+ import {
14
+ resolveScopeStorageAdapter,
15
+ getIdColumnForScope,
16
+ getFieldValueForScope,
17
+ translateColumnForScope,
18
+ buildRelationshipLinks,
19
+ applyScopeFiltersToIncludeQuery,
20
+ groupByPolymorphicType,
21
+ loadRelationshipMetadata,
22
+ } from './include-query-helpers.js'
23
+ /**
24
+ * Loads belongsTo relationships (many-to-one)
25
+ *
26
+ * This function loads the "parent" side of a relationship. For example, if comments
27
+ * belong to articles, this loads the articles for a set of comments.
28
+ *
29
+ * @param {Object} scope - The resource object containing:
30
+ * - records: Array<Object> - Records to load relationships for
31
+ * - fieldName: string - The foreign key field name (e.g., 'author_id')
32
+ * - fieldDef: Object - The field definition from the schema
33
+ * - includeName: string - The relationship name (e.g., 'author')
34
+ * - subIncludes: Object - Nested includes to process recursively
35
+ * - included: Map - Map of already included resources
36
+ * - processedPaths: Set - Set of already processed paths
37
+ * - currentPath: string - Current include path for tracking
38
+ * - fields: Object - Sparse fieldsets configuration
39
+ * @param {Object} deps - Dependencies object containing:
40
+ * - context.scopes: Object - The resources object
41
+ * - context.log: Object - Logger instance
42
+ * - context.knex: Object - Knex instance
43
+ * @returns {Promise<void>}
44
+ */
45
+ export const loadBelongsTo = async (scope, deps) => {
46
+ const { records, scopeName, fieldName, fieldDef, includeName, subIncludes, included, processedPaths, currentPath, fields } = scope
47
+ const { scopes, log, knex, capabilities, requestContext } = deps.context
48
+ try {
49
+ log.trace('[INCLUDE] Loading belongsTo:', {
50
+ fieldName,
51
+ includeName,
52
+ recordCount: records.length
53
+ })
54
+
55
+ // Get the target scope name
56
+ const targetScope = fieldDef.belongsTo
57
+ if (!scopes[targetScope]) {
58
+ try {
59
+ await log.warn('[INCLUDE] Target scope not found:', {
60
+ ...getOperationDiagnosticContext(requestContext || {}, { phase: 'include', scopeName, backend: knex.client?.config?.client }), targetScope
61
+ })
62
+ } catch { /* Diagnostics must preserve the missing-target decision. */ }
63
+ return
64
+ }
65
+
66
+ // Collect all foreign key values
67
+ const foreignKeyValues = records
68
+ .map(record => getFieldValueForScope(scopes, scopeName, record, fieldName))
69
+ .filter(val => val != null) // Filter out null/undefined
70
+
71
+ const uniqueIds = [...new Set(foreignKeyValues)]
72
+
73
+ log.debug('[INCLUDE] Loading belongsTo records:', {
74
+ targetScope,
75
+ idCount: uniqueIds.length
76
+ })
77
+
78
+ if (uniqueIds.length === 0) {
79
+ // No relationships to load, set all to null
80
+ records.forEach(record => {
81
+ if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
82
+ const relationshipObject = { data: null }
83
+
84
+ // Add links if urlPrefix is configured
85
+ if (scopeName) {
86
+ relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
87
+ }
88
+
89
+ record[RELATIONSHIPS_KEY][includeName] = relationshipObject
90
+ })
91
+ return
92
+ }
93
+
94
+ // Get target table info
95
+ const targetTableName = scopes[targetScope].vars.schemaInfo.tableName
96
+ const targetIdColumn = getIdColumnForScope(scopes, targetScope)
97
+ // Select visible fields, projections and their dependencies.
98
+ const targetScopeObject = scopes[targetScope]
99
+ const storageAdapter = resolveScopeStorageAdapter(scopes, targetScope, knex)
100
+ const fieldSelectionInfo = await buildFieldSelection(targetScopeObject, {
101
+ context: {
102
+ ...(requestContext || {}),
103
+ scopeName: targetScope,
104
+ queryParams: { fields },
105
+ schemaInfo: targetScopeObject.vars.schemaInfo,
106
+ storageAdapter,
107
+ }
108
+ })
109
+
110
+ // Load the target records
111
+ const targetRecords = []
112
+ for (let offset = 0; offset < uniqueIds.length; offset += RELATIONSHIP_READ_BATCH_SIZE) {
113
+ let query = knex(targetTableName).whereIn(targetIdColumn, uniqueIds.slice(offset, offset + RELATIONSHIP_READ_BATCH_SIZE))
114
+ const selectionState = await applyFieldSelectionToQuery({
115
+ query,
116
+ scope: targetScopeObject,
117
+ fieldSelectionInfo,
118
+ tableName: targetTableName,
119
+ useTablePrefix: false,
120
+ storageAdapter,
121
+ db: knex,
122
+ context: {
123
+ ...(requestContext || {}),
124
+ scopeName: targetScope,
125
+ schemaInfo: targetScopeObject.vars.schemaInfo,
126
+ storageAdapter,
127
+ },
128
+ scopeName: targetScope
129
+ })
130
+ query = selectionState.query
131
+ const scopedQueryState = await applyScopeFiltersToIncludeQuery({
132
+ query,
133
+ scopes,
134
+ scopeName: targetScope,
135
+ requestContext,
136
+ db: knex,
137
+ tableName: targetTableName
138
+ })
139
+ query = scopedQueryState.query
140
+ for (const record of await query) targetRecords.push(record)
141
+ }
142
+
143
+ // Load relationship metadata for all target records
144
+ await loadRelationshipMetadata(scopes, targetRecords, targetScope)
145
+
146
+ // Create lookup map
147
+ const targetById = Object.create(null)
148
+ targetRecords.forEach(record => {
149
+ targetById[getFieldValueForScope(scopes, targetScope, record, 'id')] = record
150
+ })
151
+
152
+ // Set relationships on original records
153
+ const targetRecordsToProcess = new Set()
154
+
155
+ records.forEach(record => {
156
+ if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
157
+
158
+ const targetId = getFieldValueForScope(scopes, scopeName, record, fieldName)
159
+ const targetRecord = targetById[targetId]
160
+
161
+ if (targetRecord) {
162
+ // Convert to JSON:API format
163
+ const jsonApiRecord = toJsonApiRecord(
164
+ scopes[targetScope],
165
+ targetRecord,
166
+ targetScope
167
+ )
168
+
169
+ // Add relationships from metadata
170
+ if (targetRecord[RELATIONSHIP_METADATA_KEY]) {
171
+ jsonApiRecord.relationships = targetRecord[RELATIONSHIP_METADATA_KEY]
172
+ // Clean up the temporary property
173
+ delete targetRecord[RELATIONSHIP_METADATA_KEY]
174
+ }
175
+
176
+ // Attach computed dependencies info if sparse fieldsets were used
177
+ if (fieldSelectionInfo?.computedDependencies) {
178
+ jsonApiRecord[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
179
+ }
180
+
181
+ const relationshipObject = { data: { type: targetScope, id: String(targetId) } }
182
+
183
+ // Add links if urlPrefix is configured
184
+ if (scopeName) {
185
+ relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
186
+ }
187
+
188
+ record[RELATIONSHIPS_KEY][includeName] = relationshipObject
189
+
190
+ // Add to included if not already there
191
+ const resourceKey = `${targetScope}:${targetId}`
192
+ if (!included.has(resourceKey)) {
193
+ included.set(resourceKey, jsonApiRecord) // Now includes relationships!
194
+ }
195
+ if (Object.keys(subIncludes).length > 0) {
196
+ targetRecordsToProcess.add(targetRecord)
197
+ }
198
+ } else {
199
+ const relationshipObject = { data: null }
200
+
201
+ // Add links if urlPrefix is configured
202
+ if (scopeName) {
203
+ relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
204
+ }
205
+
206
+ record[RELATIONSHIPS_KEY][includeName] = relationshipObject
207
+ }
208
+ })
209
+
210
+ // Process nested includes if any
211
+ if (targetRecordsToProcess.size > 0) {
212
+ const nextPath = `${currentPath}.${includeName}`
213
+ await deps.loadNestedIncludes(
214
+ { records: [...targetRecordsToProcess], scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
215
+ { context: { scopes, log, knex, capabilities, requestContext } }
216
+ )
217
+ }
218
+ } catch (error) {
219
+ throw wrapUnexpectedError(error, {
220
+ message: `Failed to load belongsTo relationship '${includeName}' for field '${fieldName}'`,
221
+ context: { phase: 'include', fieldName, includeName, targetScope: fieldDef.belongsTo }
222
+ })
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Loads hasOne relationships (one-to-one)
228
+ *
229
+ * Similar to hasMany but expects a single related record.
230
+ * For example, a user that has one profile.
231
+ *
232
+ * @param {Object} scope - The resource object
233
+ * @param {Object} deps - Dependencies object
234
+ */
235
+ export const loadHasOne = async (scope, deps) => {
236
+ const { records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
237
+ const { context: { scopes, log, knex, capabilities, requestContext } } = deps
238
+
239
+ const childLog = log?.child ? log.child({ method: 'loadHasOne' }) : log
240
+
241
+ if (!records || records.length === 0) {
242
+ childLog?.trace?.('[INCLUDE] No records to load hasOne for')
243
+ return
244
+ }
245
+
246
+ // Get parent IDs
247
+ const mainIds = [...new Set(records.map(record => getFieldValueForScope(scopes, scopeName, record, 'id')).filter(id => id !== null && id !== undefined))]
248
+
249
+ if (mainIds.length === 0) {
250
+ childLog?.trace?.('[INCLUDE] No parent IDs found, skipping hasOne load')
251
+ return
252
+ }
253
+
254
+ const targetScope = relDef.target
255
+ const targetTable = scopes[targetScope].vars.schemaInfo.tableName
256
+ const foreignKey = relDef.foreignKey
257
+
258
+ if (!foreignKey) {
259
+ throw new Error(`Missing foreignKey in hasOne relationship '${includeName}' for scope '${scopeName}'`)
260
+ }
261
+
262
+ childLog?.debug?.(`[INCLUDE] Loading ${targetScope} records with foreign key ${foreignKey}:`, {
263
+ parentCount: mainIds.length
264
+ })
265
+
266
+ // Build query for hasOne - expects single result per parent
267
+ const targetScopeObject = scopes[targetScope]
268
+ const storageAdapter = resolveScopeStorageAdapter(scopes, targetScope, knex)
269
+
270
+ // Select visible fields, projections and their dependencies.
271
+ const fieldSelectionInfo = await buildFieldSelection(targetScopeObject, {
272
+ context: {
273
+ ...(requestContext || {}),
274
+ scopeName: targetScope,
275
+ queryParams: { fields },
276
+ schemaInfo: targetScopeObject.vars.schemaInfo,
277
+ storageAdapter,
278
+ }
279
+ })
280
+
281
+ // Query related records
282
+ const foreignKeyColumn = translateColumnForScope(scopes, targetScope, foreignKey)
283
+ let query = knex(targetTable).modify(whereInIdentifiers, foreignKeyColumn, mainIds)
284
+
285
+ const selectionState = await applyFieldSelectionToQuery({
286
+ query,
287
+ scope: targetScopeObject,
288
+ fieldSelectionInfo,
289
+ tableName: targetTable,
290
+ useTablePrefix: false,
291
+ storageAdapter,
292
+ db: knex,
293
+ context: {
294
+ ...(requestContext || {}),
295
+ scopeName: targetScope,
296
+ schemaInfo: targetScopeObject.vars.schemaInfo,
297
+ storageAdapter,
298
+ },
299
+ scopeName: targetScope
300
+ })
301
+ query = selectionState.query
302
+
303
+ const scopedQueryState = await applyScopeFiltersToIncludeQuery({
304
+ query,
305
+ scopes,
306
+ scopeName: targetScope,
307
+ requestContext,
308
+ db: knex,
309
+ tableName: targetTable
310
+ })
311
+ query = scopedQueryState.query
312
+
313
+ const relatedRecords = await query
314
+ await loadRelationshipMetadata(scopes, relatedRecords, targetScope)
315
+
316
+ // Create a map of parent ID to related record (should be one-to-one)
317
+ const relatedByParentId = Object.create(null)
318
+ for (const related of relatedRecords) {
319
+ const parentId = getFieldValueForScope(scopes, targetScope, related, foreignKey)
320
+ if (relatedByParentId[parentId]) {
321
+ try {
322
+ await log.warn('[INCLUDE] Multiple records found for hasOne relationship', {
323
+ ...getOperationDiagnosticContext(requestContext || {}, { phase: 'include', scopeName, backend: knex.client?.config?.client }), includeName, foreignKey
324
+ })
325
+ } catch { /* Diagnostics must preserve the existing record selection. */ }
326
+ }
327
+ relatedByParentId[parentId] = related
328
+ }
329
+
330
+ // Process each parent record
331
+ for (const record of records) {
332
+ const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
333
+ const relatedRecord = relatedByParentId[recordId]
334
+
335
+ if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
336
+
337
+ if (relatedRecord) {
338
+ // Transform to JSON:API format
339
+ const transformed = toJsonApiRecord(targetScopeObject, relatedRecord, targetScope)
340
+ if (relatedRecord[RELATIONSHIP_METADATA_KEY]) {
341
+ transformed.relationships = relatedRecord[RELATIONSHIP_METADATA_KEY]
342
+ delete relatedRecord[RELATIONSHIP_METADATA_KEY]
343
+ }
344
+ if (fieldSelectionInfo?.computedDependencies) {
345
+ transformed[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
346
+ }
347
+
348
+ // Set single object (not array) for hasOne
349
+ const relatedRecordId = getFieldValueForScope(scopes, targetScope, relatedRecord, 'id')
350
+ record[RELATIONSHIPS_KEY][includeName] = {
351
+ data: { type: targetScope, id: String(relatedRecordId) },
352
+ links: buildRelationshipLinks(scopes, scopeName, record, includeName)
353
+ }
354
+
355
+ // Add to included if not already there
356
+ const key = `${targetScope}:${relatedRecordId}`
357
+ if (!included.has(key)) {
358
+ included.set(key, transformed)
359
+ }
360
+
361
+ // Process nested includes if any
362
+ if (subIncludes && Object.keys(subIncludes).length > 0) {
363
+ const newPath = `${currentPath}.${includeName}`
364
+ await deps.loadNestedIncludes(
365
+ { records: [relatedRecord], scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: newPath, fields },
366
+ { context: { scopes, log, knex, capabilities, requestContext } }
367
+ )
368
+ }
369
+ } else {
370
+ // No related record found
371
+ record[RELATIONSHIPS_KEY][includeName] = {
372
+ data: null,
373
+ links: buildRelationshipLinks(scopes, scopeName, record, includeName)
374
+ }
375
+ }
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Loads polymorphic belongsTo relationships
381
+ *
382
+ * Handles relationships where a record can belong to different types of parent records.
383
+ * For example, comments that can belong to either articles or videos.
384
+ *
385
+ * @param {Object} scope - The resource object containing:
386
+ * - records: Array<Object> - Records with polymorphic relationships
387
+ * - relName: string - The relationship name
388
+ * - relDef: Object - The relationship definition with belongsToPolymorphic
389
+ * - subIncludes: Object - Nested includes to process recursively
390
+ * - included: Map - Map of already included resources
391
+ * - processedPaths: Set - Set of already processed paths
392
+ * - currentPath: string - Current include path for tracking
393
+ * - fields: Object - Sparse fieldsets configuration
394
+ * @param {Object} deps - Dependencies object containing:
395
+ * - context.scopes: Object - The resources object
396
+ * - context.log: Object - Logger instance
397
+ * - context.knex: Object - Knex instance
398
+ * @returns {Promise<void>}
399
+ */
400
+ export const loadPolymorphicBelongsTo = async (scope, deps) => {
401
+ const { records, scopeName, relName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
402
+ const { scopes, log, knex, capabilities, requestContext } = deps.context
403
+ try {
404
+ log.trace('[INCLUDE] Loading polymorphic belongsTo:', {
405
+ relName,
406
+ recordCount: records.length
407
+ })
408
+
409
+ const { typeField, idField, types } = relDef.belongsToPolymorphic
410
+
411
+ // Group records by their target type using helper
412
+ const grouped = groupByPolymorphicType(
413
+ records,
414
+ typeField,
415
+ idField,
416
+ (record, fieldName) => getFieldValueForScope(scopes, scopeName, record, fieldName)
417
+ )
418
+
419
+ log.trace('[INCLUDE] Grouped by type:', {
420
+ types: Object.keys(grouped),
421
+ counts: Object.entries(grouped).map(([t, ids]) => `${t}: ${ids.length}`)
422
+ })
423
+
424
+ // Load each type separately
425
+ for (const [targetType, targetIds] of Object.entries(grouped)) {
426
+ if (targetIds.length === 0) continue
427
+ getPolymorphicLinkage({ type: targetType, id: targetIds[0], types, scopeName, relationshipName: relName })
428
+
429
+ // Get target table information
430
+ const targetSchemaInfo = scopes[targetType].vars.schemaInfo
431
+ const targetTable = targetSchemaInfo?.tableName || targetType
432
+
433
+ // Select visible fields, projections and their dependencies.
434
+ const targetScopeObject = scopes[targetType]
435
+ const storageAdapter = resolveScopeStorageAdapter(scopes, targetType, knex)
436
+ const fieldSelectionInfo = await buildFieldSelection(targetScopeObject, {
437
+ context: {
438
+ ...(requestContext || {}),
439
+ scopeName: targetType,
440
+ queryParams: { fields },
441
+ schemaInfo: targetScopeObject.vars.schemaInfo,
442
+ storageAdapter,
443
+ }
444
+ })
445
+
446
+ log.debug(`[INCLUDE] Loading ${targetType} records:`, {
447
+ idCount: targetIds.length
448
+ })
449
+
450
+ // Query for this type
451
+ const targetIdColumn = getIdColumnForScope(scopes, targetType)
452
+ const targetRecords = []
453
+ for (let offset = 0; offset < targetIds.length; offset += RELATIONSHIP_READ_BATCH_SIZE) {
454
+ let query = knex(targetTable).whereIn(targetIdColumn, targetIds.slice(offset, offset + RELATIONSHIP_READ_BATCH_SIZE))
455
+ const selectionState = await applyFieldSelectionToQuery({
456
+ query,
457
+ scope: targetScopeObject,
458
+ fieldSelectionInfo,
459
+ tableName: targetTable,
460
+ useTablePrefix: false,
461
+ storageAdapter,
462
+ db: knex,
463
+ context: {
464
+ ...(requestContext || {}),
465
+ scopeName: targetType,
466
+ schemaInfo: targetScopeObject.vars.schemaInfo,
467
+ storageAdapter,
468
+ },
469
+ scopeName: targetType
470
+ })
471
+ query = selectionState.query
472
+ const scopedQueryState = await applyScopeFiltersToIncludeQuery({
473
+ query,
474
+ scopes,
475
+ scopeName: targetType,
476
+ requestContext,
477
+ db: knex,
478
+ tableName: targetTable
479
+ })
480
+ query = scopedQueryState.query
481
+ for (const record of await query) targetRecords.push(record)
482
+ }
483
+
484
+ // Load relationship metadata for all target records
485
+ await loadRelationshipMetadata(scopes, targetRecords, targetType)
486
+
487
+ // Create lookup map
488
+ const targetById = Object.create(null)
489
+ targetRecords.forEach(record => {
490
+ targetById[getFieldValueForScope(scopes, targetType, record, 'id')] = record
491
+ })
492
+
493
+ // Add to included and set relationships
494
+ records.forEach(record => {
495
+ if (getFieldValueForScope(scopes, scopeName, record, typeField) === targetType) {
496
+ const targetId = getFieldValueForScope(scopes, scopeName, record, idField)
497
+ const targetRecord = targetById[targetId]
498
+
499
+ if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
500
+
501
+ if (targetRecord) {
502
+ // Add to included
503
+ const resourceKey = `${targetType}:${targetId}`
504
+ if (!included.has(resourceKey)) {
505
+ const jsonApiRecord = toJsonApiRecord(
506
+ scopes[targetType],
507
+ targetRecord,
508
+ targetType
509
+ )
510
+
511
+ // Add relationships from metadata
512
+ if (targetRecord[RELATIONSHIP_METADATA_KEY]) {
513
+ jsonApiRecord.relationships = targetRecord[RELATIONSHIP_METADATA_KEY]
514
+ // Clean up the temporary property
515
+ delete targetRecord[RELATIONSHIP_METADATA_KEY]
516
+ }
517
+
518
+ // Attach computed dependencies info if sparse fieldsets were used
519
+ if (fieldSelectionInfo?.computedDependencies) {
520
+ jsonApiRecord[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
521
+ }
522
+
523
+ included.set(resourceKey, jsonApiRecord)
524
+ }
525
+
526
+ const relationshipObject = {
527
+ data: { type: targetType, id: String(targetId) }
528
+ }
529
+
530
+ // Add links if urlPrefix is configured
531
+ if (scopeName) {
532
+ relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, relName)
533
+ }
534
+
535
+ record[RELATIONSHIPS_KEY][relName] = relationshipObject
536
+ } else {
537
+ const relationshipObject = { data: null }
538
+
539
+ // Add links if urlPrefix is configured
540
+ if (scopeName) {
541
+ relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, relName)
542
+ }
543
+
544
+ record[RELATIONSHIPS_KEY][relName] = relationshipObject
545
+ }
546
+ }
547
+ })
548
+
549
+ // Process nested includes for this type
550
+ if (Object.keys(subIncludes).length > 0 && targetRecords.length > 0) {
551
+ const nextPath = `${currentPath}.${relName}`
552
+ await deps.loadNestedIncludes(
553
+ { records: targetRecords, scopeName: targetType, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
554
+ { context: { scopes, log, knex, capabilities, requestContext } }
555
+ )
556
+ }
557
+ }
558
+
559
+ // Set null for records without relationships
560
+ records.forEach(record => {
561
+ if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
562
+ if (!Object.hasOwn(record[RELATIONSHIPS_KEY], relName)) {
563
+ const relationshipObject = { data: null }
564
+
565
+ // Add links if urlPrefix is configured
566
+ if (scopeName) {
567
+ relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, relName)
568
+ }
569
+
570
+ record[RELATIONSHIPS_KEY][relName] = relationshipObject
571
+ }
572
+ })
573
+ } catch (error) {
574
+ throw wrapUnexpectedError(error, {
575
+ message: `Failed to load polymorphic belongsTo relationship '${relName}'`,
576
+ context: { phase: 'include', relName, types: relDef?.belongsToPolymorphic?.types }
577
+ })
578
+ }
579
+ }