json-rest-api 1.0.28 → 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 (289) 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 +746 -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} +90 -31
  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 -174
  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 +67 -330
  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 +205 -30
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +20 -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 +238 -187
  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 +111 -287
  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 +109 -44
  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 +142 -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 -2509
  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 -1069
  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 -385
  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 -145
  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 +38 -69
  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 +39 -82
  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 -1238
  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 -150
  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 -118
  251. package/tests/fixtures/api-configs.js +0 -2509
  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-cursor-multifield.test.js +0 -643
  264. package/tests/pagination-enhanced.test.js +0 -563
  265. package/tests/polymorphic-relationships.test.js +0 -347
  266. package/tests/polymorphic-simple.test.js +0 -148
  267. package/tests/positioning-advanced.test.js +0 -692
  268. package/tests/positioning-extended.test.js +0 -915
  269. package/tests/positioning-multifilter.test.js +0 -699
  270. package/tests/positioning.test.js +0 -560
  271. package/tests/post-commit-hooks.test.js +0 -120
  272. package/tests/projected-fields.test.js +0 -319
  273. package/tests/public-exports.test.js +0 -15
  274. package/tests/put-create-parity.test.js +0 -216
  275. package/tests/queries.test.js +0 -521
  276. package/tests/query-limits.test.js +0 -584
  277. package/tests/relationship-endpoints.test.js +0 -620
  278. package/tests/relationship-include-storage-adapter.test.js +0 -56
  279. package/tests/relationships.test.js +0 -681
  280. package/tests/rest-api.test.js +0 -337
  281. package/tests/return-record-settings.test.js +0 -184
  282. package/tests/row-policy.test.js +0 -489
  283. package/tests/s3-storage.test.js +0 -38
  284. package/tests/searchschema-knex-behavior.test.js +0 -187
  285. package/tests/searchschema-merge.test.js +0 -151
  286. package/tests/socketio.test.js +0 -860
  287. package/tests/storage-mapping.test.js +0 -362
  288. package/tests/test-development-guide.md +0 -370
  289. package/tests/virtual-fields.test.js +0 -404
@@ -1,243 +1,65 @@
1
- /**
2
- * Transformation functions for simplified API mode
3
- *
4
- * @description
5
- * This module enables "simplified mode" - a developer-friendly alternative
6
- * to verbose JSON:API format. It allows APIs to:
7
- * - Accept plain JavaScript objects as input
8
- * - Return flat response objects instead of nested JSON:API
9
- * - Automatically convert between formats transparently
10
- * - Maintain JSON:API compliance internally
11
- */
1
+ /** Plain-record and JSON:API conversions at the resource boundary. */
12
2
 
13
3
  import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
14
4
 
5
+ const indexIncludedResources = (included, data) => {
6
+ const byType = new Map()
7
+ let primaryRecords = []
8
+ if (Array.isArray(data)) {
9
+ primaryRecords = data
10
+ } else if (data) {
11
+ primaryRecords = [data]
12
+ }
13
+ // Index primary records first so they win when included repeats an identity.
14
+ for (const record of [...primaryRecords, ...(included || [])]) {
15
+ const { type, id } = record
16
+ if (!byType.has(type)) byType.set(type, new Map())
17
+ const byId = byType.get(type)
18
+ if (!byId.has(id)) byId.set(id, record)
19
+ }
20
+ return byType
21
+ }
22
+
15
23
  /**
16
- * Transforms simplified plain object into JSON:API format
17
- *
18
- * @param {Object} scope - Scope containing input data
19
- * @param {Object} scope.inputRecord - Plain object or JSON:API document
20
- * @param {Object} deps - Dependencies with context
21
- * @param {string} deps.context.scopeName - Resource type name
22
- * @param {Object} deps.context.schemaStructure - Schema with field definitions
23
- * @param {Object} deps.context.schemaRelationships - Relationship configurations
24
- * @returns {Object} JSON:API formatted document
25
- *
26
- * @example
27
- * // Input: Simple object with only attributes
28
- * const input = {
29
- * id: '123',
30
- * title: 'My Article',
31
- * content: 'Article content here'
32
- * };
33
- * const result = transformSimplifiedToJsonApi(
34
- * { inputRecord: input },
35
- * { context: { scopeName: 'articles', schemaStructure: schema } }
36
- * );
37
- *
38
- * // Output: Proper JSON:API structure
39
- * // {
40
- * // data: {
41
- * // type: 'articles',
42
- * // id: '123',
43
- * // attributes: {
44
- * // title: 'My Article',
45
- * // content: 'Article content here'
46
- * // }
47
- * // }
48
- * // }
49
- *
50
- * @example
51
- * // Input: Object with relationship name (preferred way)
52
- * const schema = {
53
- * title: { type: 'string' },
54
- * author_id: {
55
- * type: 'number',
56
- * belongsTo: 'users',
57
- * as: 'author'
58
- * }
59
- * };
60
- * const input = {
61
- * id: '456',
62
- * title: 'Another Article',
63
- * author: 789 // Using relationship name
64
- * };
65
- *
66
- * // Output: Relationship becomes JSON:API relationship
67
- * // {
68
- * // data: {
69
- * // type: 'articles',
70
- * // id: '456',
71
- * // attributes: {
72
- * // title: 'Another Article' // author removed from attributes
73
- * // },
74
- * // relationships: {
75
- * // author: {
76
- * // data: { type: 'users', id: '789' }
77
- * // }
78
- * // }
79
- * // }
80
- * // }
81
- *
82
- * @example
83
- * // Input: Object with relationship name (only supported way)
84
- * const input = {
85
- * id: '456',
86
- * title: 'Another Article',
87
- * author: 789 // Using relationship name
88
- * };
89
- * // Output: Same JSON:API structure as above
90
- *
91
- * @example
92
- * // Input: Many-to-many relationship
93
- * const relationships = {
94
- * tags: {
95
- * type: 'manyToMany',
96
- * through: 'article_tags',
97
- * foreignKey: 'article_id',
98
- * otherKey: 'tag_id',
99
- * target: 'tags'
100
- * }
101
- * };
102
- * const input = {
103
- * title: 'Tagged Article',
104
- * tags: ['10', '20', '30'] // Array of IDs
105
- * };
106
- *
107
- * // Output: Array becomes relationship data array
108
- * // {
109
- * // data: {
110
- * // type: 'articles',
111
- * // attributes: {
112
- * // title: 'Tagged Article'
113
- * // },
114
- * // relationships: {
115
- * // tags: {
116
- * // data: [
117
- * // { type: 'tags', id: '10' },
118
- * // { type: 'tags', id: '20' },
119
- * // { type: 'tags', id: '30' }
120
- * // ]
121
- * // }
122
- * // }
123
- * // }
124
- * // }
125
- *
126
- * @example
127
- * // Input: Already JSON:API formatted
128
- * const input = {
129
- * data: {
130
- * type: 'articles',
131
- * id: '1',
132
- * attributes: { title: 'Already formatted' }
133
- * }
134
- * };
135
- * // Output: Returned unchanged (detected by data.type)
136
- *
137
- * @example
138
- * // Input: Polymorphic relationship with minimal object format
139
- * const relationships = {
140
- * commentable: {
141
- * belongsToPolymorphic: {
142
- * types: ['posts', 'videos'],
143
- * typeField: 'commentable_type',
144
- * idField: 'commentable_id'
145
- * }
146
- * }
147
- * };
148
- * const input = {
149
- * text: 'Great content!',
150
- * commentable: { id: '456', _type: 'posts' } // Minimal object format
151
- * };
152
- *
153
- * // Output: Proper JSON:API structure for polymorphic
154
- * // {
155
- * // data: {
156
- * // type: 'comments',
157
- * // attributes: {
158
- * // text: 'Great content!'
159
- * // },
160
- * // relationships: {
161
- * // commentable: {
162
- * // data: { type: 'posts', id: '456' }
163
- * // }
164
- * // }
165
- * // }
166
- * // }
167
- *
168
- * @example
169
- * // Input: Clearing polymorphic relationship
170
- * const input = {
171
- * text: 'Updated comment',
172
- * commentable: null // Clear the relationship
173
- * };
174
- *
175
- * // Output: Relationship set to null
176
- * // {
177
- * // data: {
178
- * // type: 'comments',
179
- * // attributes: {
180
- * // text: 'Updated comment'
181
- * // },
182
- * // relationships: {
183
- * // commentable: { data: null }
184
- * // }
185
- * // }
186
- * // }
187
- *
188
- * @description
189
- * Used by:
190
- * - rest-api-plugin for POST/PUT/PATCH when simplified: true
191
- * - Processes input before validation and storage
192
- *
193
- * Purpose:
194
- * - Provides developer-friendly plain object API
195
- * - Automatically detects and converts relationships
196
- * - Supports relationship alias notation
197
- * - Supports polymorphic relationships with minimal object format
198
- * - Maintains JSON:API compliance internally
199
- * - Allows gradual migration from simple to JSON:API
200
- *
201
- * Data flow:
202
- * 1. Checks if already JSON:API (has data.type)
203
- * 2. Extracts ID from input object
204
- * 3. Processes belongsTo relationships from schema
205
- * 4. Processes hasMany/manyToMany from relationships
206
- * 5. Processes polymorphic belongsTo from relationships
207
- * 6. Remaining fields become attributes
208
- * 6. Builds proper JSON:API document structure
24
+ * Convert plain input to a JSON:API document without mutating the input.
25
+ * Relationship aliases become linkage; remaining keys become attributes.
26
+ * A plain field named data is ordinary input, not automatic format detection.
27
+ * Physical belongsTo keys are rejected in favor of relationship aliases.
28
+ *
29
+ * @param {Object} scope
30
+ * @param {Object} scope.inputRecord - Plain input record
31
+ * @param {Object} deps
32
+ * @param {Object} deps.context
33
+ * @param {string} deps.context.scopeName - Resource type
34
+ * @param {Object} deps.context.schemaStructure - Compiled writable fields
35
+ * @param {Object} [deps.context.schemaRelationships] - Relationship definitions
36
+ * @returns {Object} JSON:API document for subsequent validation
37
+ * @example
38
+ * transformSimplifiedToJsonApi({ inputRecord: { id: '7', title: 'Book' } }, {
39
+ * context: { scopeName: 'books', schemaStructure: {}, schemaRelationships: {} }
40
+ * }) // { data: { type: 'books', id: '7', attributes: { title: 'Book' } } }
209
41
  */
210
42
  export const transformSimplifiedToJsonApi = (scope, deps) => {
211
- // Extract needed values from scope and deps
212
43
  const input = scope.inputRecord
213
44
  const scopeName = deps.context.scopeName
214
45
  const schema = deps.context.schemaStructure
215
46
  const relationships = deps.context.schemaRelationships
216
47
 
217
- // If already JSON:API format, return as-is
218
- if (input?.data?.type) {
219
- return input
220
- }
221
-
222
- const attributes = {}
223
48
  const relationshipsData = {}
224
- const tempInput = { ...input } // Create a mutable copy of the input
49
+ const tempInput = { ...input }
225
50
 
226
51
  const id = tempInput.id
227
- delete tempInput.id // Extract ID from tempInput
52
+ delete tempInput.id
228
53
 
229
- // 1. Process belongsTo relationships (from schema fields)
230
- // Iterate through the schema structure to find belongsTo fields
54
+ // Relationship input uses aliases; physical foreign keys are rejected.
231
55
  for (const [fieldName, fieldDef] of Object.entries(schema)) {
232
56
  if (fieldDef.belongsTo) {
233
- const relAlias = fieldDef.as || fieldName // Use 'as' alias or foreign key name as default alias
234
- let valueToProcess
57
+ const relName = fieldDef.as || fieldName
235
58
 
236
- // Check if the user provided the foreign key name (e.g., country_id) - NO LONGER ALLOWED
237
59
  if (tempInput[fieldName] !== undefined) {
238
60
  throw new RestApiValidationError(
239
61
  `Foreign key field '${fieldName}' is no longer supported for relationship input. ` +
240
- `Use the relationship name '${fieldDef.as || fieldName}' instead.`,
62
+ `Use the relationship name '${relName}' instead.`,
241
63
  {
242
64
  fields: [fieldName],
243
65
  violations: [{
@@ -249,16 +71,11 @@ export const transformSimplifiedToJsonApi = (scope, deps) => {
249
71
  )
250
72
  }
251
73
 
252
- // Check if the user provided the relationship name (e.g., country)
253
- const relName = fieldDef.as || fieldName
254
- if (tempInput[relName] !== undefined) {
255
- valueToProcess = tempInput[relName]
256
- delete tempInput[relName] // Remove from tempInput once processed
257
- }
258
-
259
- if (valueToProcess !== undefined) {
260
- relationshipsData[relAlias] = {
261
- data: valueToProcess ? { type: fieldDef.belongsTo, id: String(valueToProcess) } : null
74
+ const value = tempInput[relName]
75
+ if (value !== undefined) {
76
+ delete tempInput[relName]
77
+ relationshipsData[relName] = {
78
+ data: value === null ? null : { type: fieldDef.belongsTo, id: value }
262
79
  }
263
80
  }
264
81
  }
@@ -278,22 +95,22 @@ export const transformSimplifiedToJsonApi = (scope, deps) => {
278
95
  // - For hasMany/hasOne: the targetType is specified in the target property
279
96
  const targetType = relConfig.type === 'manyToMany' ? relName : relConfig.target
280
97
  relationshipsData[relName] = {
281
- data: value.map(relId => ({ type: targetType, id: String(relId) }))
98
+ data: value.map(relId => ({ type: targetType, id: relId }))
282
99
  }
283
100
  } else if (relConfig.type === 'hasOne' && value !== undefined) {
284
101
  relationshipsData[relName] = {
285
- data: value ? { type: relConfig.target, id: String(value) } : null
102
+ data: value === null ? null : { type: relConfig.target, id: value }
286
103
  }
287
104
  } else if (relConfig.belongsTo && value !== undefined) {
288
105
  relationshipsData[relName] = {
289
- data: value ? { type: relConfig.belongsTo, id: String(value) } : null
106
+ data: value === null ? null : { type: relConfig.belongsTo, id: value }
290
107
  }
291
108
  } else if (relConfig.belongsToPolymorphic && value !== undefined) {
292
109
  if (value === null) {
293
110
  relationshipsData[relName] = { data: null }
294
- } else if (typeof value === 'object' && value !== null && value.id && value._type) {
111
+ } else if (typeof value === 'object' && !Array.isArray(value) && Object.hasOwn(value, 'id') && value._type) {
295
112
  relationshipsData[relName] = {
296
- data: { type: value._type, id: String(value.id) }
113
+ data: { type: value._type, id: value.id }
297
114
  }
298
115
  } else {
299
116
  throw new RestApiValidationError(
@@ -313,12 +130,12 @@ export const transformSimplifiedToJsonApi = (scope, deps) => {
313
130
  }
314
131
 
315
132
  // 3. Any remaining fields in tempInput are attributes
316
- Object.assign(attributes, tempInput)
133
+ const attributes = tempInput
317
134
 
318
135
  return {
319
136
  data: {
320
137
  type: scopeName,
321
- ...(id && { id: String(id) }),
138
+ ...(id !== undefined && { id }),
322
139
  ...(Object.keys(attributes).length > 0 && { attributes }),
323
140
  ...(Object.keys(relationshipsData).length > 0 && { relationships: relationshipsData })
324
141
  }
@@ -326,181 +143,39 @@ export const transformSimplifiedToJsonApi = (scope, deps) => {
326
143
  }
327
144
 
328
145
  /**
329
- * Transforms JSON:API response into simplified plain object
330
- *
331
- * @param {Object} scope - Scope containing JSON:API response
332
- * @param {Object} scope.record - JSON:API document to transform
333
- * @param {Object} deps - Dependencies with context
334
- * @param {Object} deps.context.schemaStructure - Schema for mapping relationships
335
- * @param {Object} deps.context.schemaRelationships - Relationship configurations
336
- * @param {Object} deps.context.scopes - All scopes for nested transformations
337
- * @returns {Object|Object[]} Simplified object or array of objects
338
- *
339
- * @example
340
- * // Input: Single resource JSON:API
341
- * const jsonApi = {
342
- * data: {
343
- * type: 'articles',
344
- * id: '123',
345
- * attributes: {
346
- * title: 'My Article',
347
- * content: 'Content here'
348
- * }
349
- * }
350
- * };
351
- * const result = transformJsonApiToSimplified(
352
- * { record: jsonApi },
353
- * { context: { schemaStructure: schema } }
354
- * );
355
- *
356
- * // Output: Flattened object
357
- * // {
358
- * // id: '123',
359
- * // title: 'My Article', // Attributes flattened
360
- * // content: 'Content here'
361
- * // }
362
- *
363
- * @example
364
- * // Input: Resource with belongsTo relationship
365
- * const schema = {
366
- * title: { type: 'string' },
367
- * author_id: { type: 'number', belongsTo: 'users', as: 'author' }
368
- * };
369
- * const jsonApi = {
370
- * data: {
371
- * type: 'articles',
372
- * id: '456',
373
- * attributes: { title: 'Article with Author' },
374
- * relationships: {
375
- * author: {
376
- * data: { type: 'users', id: '789' }
377
- * }
378
- * }
379
- * }
380
- * };
381
- *
382
- * // Output: Relationship becomes foreign key
383
- * // {
384
- * // id: '456',
385
- * // title: 'Article with Author',
386
- * // author_id: '789' // Restored from relationship
387
- * // }
388
- *
389
- * @example
390
- * // Input: Collection response
391
- * const jsonApi = {
392
- * data: [
393
- * {
394
- * type: 'articles',
395
- * id: '1',
396
- * attributes: { title: 'First Article' }
397
- * },
398
- * {
399
- * type: 'articles',
400
- * id: '2',
401
- * attributes: { title: 'Second Article' }
402
- * }
403
- * ],
404
- * meta: { total: 2 },
405
- * links: { next: '/articles?page=2' }
406
- * };
407
- *
408
- * // Output: Preserves meta and links
409
- * // {
410
- * // data: [
411
- * // { id: '1', title: 'First Article' },
412
- * // { id: '2', title: 'Second Article' }
413
- * // ],
414
- * // meta: { total: 2 },
415
- * // links: { next: '/articles?page=2' }
416
- * // }
417
- *
418
- * @example
419
- * // Input: With included resources
420
- * const jsonApi = {
421
- * data: {
422
- * type: 'articles',
423
- * id: '400',
424
- * attributes: { title: 'JavaScript Guide' },
425
- * relationships: {
426
- * author: {
427
- * data: { type: 'authors', id: '75' }
428
- * },
429
- * chapters: {
430
- * data: [
431
- * { type: 'chapters', id: '1001' },
432
- * { type: 'chapters', id: '1002' }
433
- * ]
434
- * }
435
- * }
436
- * },
437
- * included: [
438
- * {
439
- * type: 'authors',
440
- * id: '75',
441
- * attributes: { name: 'Jane Smith' }
442
- * },
443
- * {
444
- * type: 'chapters',
445
- * id: '1001',
446
- * attributes: { number: 1, title: 'Introduction' }
447
- * },
448
- * {
449
- * type: 'chapters',
450
- * id: '1002',
451
- * attributes: { number: 2, title: 'Basics' }
452
- * }
453
- * ]
454
- * };
455
- *
456
- * // Output: Expands included resources
457
- * // {
458
- * // id: '400',
459
- * // title: 'JavaScript Guide',
460
- * // author_id: '75',
461
- * // author: { // Expanded from included
462
- * // id: '75',
463
- * // name: 'Jane Smith'
464
- * // },
465
- * // chapters: [{ id: '1001' }, { id: '1002' }],
466
- * // chapters: [ // Expanded array
467
- * // { id: '1001', number: 1, title: 'Introduction' },
468
- * // { id: '1002', number: 2, title: 'Basics' }
469
- * // ]
470
- * // }
471
- *
472
- * @description
473
- * Used by:
474
- * - rest-api-plugin for GET/QUERY when simplified: true
475
- * - Transforms responses before sending to client
476
- *
477
- * Purpose:
478
- * - Provides cleaner response format for clients
479
- * - Creates minimal relationship objects from relationships
480
- * - Expands included resources for convenience
481
- * - Maintains compatibility with collection metadata
482
- * - Handles both single and array responses
483
- *
484
- * Data flow:
485
- * 1. Detects array vs single resource
486
- * 2. For arrays, preserves meta/links at root
487
- * 3. Calls transformSingleJsonApiToSimplified for each
488
- * 4. Returns simplified structure
146
+ * Flatten a JSON:API response using one resource index for the whole document.
147
+ * Collections retain their data wrapper, meta and links; single resources
148
+ * return the plain record directly. Missing/null data passes through unchanged.
149
+ * Relationship output uses alias-named objects, not physical foreign-key fields.
150
+ *
151
+ * @param {Object} scope
152
+ * @param {Object} scope.record - JSON:API response document
153
+ * @param {Object} deps
154
+ * @param {Object} deps.context
155
+ * @param {Object} [deps.context.schemaStructure] - Compiled resource fields
156
+ * @param {Object} [deps.context.schemaRelationships] - Relationship definitions
157
+ * @param {Object} deps.context.scopes - Resource registry for included records
158
+ * @returns {Object} Plain record or collection envelope
159
+ * @example
160
+ * transformJsonApiToSimplified({ record: {
161
+ * data: { type: 'books', id: '7', attributes: { title: 'Book' },
162
+ * relationships: { author: { data: { type: 'authors', id: '2' } } } }
163
+ * } }, { context: {} }) // { id: '7', title: 'Book', author: { id: '2' } }
489
164
  */
490
165
  export const transformJsonApiToSimplified = (scope, deps) => {
491
- // Extract needed values from scope and deps
492
166
  const jsonApi = scope.record
493
167
  const schema = deps.context.schemaStructure
494
168
  const relationships = deps.context.schemaRelationships
495
169
  const scopes = deps.context.scopes
496
170
 
497
171
  if (!jsonApi?.data) return jsonApi
172
+ const includedByType = indexIncludedResources(jsonApi.included, jsonApi.data)
498
173
 
499
174
  // Handle array response (QUERY)
500
175
  if (Array.isArray(jsonApi.data)) {
501
176
  const simplifiedData = jsonApi.data.map(item =>
502
177
  transformSingleJsonApiToSimplified(
503
- { data: item, included: jsonApi.included },
178
+ { data: item, includedByType },
504
179
  { context: { schemaStructure: schema, schemaRelationships: relationships, scopes } }
505
180
  )
506
181
  )
@@ -523,328 +198,54 @@ export const transformJsonApiToSimplified = (scope, deps) => {
523
198
 
524
199
  // Handle single response (no change for single resources)
525
200
  return transformSingleJsonApiToSimplified(
526
- { data: jsonApi.data, included: jsonApi.included },
201
+ { data: jsonApi.data, includedByType },
527
202
  { context: { schemaStructure: schema, schemaRelationships: relationships, scopes } }
528
203
  )
529
204
  }
530
205
 
531
206
  /**
532
- * Transforms single JSON:API resource into simplified format
533
- *
534
- * @param {Object} scope - Scope with data and included resources
535
- * @param {Object} scope.data - Single JSON:API resource object
536
- * @param {Array} scope.included - Array of included resources
537
- * @param {Object} deps - Dependencies with context
538
- * @param {Object} deps.context.schemaStructure - Resource schema
539
- * @param {Object} deps.context.schemaRelationships - Relationship configs
540
- * @param {Object} deps.context.scopes - All scopes for recursion
541
- * @returns {Object} Simplified plain object
542
- *
543
- * @example
544
- * // Input: Basic resource with relationships
545
- * const data = {
546
- * type: 'comments',
547
- * id: '100',
548
- * attributes: {
549
- * text: 'Great article!',
550
- * created_at: '2024-01-15'
551
- * },
552
- * relationships: {
553
- * author: {
554
- * data: { type: 'users', id: '50' }
555
- * },
556
- * article: {
557
- * data: { type: 'articles', id: '200' }
558
- * }
559
- * }
560
- * };
561
- * const schema = {
562
- * text: { type: 'string' },
563
- * created_at: { type: 'datetime' },
564
- * author_id: { type: 'number', belongsTo: 'users', as: 'author' },
565
- * article_id: { type: 'number', belongsTo: 'articles', as: 'article' }
566
- * };
567
- * const result = transformSingleJsonApiToSimplified(
568
- * { data: data, included: null },
569
- * { context: { schemaStructure: schema } }
570
- * );
571
- *
572
- * // Output: Minimal relationship objects
573
- * // {
574
- * // id: '100',
575
- * // text: 'Great article!',
576
- * // created_at: '2024-01-15',
577
- * // author: { id: '50' }, // Minimal object from relationships.author
578
- * // article: { id: '200' } // Minimal object from relationships.article
579
- * // }
580
- *
581
- * @example
582
- * // Input: Many-to-many relationships
583
- * const data = {
584
- * type: 'articles',
585
- * id: '300',
586
- * attributes: { title: 'My Article' },
587
- * relationships: {
588
- * tags: {
589
- * data: [
590
- * { type: 'tags', id: '1' },
591
- * { type: 'tags', id: '2' },
592
- * { type: 'tags', id: '3' }
593
- * ]
594
- * }
595
- * }
596
- * };
597
- * const relationships = {
598
- * tags: { manyToMany: { via: 'article_tags' } }
599
- * };
600
- *
601
- * // Output: Minimal relationship objects
602
- * // {
603
- * // id: '300',
604
- * // title: 'My Article',
605
- * // tags: [{ id: '1' }, { id: '2' }, { id: '3' }] // Minimal objects
606
- * // }
607
- *
608
- * @example
609
- * // Input: Polymorphic relationship
610
- * const data = {
611
- * type: 'reviews',
612
- * id: '500',
613
- * attributes: { rating: 5, comment: 'Great!' },
614
- * relationships: {
615
- * reviewable: {
616
- * data: { type: 'authors', id: '75' }
617
- * }
618
- * }
619
- * };
620
- * const relationships = {
621
- * reviewable: {
622
- * belongsToPolymorphic: {
623
- * types: ['authors', 'publishers'],
624
- * typeField: 'reviewable_type',
625
- * idField: 'reviewable_id'
626
- * }
627
- * }
628
- * };
629
- *
630
- * // Output: Minimal polymorphic object with type
631
- * // {
632
- * // id: '500',
633
- * // rating: 5,
634
- * // comment: 'Great!',
635
- * // reviewable: { id: '75', type: 'authors' } // Minimal object with type
636
- * // }
637
- *
638
- * @example
639
- * // Input: Polymorphic relationship with included resource
640
- * const dataWithInclude = {
641
- * type: 'reviews',
642
- * id: '500',
643
- * attributes: { rating: 5, comment: 'Great!' },
644
- * relationships: {
645
- * reviewable: {
646
- * data: { type: 'authors', id: '75' }
647
- * }
648
- * }
649
- * };
650
- * const included = [
651
- * {
652
- * type: 'authors',
653
- * id: '75',
654
- * attributes: { name: 'Jane Smith', bio: 'Fantastic author' }
655
- * }
656
- * ];
657
- *
658
- * // Output: Full polymorphic object (type field removed as redundant)
659
- * // {
660
- * // id: '500',
661
- * // rating: 5,
662
- * // comment: 'Great!',
663
- * // reviewable: { // Full object when included
664
- * // id: '75',
665
- * // name: 'Jane Smith',
666
- * // bio: 'Fantastic author'
667
- * // }
668
- * // }
669
- *
670
- * @example
671
- * // Input: With included resources (recursive expansion)
672
- * const data = {
673
- * type: 'books',
674
- * id: '400',
675
- * attributes: { title: 'JavaScript Guide' },
676
- * relationships: {
677
- * author: {
678
- * data: { type: 'authors', id: '75' }
679
- * },
680
- * chapters: {
681
- * data: [
682
- * { type: 'chapters', id: '1001' },
683
- * { type: 'chapters', id: '1002' }
684
- * ]
685
- * }
686
- * }
687
- * };
688
- * const included = [
689
- * {
690
- * type: 'authors',
691
- * id: '75',
692
- * attributes: { name: 'Jane Smith' },
693
- * relationships: {
694
- * company: {
695
- * data: { type: 'companies', id: '5' }
696
- * }
697
- * }
698
- * },
699
- * {
700
- * type: 'chapters',
701
- * id: '1001',
702
- * attributes: { number: 1, title: 'Introduction' }
703
- * },
704
- * {
705
- * type: 'chapters',
706
- * id: '1002',
707
- * attributes: { number: 2, title: 'Basics' }
708
- * }
709
- * ];
710
- *
711
- * // Output: Recursively expanded
712
- * // {
713
- * // id: '400',
714
- * // title: 'JavaScript Guide',
715
- * // author: { // Expanded and transformed
716
- * // id: '75',
717
- * // name: 'Jane Smith',
718
- * // company: { id: '5' } // Author's relationships processed as minimal objects!
719
- * // },
720
- * // chapters: [
721
- * // { id: '1001', number: 1, title: 'Introduction' },
722
- * // { id: '1002', number: 2, title: 'Basics' }
723
- * // ]
724
- * // }
725
- *
726
- * @description
727
- * Used by:
728
- * - transformJsonApiToSimplified for each resource
729
- * - Calls itself recursively for included resources
730
- *
731
- * Purpose:
732
- * - Core transformation logic for single resources
733
- * - Creates minimal relationship objects from relationships
734
- * - Creates arrays of minimal relationship objects for to-many relationships
735
- * - Recursively expands and transforms included data
736
- * - Handles polymorphic relationships
737
- *
738
- * Data flow:
739
- * 1. Copies ID and attributes to result
740
- * 2. Processes each relationship:
741
- * - BelongsTo: creates minimal relationship object
742
- * - Polymorphic: creates minimal relationship object with type
743
- * - HasMany/ManyToMany: creates array of minimal relationship objects
744
- * 3. If included data exists:
745
- * - Finds matching included resources
746
- * - Recursively transforms them (preserving their relationships!)
747
- * - Adds transformed objects/arrays to result
748
- *
749
- * @private
207
+ * Flatten one resource and expand linkage found in the shared document index.
208
+ * Absent to-one linkage is omitted. Unexpanded references retain their ID;
209
+ * polymorphic references also retain _type. Cycle detection is path-local so
210
+ * repeated siblings expand independently while back-references stay minimal.
211
+ *
212
+ * @param {Object} scope
213
+ * @param {Object} scope.data - JSON:API resource
214
+ * @param {Array<Object>} [scope.included] - Used when no shared index is supplied
215
+ * @param {Map} [scope.includedByType] - Shared type/ID lookup
216
+ * @param {Object} deps
217
+ * @param {Object} deps.context
218
+ * @param {Object} [deps.context.schemaRelationships] - Relationship definitions
219
+ * @param {Object} deps.context.scopes - Registry for nested resource metadata
220
+ * @param {Set<string>} [ancestors] - Resource identities on the current path
221
+ * @returns {Object} Plain record with minimal or expanded relationship objects
750
222
  */
751
- export const transformSingleJsonApiToSimplified = (scope, deps) => {
752
- // Extract needed values from scope and deps
753
- const data = scope.data
754
- const included = scope.included
755
- const schema = deps.context.schemaStructure
756
- const relationships = deps.context.schemaRelationships
223
+ export const transformSingleJsonApiToSimplified = (scope, deps, ancestors = new Set()) => {
224
+ const { data, includedByType = indexIncludedResources(scope.included, scope.data) } = scope
225
+ const relationships = deps.context.schemaRelationships || {}
757
226
  const scopes = deps.context.scopes
758
-
759
- const simplified = {}
760
-
761
- // Add ID
762
- if (data.id) {
763
- simplified.id = data.id
764
- }
765
-
766
- // Add attributes
767
- Object.assign(simplified, data.attributes || {})
768
-
769
- // Process relationships to create minimal objects
770
- if (data.relationships) {
771
- for (const [relName, relData] of Object.entries(data.relationships)) {
772
- // Find schema field for this belongsTo relationship
773
- const schemaEntry = Object.entries(schema).find(([_, def]) => def.as === relName)
774
-
775
- if (schemaEntry) {
776
- const [, fieldDef] = schemaEntry
777
- if (fieldDef.belongsTo && relData.data) {
778
- // Create minimal relationship object (no more foreign key field)
779
- simplified[relName] = { id: relData.data.id }
780
- }
781
- }
782
-
783
- // Check if this is a polymorphic relationship from the relationships config
784
- const rel = relationships?.[relName]
785
- if (rel?.belongsToPolymorphic && relData.data) {
786
- // Create minimal relationship object for polymorphic (no more type/id fields)
787
- simplified[relName] = { id: relData.data.id, _type: relData.data.type }
788
- }
789
-
790
- // Handle to-many relationships (create minimal objects with just IDs)
791
- if (rel?.type === 'hasMany' || rel?.type === 'manyToMany') {
792
- if (relData.data && Array.isArray(relData.data)) {
793
- simplified[relName] = relData.data.map(item => ({ id: item.id }))
794
- }
795
- }
796
-
797
- // Handle includes (nested objects)
798
- if (included && relData.data) {
799
- const findIncluded = (ref) =>
800
- included.find(inc => inc.type === ref.type && inc.id === ref.id)
801
-
802
- if (Array.isArray(relData.data)) {
803
- const nestedData = relData.data.map(findIncluded).filter(Boolean)
804
- if (nestedData.length > 0) {
805
- simplified[relName] = nestedData.map(item => {
806
- // For each included record, transform it recursively to restore its relationship fields
807
- const itemSimplified = transformSingleJsonApiToSimplified(
808
- { data: item, included: null },
809
- {
810
- context: {
811
- schemaStructure: scopes?.[item.type]?.vars?.schemaInfo?.schema || {},
812
- schemaRelationships: scopes?.[item.type]?.vars?.schemaInfo?.schemaRelationships || {},
813
- scopes
814
- }
815
- }
816
- )
817
- // For polymorphic relationships, add _type field to identify the resource type
818
- if (rel?.belongsToPolymorphic) {
819
- itemSimplified._type = item.type
820
- }
821
- return itemSimplified
822
- })
823
- }
824
- } else {
825
- const nestedData = findIncluded(relData.data)
826
- if (nestedData) {
827
- // Transform the single included record to restore its relationship fields
828
- const itemSimplified = transformSingleJsonApiToSimplified(
829
- { data: nestedData, included: null },
830
- {
831
- context: {
832
- schemaStructure: scopes?.[nestedData.type]?.vars?.schemaInfo?.schema || {},
833
- schemaRelationships: scopes?.[nestedData.type]?.vars?.schemaInfo?.schemaRelationships || {},
834
- scopes
835
- }
836
- }
837
- )
838
- // For polymorphic relationships, add _type field to identify the resource type
839
- if (rel?.belongsToPolymorphic) {
840
- itemSimplified._type = nestedData.type
841
- }
842
- simplified[relName] = itemSimplified
843
- }
844
- }
845
- }
227
+ const identity = JSON.stringify([data.type, data.id])
228
+ const path = new Set(ancestors)
229
+ path.add(identity)
230
+
231
+ const simplified = { ...(data.id != null ? { id: data.id } : {}), ...data.attributes }
232
+ for (const [name, relationship] of Object.entries(data.relationships || {})) {
233
+ if (relationship?.data == null) continue
234
+ const polymorphic = relationships[name]?.belongsToPolymorphic
235
+ const convert = reference => {
236
+ const minimal = { id: reference.id, ...(polymorphic ? { _type: reference.type } : {}) }
237
+ // Expand repeated siblings independently; stop only cycles on this path.
238
+ if (path.has(JSON.stringify([reference.type, reference.id]))) return minimal
239
+ const target = includedByType.get(reference.type)?.get(reference.id)
240
+ if (!target) return minimal
241
+ const schemaInfo = scopes?.[target.type]?.vars?.schemaInfo
242
+ const nested = transformSingleJsonApiToSimplified({ data: target, includedByType }, {
243
+ context: { schemaStructure: schemaInfo?.schemaStructure, schemaRelationships: schemaInfo?.schemaRelationships, scopes }
244
+ }, path)
245
+ if (polymorphic) nested._type = reference.type
246
+ return nested
846
247
  }
248
+ simplified[name] = Array.isArray(relationship.data) ? relationship.data.map(convert) : convert(relationship.data)
847
249
  }
848
-
849
250
  return simplified
850
251
  }