json-rest-api 1.0.29 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -148
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. package/tests/virtual-fields.test.js +0 -404
@@ -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
  }