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
@@ -1,366 +1,96 @@
1
- import { getForeignKeyFields as getForeignKeyFieldsFromUtils } from '../querying-writing/field-utils.js'
2
- import { RELATIONSHIPS_KEY, RELATIONSHIP_METADATA_KEY, ROW_NUMBER_KEY, COMPUTED_DEPENDENCIES_KEY, getSchemaStructure } from '../querying-writing/knex-constants.js'
3
- import { getUrlPrefix, buildResourceUrl, buildRelationshipUrl } from './url-helpers.js'
1
+ // @ts-check
2
+ /** @import { JsonApiRelationship, JsonApiResource } from '../../../../types/representations.js' */
3
+ /** @import { DataDocument, DataOperationScope, DataReadContext, DataResource, DataStorageRow, ResourceConversionScope, StorageRow } from '../storage/storage-types.js' */
4
+ import { RELATIONSHIPS_KEY, RELATIONSHIP_METADATA_KEY, ROW_NUMBER_KEY, COMPUTED_DEPENDENCIES_KEY } from '../querying-writing/knex-constants.js'
5
+ import { getUrlPrefix, buildResourceUrl, buildRelationshipUrl, buildJsonApiLink } from './url-helpers.js'
4
6
  import { translateRecordFromStorage } from '../storage/storage-mapping.js'
7
+ import { getPolymorphicLinkage } from '../querying-writing/relationship-contracts.js'
8
+ import { wrapUnexpectedError } from '../../../../lib/error-context.js'
9
+
10
+ const internalFields = new Set([
11
+ RELATIONSHIPS_KEY,
12
+ RELATIONSHIP_METADATA_KEY,
13
+ ROW_NUMBER_KEY,
14
+ COMPUTED_DEPENDENCIES_KEY
15
+ ])
5
16
 
6
17
  /**
7
- * Transforms a flat database record into JSON:API resource format
8
- *
9
- * @private
10
- * @param {Object} scope - Scope containing schema and configuration
11
- * @param {Object} record - Database record to transform
12
- * @param {Object} deps - Dependencies including context with scopeName and polymorphicFields
13
- * @returns {Object|null} JSON:API resource object or null if no record
14
- *
15
- * @example
16
- * // Input: Database record with foreign keys and internal fields
17
- * const record = {
18
- * id: 1,
19
- * title: 'Hello World',
20
- * content: 'Article content',
21
- * author_id: 2, // Foreign key
22
- * category_id: 5, // Foreign key
23
- * __$jsonrestapi_rn$__: 1 // Internal field
24
- * };
25
- *
26
- * const jsonApiResource = toJsonApi(scope, record, deps);
27
- *
28
- * // Output: Clean JSON:API resource
29
- * // {
30
- * // type: 'articles',
31
- * // id: '1',
32
- * // attributes: {
33
- * // title: 'Hello World',
34
- * // content: 'Article content'
35
- * // // Note: author_id, category_id, and internal fields are removed
36
- * // }
37
- * // }
38
- *
39
- * @example
40
- * // Input: Polymorphic record
41
- * const record = {
42
- * id: 10,
43
- * text: 'Great article!',
44
- * commentable_type: 'articles', // Polymorphic type field
45
- * commentable_id: 1 // Polymorphic id field
46
- * };
47
- * // deps.context.polymorphicFields = Set(['commentable_type', 'commentable_id'])
48
- *
49
- * const jsonApiResource = toJsonApi(scope, record, deps);
50
- *
51
- * // Output: Polymorphic fields filtered out
52
- * // {
53
- * // type: 'comments',
54
- * // id: '10',
55
- * // attributes: {
56
- * // text: 'Great article!'
57
- * // }
58
- * // }
59
- *
60
- * @description
61
- * Used by:
62
- * - toJsonApiRecord calls this as the core transformation logic
63
- * - buildJsonApiResponse uses this indirectly through toJsonApiRecord
64
- *
65
- * Purpose:
66
- * - Separates database structure from API structure
67
- * - Filters out foreign keys that become relationships in JSON:API
68
- * - Removes internal fields used for query optimization
69
- * - Ensures clean attribute objects without implementation details
70
- *
71
- * Data flow:
72
- * - Called after database query returns flat records
73
- * - Transforms each record individually
74
- * - Output feeds into relationship building and response assembly
18
+ * @overload
19
+ * @param {ResourceConversionScope} scope
20
+ * @param {StorageRow} record
21
+ * @param {string} scopeName
22
+ * @returns {JsonApiResource}
75
23
  */
76
- const toJsonApi = (scope, record, deps) => {
77
- if (!record) return null
78
-
79
- const schemaInfo = scope.vars.schemaInfo
80
- const schemaInstance = schemaInfo.schemaInstance
81
- const logicalRecord = translateRecordFromStorage(record, schemaInfo)
24
+ /**
25
+ * @overload
26
+ * @param {ResourceConversionScope} scope
27
+ * @param {StorageRow | null | undefined} record
28
+ * @param {string} scopeName
29
+ * @returns {JsonApiResource | null}
30
+ */
31
+ /** @param {ResourceConversionScope} scope @param {StorageRow | null | undefined} record @param {string} scopeName @returns {JsonApiResource | null} */
32
+ export function toJsonApiRecord (scope, record, scopeName) {
33
+ let foreignKeys
34
+ try {
35
+ foreignKeys = scope.vars.schemaInfo.foreignKeyFields
36
+ } catch (error) {
37
+ throw wrapUnexpectedError(error, {
38
+ message: `Failed to read relationship metadata for '${scopeName}'`,
39
+ context: { scopeName, phase: 'relationshipMetadata' }
40
+ })
41
+ }
82
42
 
83
- const { context } = deps
84
- const scopeName = context.scopeName
85
- const idProperty = context.schemaInfo?.idProperty || 'id'
86
- const polymorphicFields = context.polymorphicFields || new Set()
43
+ const { idProperty = 'id' } = scope.vars.schemaInfo
44
+ if (!record) return null
87
45
 
46
+ const logicalRecord = translateRecordFromStorage(record, scope.vars.schemaInfo)
88
47
  const { id, ...allAttributes } = logicalRecord
89
-
90
- const foreignKeys = schemaInstance ? getForeignKeyFieldsFromUtils(schemaInstance) : new Set()
91
-
92
- const internalFields = new Set([
93
- RELATIONSHIPS_KEY,
94
- RELATIONSHIP_METADATA_KEY,
95
- ROW_NUMBER_KEY,
96
- COMPUTED_DEPENDENCIES_KEY
97
- ])
98
-
48
+ /** @type {StorageRow} */
99
49
  const attributes = {}
100
- Object.entries(allAttributes).forEach(([key, value]) => {
101
- if (!foreignKeys.has(key) && !polymorphicFields.has(key) && !internalFields.has(key) && key !== idProperty) {
50
+ for (const [key, value] of Object.entries(allAttributes)) {
51
+ if (!foreignKeys.has(key) && !internalFields.has(key) && key !== idProperty) {
102
52
  attributes[key] = value
103
53
  }
104
- })
105
-
106
- return {
107
- type: scopeName,
108
- id: String(id),
109
- attributes
110
54
  }
55
+ return { type: scopeName, id: String(id), attributes }
111
56
  }
112
57
 
113
58
  /**
114
- * Converts a database record to JSON:API format with proper field filtering
115
- *
116
- * @param {Object} scope - Scope containing schema and relationship definitions
117
- * @param {Object} record - Raw database record
118
- * @param {string} scopeName - Resource type name
119
- * @returns {Object} JSON:API formatted resource
120
- *
121
- * @example
122
- * // Input: Database record with various field types
123
- * const record = {
124
- * id: 1,
125
- * title: 'My Article',
126
- * author_id: 2, // belongsTo relationship
127
- * category_id: 3, // belongsTo relationship
128
- * views: 150,
129
- * published: true
130
- * };
131
- *
132
- * const jsonApi = toJsonApiRecord(scope, record, 'articles');
133
- *
134
- * // Output: Foreign keys filtered out
135
- * // {
136
- * // type: 'articles',
137
- * // id: '1',
138
- * // attributes: {
139
- * // title: 'My Article',
140
- * // views: 150,
141
- * // published: true
142
- * // }
143
- * // }
144
- *
145
- * @example
146
- * // Input: Record with polymorphic relationship
147
- * const record = {
148
- * id: 5,
149
- * body: 'Nice post!',
150
- * author_id: 10,
151
- * commentable_type: 'posts', // Polymorphic
152
- * commentable_id: 3 // Polymorphic
153
- * };
154
- * // Schema has polymorphic relationship defined
155
- *
156
- * const jsonApi = toJsonApiRecord(scope, record, 'comments');
157
- *
158
- * // Output: Both foreign keys and polymorphic fields filtered
159
- * // {
160
- * // type: 'comments',
161
- * // id: '5',
162
- * // attributes: {
163
- * // body: 'Nice post!'
164
- * // }
165
- * // }
166
- *
167
- * @description
168
- * Used by:
169
- * - rest-api-knex-plugin's dataGet method for single resource responses
170
- * - rest-api-knex-plugin's dataQuery method for collection responses
171
- * - processIncludes when transforming included resources
172
- * - buildJsonApiResponse as the primary transformation function
173
- *
174
- * Purpose:
175
- * - Provides consistent JSON:API transformation across all query operations
176
- * - Automatically identifies and filters foreign keys based on schema
177
- * - Handles polymorphic relationship fields (type/id pairs)
178
- * - Ensures IDs are always strings as required by JSON:API spec
179
- *
180
- * Data flow:
181
- * 1. Database query returns flat records with all fields
182
- * 2. toJsonApiRecord identifies foreign keys from schema
183
- * 3. Filters out foreign keys and internal fields from attributes
184
- * 4. Returns clean JSON:API resource ready for relationship processing
185
- * 5. buildJsonApiResponse adds relationships and links to complete the response
59
+ * @overload
60
+ * @param {DataOperationScope} scope
61
+ * @param {DataStorageRow[]} records
62
+ * @param {DataResource[]} included
63
+ * @param {true} isSingle
64
+ * @param {string} scopeName
65
+ * @param {DataReadContext} context
66
+ * @returns {Promise<DataDocument<DataResource>>}
186
67
  */
187
- export const toJsonApiRecord = (scope, record, scopeName) => {
188
- const {
189
- vars: {
190
- schemaInfo: { schemaRelationships: relationships, idProperty }
191
- }
192
- } = scope
193
-
194
- const polymorphicFields = new Set()
195
- try {
196
- Object.entries(relationships || {}).forEach(([relName, relDef]) => {
197
- if (relDef.belongsToPolymorphic) {
198
- const typeFieldName = relDef.belongsToPolymorphic.typeField || `${relName}_type`
199
- const idFieldName = relDef.belongsToPolymorphic.idField || `${relName}_id`
200
- polymorphicFields.add(typeFieldName)
201
- polymorphicFields.add(idFieldName)
202
- }
203
- })
204
- } catch (e) {
205
- }
206
-
207
- const deps = {
208
- context: {
209
- scopeName,
210
- schemaInfo: { idProperty },
211
- polymorphicFields
212
- }
213
- }
214
-
215
- return toJsonApi(scope, record, deps)
216
- }
217
-
218
68
  /**
219
- * Builds complete JSON:API response with data, relationships, links, and optional includes
220
- *
221
- * @async
222
- * @param {Object} scope - Scope containing schema and configuration
223
- * @param {Array<Object>} records - Primary records to include in response
224
- * @param {Array<Object>} included - Resources to include in 'included' array
225
- * @param {boolean} isSingle - Whether this is a single resource response
226
- * @param {string} scopeName - Resource type name
227
- * @param {Object} context - Request context with pagination metadata
228
- * @returns {Promise<Object>} Complete JSON:API response document
229
- *
230
- * @example
231
- * // Input: Single article with author include
232
- * const records = [{
233
- * id: 1,
234
- * title: 'Article Title',
235
- * content: 'Article content',
236
- * author_id: 2,
237
- * category_id: 3
238
- * }];
239
- *
240
- * const included = [{
241
- * type: 'authors',
242
- * id: '2',
243
- * attributes: { name: 'John Doe', email: 'john@example.com' }
244
- * }];
245
- *
246
- * const response = await buildJsonApiResponse(scope, records, included, true, 'articles', context);
247
- *
248
- * // Output: Complete JSON:API document
249
- * // {
250
- * // data: {
251
- * // type: 'articles',
252
- * // id: '1',
253
- * // attributes: {
254
- * // title: 'Article Title',
255
- * // content: 'Article content'
256
- * // },
257
- * // relationships: {
258
- * // author: {
259
- * // data: { type: 'authors', id: '2' },
260
- * // links: {
261
- * // self: '/articles/1/relationships/author',
262
- * // related: '/articles/1/author'
263
- * // }
264
- * // },
265
- * // category: {
266
- * // data: { type: 'categories', id: '3' },
267
- * // links: {
268
- * // self: '/articles/1/relationships/category',
269
- * // related: '/articles/1/category'
270
- * // }
271
- * // }
272
- * // },
273
- * // links: {
274
- * // self: '/articles/1'
275
- * // }
276
- * // },
277
- * // included: [{
278
- * // type: 'authors',
279
- * // id: '2',
280
- * // attributes: { name: 'John Doe', email: 'john@example.com' },
281
- * // links: { self: '/authors/2' }
282
- * // }],
283
- * // links: {
284
- * // self: '/articles/1'
285
- * // }
286
- * // }
287
- *
288
- * @example
289
- * // Input: Collection with pagination
290
- * const records = [
291
- * { id: 1, title: 'Article 1', author_id: 10 },
292
- * { id: 2, title: 'Article 2', author_id: 10 },
293
- * { id: 3, title: 'Article 3', author_id: 11 }
294
- * ];
295
- *
296
- * context.returnMeta = {
297
- * paginationMeta: { page: 2, pageSize: 3, pageCount: 10, total: 30 },
298
- * paginationLinks: {
299
- * self: '/articles?page[number]=2&page[size]=3',
300
- * first: '/articles?page[number]=1&page[size]=3',
301
- * prev: '/articles?page[number]=1&page[size]=3',
302
- * next: '/articles?page[number]=3&page[size]=3',
303
- * last: '/articles?page[number]=10&page[size]=3'
304
- * }
305
- * };
306
- *
307
- * const response = await buildJsonApiResponse(scope, records, [], false, 'articles', context);
308
- *
309
- * // Output: Collection with pagination metadata
310
- * // {
311
- * // data: [
312
- * // { type: 'articles', id: '1', attributes: { title: 'Article 1' }, ... },
313
- * // { type: 'articles', id: '2', attributes: { title: 'Article 2' }, ... },
314
- * // { type: 'articles', id: '3', attributes: { title: 'Article 3' }, ... }
315
- * // ],
316
- * // meta: {
317
- * // pagination: { page: 2, pageSize: 3, pageCount: 10, total: 30 }
318
- * // },
319
- * // links: {
320
- * // self: '/articles?page[number]=2&page[size]=3',
321
- * // first: '/articles?page[number]=1&page[size]=3',
322
- * // prev: '/articles?page[number]=1&page[size]=3',
323
- * // next: '/articles?page[number]=3&page[size]=3',
324
- * // last: '/articles?page[number]=10&page[size]=3'
325
- * // }
326
- * // }
327
- *
328
- * @description
329
- * Used by:
330
- * - rest-api-knex-plugin's dataGet method for single resource responses
331
- * - rest-api-knex-plugin's dataQuery method for collection responses
332
- * - Called at the end of the query pipeline to assemble the final response
333
- *
334
- * Purpose:
335
- * - Assembles all parts of a JSON:API response in the correct structure
336
- * - Adds relationship objects with proper links for each foreign key
337
- * - Handles both regular belongsTo and polymorphic relationships
338
- * - Adds self links to all resources as required by JSON:API
339
- * - Includes pagination metadata and links when applicable
340
- *
341
- * Data flow:
342
- * 1. Query operations fetch primary records and optional includes
343
- * 2. Records are transformed to JSON:API format via toJsonApiRecord
344
- * 3. buildJsonApiResponse adds relationship objects for all foreign keys
345
- * 4. Adds proper links (self, related) for navigating the API
346
- * 5. Assembles included resources with their own links
347
- * 6. Adds pagination metadata and links if provided
348
- * 7. Returns complete JSON:API document ready for HTTP response
69
+ * @overload
70
+ * @param {DataOperationScope} scope
71
+ * @param {DataStorageRow[]} records
72
+ * @param {DataResource[]} included
73
+ * @param {false} isSingle
74
+ * @param {string} scopeName
75
+ * @param {DataReadContext} context
76
+ * @returns {Promise<DataDocument<DataResource[]>>}
349
77
  */
350
- export const buildJsonApiResponse = async (scope, records, included = [], isSingle = false, scopeName, context) => {
351
- const {
352
- vars: {
353
- schemaInfo
354
- }
355
- } = scope
78
+ /**
79
+ * @param {DataOperationScope} scope
80
+ * @param {DataStorageRow[]} records
81
+ * @param {DataResource[]} included
82
+ * @param {boolean} isSingle
83
+ * @param {string} scopeName
84
+ * @param {DataReadContext} context
85
+ */
86
+ export async function buildJsonApiResponse (scope, records, included = [], isSingle = false, scopeName, context) {
87
+ const { schemaInfo } = scope.vars
356
88
 
357
89
  const {
358
- schemaInstance,
90
+ schemaStructure,
359
91
  schemaRelationships: relationships
360
92
  } = schemaInfo
361
93
 
362
- const schemaStructure = getSchemaStructure(schemaInstance)
363
-
364
94
  const processedRecords = records.map(record => {
365
95
  const { [RELATIONSHIPS_KEY]: _relationships, ...cleanRecord } = record
366
96
  const logicalRecord = translateRecordFromStorage(cleanRecord, schemaInfo)
@@ -372,34 +102,18 @@ export const buildJsonApiResponse = async (scope, records, included = [], isSing
372
102
  }
373
103
 
374
104
  for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
375
- if (fieldDef.belongsTo && fieldDef.as && fieldName in logicalRecord) {
105
+ if (fieldDef.belongsTo && fieldDef.as && Object.hasOwn(logicalRecord, fieldName)) {
376
106
  jsonApiRecord.relationships = jsonApiRecord.relationships || {}
377
- if (!jsonApiRecord.relationships[fieldDef.as]) {
378
- if (logicalRecord[fieldName] !== null && logicalRecord[fieldName] !== undefined) {
379
- const relationshipObject = {
380
- data: {
381
- type: fieldDef.belongsTo,
382
- id: String(logicalRecord[fieldName])
383
- }
384
- }
385
-
386
- relationshipObject.links = {
107
+ if (!Object.hasOwn(jsonApiRecord.relationships, fieldDef.as)) {
108
+ const value = logicalRecord[fieldName]
109
+ jsonApiRecord.relationships[fieldDef.as] = {
110
+ data: value === null || value === undefined
111
+ ? null
112
+ : { type: fieldDef.belongsTo, id: String(value) },
113
+ links: {
387
114
  self: buildRelationshipUrl(context, scope, scopeName, resourceId, fieldDef.as, true),
388
115
  related: buildRelationshipUrl(context, scope, scopeName, resourceId, fieldDef.as, false)
389
116
  }
390
-
391
- jsonApiRecord.relationships[fieldDef.as] = relationshipObject
392
- } else {
393
- const relationshipObject = {
394
- data: null
395
- }
396
-
397
- relationshipObject.links = {
398
- self: buildRelationshipUrl(context, scope, scopeName, resourceId, fieldDef.as, true),
399
- related: buildRelationshipUrl(context, scope, scopeName, resourceId, fieldDef.as, false)
400
- }
401
-
402
- jsonApiRecord.relationships[fieldDef.as] = relationshipObject
403
117
  }
404
118
  }
405
119
  }
@@ -409,27 +123,18 @@ export const buildJsonApiResponse = async (scope, records, included = [], isSing
409
123
  if (relDef.belongsToPolymorphic) {
410
124
  const typeValue = logicalRecord[relDef.belongsToPolymorphic.typeField]
411
125
  const idValue = logicalRecord[relDef.belongsToPolymorphic.idField]
412
-
413
- if (typeValue && idValue) {
126
+ const data = getPolymorphicLinkage({
127
+ type: typeValue,
128
+ id: idValue,
129
+ types: relDef.belongsToPolymorphic.types,
130
+ scopeName,
131
+ relationshipName: relName
132
+ })
133
+
134
+ if (data || typeValue === null || idValue === null) {
414
135
  jsonApiRecord.relationships = jsonApiRecord.relationships || {}
415
- const relationshipObject = {
416
- data: {
417
- type: typeValue,
418
- id: String(idValue)
419
- }
420
- }
421
-
422
- relationshipObject.links = {
423
- self: buildRelationshipUrl(context, scope, scopeName, resourceId, relName, true),
424
- related: buildRelationshipUrl(context, scope, scopeName, resourceId, relName, false)
425
- }
426
-
427
- jsonApiRecord.relationships[relName] = relationshipObject
428
- } else if (typeValue === null || idValue === null) {
429
- jsonApiRecord.relationships = jsonApiRecord.relationships || {}
430
- const relationshipObject = {
431
- data: null
432
- }
136
+ /** @type {JsonApiRelationship} */
137
+ const relationshipObject = { data }
433
138
 
434
139
  relationshipObject.links = {
435
140
  self: buildRelationshipUrl(context, scope, scopeName, resourceId, relName, true),
@@ -441,28 +146,21 @@ export const buildJsonApiResponse = async (scope, records, included = [], isSing
441
146
  }
442
147
  })
443
148
 
149
+ jsonApiRecord.links ||= {}
150
+ jsonApiRecord.links.self = buildResourceUrl(context, scope, scopeName, resourceId)
444
151
  return jsonApiRecord
445
152
  })
446
153
 
447
- const normalizedData = isSingle ? processedRecords[0] : processedRecords
448
-
449
- if (normalizedData) {
450
- if (Array.isArray(normalizedData)) {
451
- normalizedData.forEach(item => {
452
- if (!item.links) item.links = {}
453
- item.links.self = buildResourceUrl(context, scope, scopeName, item.id)
454
- })
455
- } else {
456
- if (!normalizedData.links) normalizedData.links = {}
457
- normalizedData.links.self = buildResourceUrl(context, scope, scopeName, normalizedData.id)
458
- }
459
- }
154
+ const singleRecord = processedRecords[0]
155
+ if (isSingle && !singleRecord) throw new Error('A single-resource response requires a record')
156
+ const normalizedData = isSingle && singleRecord ? singleRecord : processedRecords
460
157
 
158
+ /** @type {DataDocument<DataResource | DataResource[]>} */
461
159
  const response = {
462
160
  data: normalizedData
463
161
  }
464
162
 
465
- if (included.length > 0) {
163
+ if (included.length > 0 || context.queryParams?.include !== undefined) {
466
164
  included.forEach(item => {
467
165
  if (!item.links) item.links = {}
468
166
  item.links.self = buildResourceUrl(context, scope, item.type, item.id)
@@ -482,8 +180,8 @@ export const buildJsonApiResponse = async (scope, records, included = [], isSing
482
180
  } else {
483
181
  const urlPrefix = getUrlPrefix(context, scope)
484
182
  response.links = {
485
- self: isSingle
486
- ? buildResourceUrl(context, scope, scopeName, normalizedData.id)
183
+ self: isSingle && singleRecord
184
+ ? buildJsonApiLink(buildResourceUrl(context, scope, scopeName, singleRecord.id), context.queryParams)
487
185
  : `${urlPrefix}/${scopeName}${context?.returnMeta?.queryString || ''}`
488
186
  }
489
187
  }