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,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
+ }