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,133 +1,48 @@
1
+ // @ts-check
2
+ /** @import { ResourceConversionScope, StorageRow } from '../storage/storage-types.js' */
3
+ /** @import { JsonApiResource, JsonApiRelationship } from '../../../../types/representations.js' */
1
4
  import { toJsonApiRecord } from '../querying/knex-json-api-transformers-querying.js'
2
- import { getSchemaStructure } from './knex-constants.js'
3
5
  import { translateRecordFromStorage } from '../storage/storage-mapping.js'
6
+ import { getPolymorphicLinkage } from './relationship-contracts.js'
4
7
 
8
+ /** @overload @param {ResourceConversionScope} scope @param {StorageRow} record @param {string} scopeName @returns {JsonApiResource} */
9
+ /** @overload @param {ResourceConversionScope} scope @param {StorageRow | null | undefined} record @param {string} scopeName @returns {JsonApiResource | null} */
5
10
  /**
6
- * Converts database record to JSON:API format with belongsTo relationships
7
- *
8
- * @param {Object} scope - Resource scope with schema and relationship info
9
- * @param {Object} record - Raw database record
10
- * @param {string} scopeName - Resource type name
11
- * @returns {Object} JSON:API resource with belongsTo relationships
12
- *
13
- * @example
14
- * // Input: Database record with foreign keys
15
- * const dbRecord = {
16
- * id: 1,
17
- * title: 'Hello World',
18
- * author_id: 2,
19
- * publisher_id: 3,
20
- * category_id: null
21
- * };
22
- * const result = toJsonApiRecordWithBelongsTo(scope, dbRecord, 'articles');
23
- *
24
- * // Output: JSON:API with relationship objects
25
- * // {
26
- * // type: 'articles',
27
- * // id: '1',
28
- * // attributes: {
29
- * // title: 'Hello World'
30
- * // // Note: author_id, publisher_id, category_id removed
31
- * // },
32
- * // relationships: {
33
- * // author: {
34
- * // data: { type: 'authors', id: '2' }
35
- * // },
36
- * // publisher: {
37
- * // data: { type: 'publishers', id: '3' }
38
- * // },
39
- * // category: {
40
- * // data: null // Explicit null for empty relationship
41
- * // }
42
- * // }
43
- * // }
44
- *
45
- * @example
46
- * // Input: Polymorphic relationships
47
- * const dbRecord = {
48
- * id: 5,
49
- * content: 'Great post!',
50
- * commentable_type: 'articles',
51
- * commentable_id: 10
52
- * };
53
- * // With schema defining polymorphic relationship:
54
- * // relationships: {
55
- * // commentable: {
56
- * // belongsToPolymorphic: true,
57
- * // typeField: 'commentable_type',
58
- * // idField: 'commentable_id'
59
- * // }
60
- * // }
61
- *
62
- * // Output: Polymorphic relationship in JSON:API
63
- * // {
64
- * // type: 'comments',
65
- * // id: '5',
66
- * // attributes: { content: 'Great post!' },
67
- * // relationships: {
68
- * // commentable: {
69
- * // data: { type: 'articles', id: '10' }
70
- * // }
71
- * // }
72
- * // }
73
- *
74
- * @description
75
- * Used by:
76
- * - dataGetMinimal for lightweight single record fetches
77
- * - dataDelete to return deleted record structure
78
- * - Internal operations needing quick JSON:API conversion
79
- *
80
- * Purpose:
81
- * - Provides JSON:API structure without loading related data
82
- * - Transforms foreign keys into relationship objects
83
- * - Handles both regular and polymorphic belongsTo
84
- * - Maintains explicit nulls for empty relationships
85
- *
86
- * Data flow:
87
- * 1. Calls toJsonApiRecord for basic transformation
88
- * 2. Scans schema for belongsTo field definitions
89
- * 3. Converts foreign key values to relationship objects
90
- * 4. Processes polymorphic relationships separately
91
- * 5. Returns complete JSON:API resource object
11
+ * Convert one storage row and its belongs-to linkage without loading targets.
12
+ * Empty foreign keys become null linkage; backing fields stay out of attributes.
13
+ * @param {ResourceConversionScope} scope
14
+ * @param {StorageRow | null | undefined} record
15
+ * @param {string} scopeName
16
+ * @returns {JsonApiResource | null}
92
17
  */
93
- export const toJsonApiRecordWithBelongsTo = (scope, record, scopeName) => {
18
+ export function toJsonApiRecordWithBelongsTo (scope, record, scopeName) {
94
19
  if (!record) return null
95
20
 
96
21
  const schemaInfo = scope.vars.schemaInfo
97
22
  const logicalRecord = translateRecordFromStorage(record, schemaInfo)
98
23
 
99
24
  // Get the basic JSON:API structure (without relationships)
100
- const jsonApiRecord = toJsonApiRecord(scope, logicalRecord, scopeName)
25
+ const jsonApiRecord = toJsonApiRecord(scope, record, scopeName)
101
26
 
102
27
  // Extract schema info from scope
103
28
  const {
104
- schemaInstance,
29
+ schemaStructure,
105
30
  schemaRelationships: relationships
106
31
  } = schemaInfo
107
32
 
108
- // Get schema structure
109
- const schemaStructure = getSchemaStructure(schemaInstance)
110
-
111
33
  // Initialize relationships object
112
- jsonApiRecord.relationships = {}
34
+ /** @type {Record<string, JsonApiRelationship>} */
35
+ const relationshipData = jsonApiRecord.relationships = {}
113
36
 
114
37
  // Process regular belongsTo relationships from schema
115
38
  for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
116
39
  if (fieldDef.belongsTo && fieldDef.as) {
117
40
  const fieldValue = logicalRecord[fieldName]
118
41
 
119
- if (fieldValue !== null && fieldValue !== undefined) {
120
- jsonApiRecord.relationships[fieldDef.as] = {
121
- data: {
122
- type: fieldDef.belongsTo,
123
- id: String(fieldValue)
124
- }
125
- }
126
- } else {
127
- // Explicitly null relationship
128
- jsonApiRecord.relationships[fieldDef.as] = {
129
- data: null
130
- }
42
+ relationshipData[fieldDef.as] = {
43
+ data: fieldValue === null || fieldValue === undefined
44
+ ? null
45
+ : { type: fieldDef.belongsTo, id: String(fieldValue) }
131
46
  }
132
47
  }
133
48
  }
@@ -135,21 +50,17 @@ export const toJsonApiRecordWithBelongsTo = (scope, record, scopeName) => {
135
50
  // Process polymorphic belongsTo relationships
136
51
  Object.entries(relationships || {}).forEach(([relName, relDef]) => {
137
52
  if (relDef.belongsToPolymorphic) {
138
- const typeValue = logicalRecord[relDef.typeField]
139
- const idValue = logicalRecord[relDef.idField]
53
+ const typeValue = logicalRecord[relDef.belongsToPolymorphic.typeField]
54
+ const idValue = logicalRecord[relDef.belongsToPolymorphic.idField]
140
55
 
141
- if (typeValue && idValue) {
142
- jsonApiRecord.relationships[relName] = {
143
- data: {
144
- type: typeValue,
145
- id: String(idValue)
146
- }
147
- }
148
- } else {
149
- // Explicitly null relationship
150
- jsonApiRecord.relationships[relName] = {
151
- data: null
152
- }
56
+ relationshipData[relName] = {
57
+ data: getPolymorphicLinkage({
58
+ type: typeValue,
59
+ id: idValue,
60
+ types: relDef.belongsToPolymorphic.types,
61
+ scopeName,
62
+ relationshipName: relName
63
+ })
153
64
  }
154
65
  }
155
66
  })
@@ -1,5 +1,67 @@
1
+ // @ts-check
2
+ /** @import { StorageDatabase, StorageFieldDefinition } from '../storage/storage-types.js' */
3
+ /** @import { CompiledQueryField, ProjectionExpression, ProjectionRequest, ProjectionRuntime, QueryFieldCompilation, QueryFieldDeclaration } from './query-field-types.js' */
1
4
  import { getStorageColumn } from '../storage/storage-mapping.js'
5
+ import { wrapUnexpectedError } from '../../../../lib/error-context.js'
2
6
 
7
+ // PostgreSQL json has no equality operator for DISTINCT parent selections.
8
+ /** @template {string | ProjectionExpression} T @param {T} expression @param {StorageFieldDefinition | undefined} definition @param {StorageDatabase | null} db @returns {T | import('knex').Knex.Raw<unknown>} */
9
+ export function normalizeStructuredSelect (expression, definition, db) {
10
+ if (!db || !['pg', 'postgresql'].includes(db.client.config.client) || !['object', 'array'].includes(definition?.type || '')) return expression
11
+ return typeof expression === 'string'
12
+ ? db.raw('to_jsonb(??)', [expression])
13
+ : db.raw('to_jsonb((?))', [expression])
14
+ }
15
+
16
+ /** Compile projection declarations against the final resource namespace.
17
+ * @param {Record<string, QueryFieldDeclaration>} rawQueryFields
18
+ * @param {QueryFieldCompilation} options
19
+ * @returns {Record<string, CompiledQueryField>}
20
+ */
21
+ export function compileQueryFields (rawQueryFields, { scopeName, schemaStructure, computed, schemaRelationships, idProperty }) {
22
+ if (!rawQueryFields || typeof rawQueryFields !== 'object' || Array.isArray(rawQueryFields)) {
23
+ throw new Error(`Invalid queryFields configuration for scope '${scopeName}'. Expected an object.`)
24
+ }
25
+ const reservedNames = new Set([
26
+ 'id', idProperty,
27
+ ...Object.keys(schemaStructure),
28
+ ...Object.keys(computed),
29
+ ...Object.keys(schemaRelationships)
30
+ ])
31
+ for (const definition of Object.values(schemaStructure)) {
32
+ if (definition.belongsTo && definition.as) reservedNames.add(definition.as)
33
+ }
34
+ /** @type {Array<[string, CompiledQueryField]>} */
35
+ const normalized = []
36
+ for (const [fieldName, fieldDef] of Object.entries(rawQueryFields)) {
37
+ if (!fieldDef || typeof fieldDef !== 'object' || Array.isArray(fieldDef)) {
38
+ throw new Error(`Query field '${fieldName}' in scope '${scopeName}' must be an object.`)
39
+ }
40
+ if (reservedNames.has(fieldName)) {
41
+ throw new Error(`Query field '${fieldName}' in scope '${scopeName}' conflicts with an existing schema, computed, or relationship name.`)
42
+ }
43
+ if (!fieldDef.type) {
44
+ throw new Error(`Query field '${fieldName}' in scope '${scopeName}' must have a type.`)
45
+ }
46
+ if (fieldDef.storage !== undefined) {
47
+ throw new Error(`Query field '${fieldName}' in scope '${scopeName}' cannot declare storage. Projections select SQL values directly; put storage serializers on stored fields and use getters for public formatting.`)
48
+ }
49
+ const select = fieldDef.select || fieldDef.project
50
+ if (typeof select !== 'function') {
51
+ throw new Error(`Query field '${fieldName}' in scope '${scopeName}' must define a select() function.`)
52
+ }
53
+ normalized.push([fieldName, {
54
+ ...fieldDef,
55
+ select,
56
+ sortable: fieldDef.sortable === true,
57
+ hidden: fieldDef.hidden === true,
58
+ normallyHidden: fieldDef.normallyHidden === true
59
+ }])
60
+ }
61
+ return Object.fromEntries(normalized)
62
+ }
63
+
64
+ /** @param {string} columnName @param {string} tableName */
3
65
  function qualifyColumnReference (columnName, tableName) {
4
66
  if (!tableName || !columnName || columnName === '*' || String(columnName).includes('.')) {
5
67
  return columnName
@@ -8,6 +70,7 @@ function qualifyColumnReference (columnName, tableName) {
8
70
  return `${tableName}.${columnName}`
9
71
  }
10
72
 
73
+ /** @param {ProjectionExpression} expression */
11
74
  function normalizeQueryFieldSql (expression) {
12
75
  if (expression && typeof expression.toSQL === 'function') {
13
76
  const compiled = expression.toSQL()
@@ -20,6 +83,12 @@ function normalizeQueryFieldSql (expression) {
20
83
  throw new Error('Query field expressions must return knex raw, knex ref, or a knex query builder.')
21
84
  }
22
85
 
86
+ /** @param {unknown} value @returns {value is ProjectionExpression} */
87
+ export function isImmediateExpression (value) {
88
+ return value !== null && typeof value === 'object' && typeof Reflect.get(value, 'toSQL') === 'function'
89
+ }
90
+
91
+ /** @param {ProjectionRequest} [options] @returns {Promise<Map<string, ProjectionRuntime>>} */
23
92
  export async function buildQueryFieldRuntimes ({
24
93
  queryFieldNames = [],
25
94
  queryFields = {},
@@ -30,6 +99,7 @@ export async function buildQueryFieldRuntimes ({
30
99
  context = null,
31
100
  scopeName = ''
32
101
  } = {}) {
102
+ /** @type {Map<string, ProjectionRuntime>} */
33
103
  const runtimes = new Map()
34
104
  const seen = new Set()
35
105
 
@@ -44,44 +114,50 @@ export async function buildQueryFieldRuntimes ({
44
114
  throw new Error(`Unknown query field '${fieldName}' requested for '${scopeName}'.`)
45
115
  }
46
116
 
117
+ /** @param {string} logicalField */
47
118
  const column = (logicalField) => {
48
119
  const translated = storageAdapter?.translateColumn?.(logicalField) || getStorageColumn(schemaInfo, logicalField)
49
120
  return qualifyColumnReference(translated, tableName)
50
121
  }
51
122
 
52
- const selectResult = fieldDef.select({
53
- knex: db,
54
- db,
55
- context,
56
- scopeName,
57
- tableName,
58
- fieldName,
59
- schemaInfo,
60
- adapter: storageAdapter,
61
- column,
62
- ref: (logicalField) => db?.ref ? db.ref(column(logicalField)) : column(logicalField)
63
- })
64
-
65
- const expression = (
66
- selectResult &&
67
- typeof selectResult === 'object' &&
68
- typeof selectResult.toSQL === 'function'
69
- )
70
- ? selectResult
71
- : await selectResult
72
-
73
- if (expression === undefined || expression === null) {
74
- throw new Error(`Query field '${fieldName}' in scope '${scopeName}' returned an empty select expression.`)
75
- }
123
+ try {
124
+ const selectResult = fieldDef.select({
125
+ knex: db,
126
+ db,
127
+ context,
128
+ scopeName,
129
+ tableName,
130
+ fieldName,
131
+ schemaInfo,
132
+ adapter: storageAdapter,
133
+ column,
134
+ ref: (logicalField) => db?.ref ? db.ref(column(logicalField)) : column(logicalField)
135
+ })
136
+
137
+ const expression = isImmediateExpression(selectResult)
138
+ ? selectResult
139
+ : await selectResult
76
140
 
77
- const { sql, bindings } = normalizeQueryFieldSql(expression)
78
- runtimes.set(fieldName, {
79
- fieldName,
80
- definition: fieldDef,
81
- expression,
82
- sql,
83
- bindings
84
- })
141
+ if (expression === undefined || expression === null) {
142
+ throw new Error(`Query field '${fieldName}' in scope '${scopeName}' returned an empty select expression.`)
143
+ }
144
+
145
+ const selectedExpression = normalizeStructuredSelect(expression, fieldDef, db)
146
+ const { sql, bindings } = normalizeQueryFieldSql(selectedExpression)
147
+ runtimes.set(fieldName, {
148
+ fieldName,
149
+ definition: fieldDef,
150
+ schemaInfo,
151
+ expression: selectedExpression,
152
+ sql,
153
+ bindings
154
+ })
155
+ } catch (error) {
156
+ throw wrapUnexpectedError(error, {
157
+ message: `Failed to select query field '${fieldName}' for scope '${scopeName}'`,
158
+ context: { scopeName, fieldName, phase: 'projection' }
159
+ })
160
+ }
85
161
  }
86
162
 
87
163
  return runtimes
@@ -0,0 +1,60 @@
1
+ import type { Knex } from 'knex'
2
+ import type { StorageAdapter, StorageContext, StorageDatabase, StorageFieldDefinition, StorageRow, StorageSchemaInfo } from '../storage/storage-types.js'
3
+
4
+ export type ProjectionExpression = Knex.Raw<unknown> | Knex.QueryBuilder<StorageRow, unknown>
5
+
6
+ export interface ProjectionContext {
7
+ knex: StorageDatabase | null
8
+ db: StorageDatabase | null
9
+ context: StorageContext | null
10
+ scopeName: string
11
+ tableName: string
12
+ fieldName: string
13
+ schemaInfo: Partial<StorageSchemaInfo>
14
+ adapter: StorageAdapter | null
15
+ column: (logicalField: string) => string
16
+ ref: (logicalField: string) => string | Knex.Ref<string, Record<string, string>>
17
+ }
18
+
19
+ export type ProjectionSelector = (context: ProjectionContext) => ProjectionExpression
20
+
21
+ export interface QueryFieldDeclaration extends StorageFieldDefinition {
22
+ select?: ProjectionSelector
23
+ project?: ProjectionSelector
24
+ sortable?: boolean
25
+ }
26
+
27
+ export interface CompiledQueryField extends QueryFieldDeclaration {
28
+ select: ProjectionSelector
29
+ sortable: boolean
30
+ hidden: boolean
31
+ normallyHidden: boolean
32
+ }
33
+
34
+ export interface QueryFieldCompilation {
35
+ scopeName: string
36
+ schemaStructure: Record<string, StorageFieldDefinition>
37
+ computed: Record<string, StorageFieldDefinition>
38
+ schemaRelationships: Record<string, unknown>
39
+ idProperty: string
40
+ }
41
+
42
+ export interface ProjectionRequest {
43
+ queryFieldNames?: readonly string[]
44
+ queryFields?: Record<string, CompiledQueryField>
45
+ schemaInfo?: Partial<StorageSchemaInfo>
46
+ tableName?: string
47
+ storageAdapter?: StorageAdapter | null
48
+ db?: StorageDatabase | null
49
+ context?: StorageContext | null
50
+ scopeName?: string
51
+ }
52
+
53
+ export interface ProjectionRuntime {
54
+ fieldName: string
55
+ definition: CompiledQueryField
56
+ schemaInfo: Partial<StorageSchemaInfo>
57
+ expression: ProjectionExpression
58
+ sql: string
59
+ bindings: readonly unknown[]
60
+ }
@@ -1,23 +1,43 @@
1
+ // @ts-check
2
+ /** @import { StorageFieldDefinition } from '../storage/storage-types.js' */
1
3
  import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
4
+ import { wrapUnexpectedError } from '../../../../lib/error-context.js'
5
+ import { RELATIONSHIP_CAPABILITIES } from './database-capabilities.js'
2
6
 
3
- export function getRelationshipCardinality (relDef) {
4
- if (!relDef) return null
5
-
6
- if (
7
- relDef.belongsTo ||
8
- relDef.belongsToPolymorphic ||
9
- relDef.type === 'hasOne'
10
- ) {
11
- return 'one'
7
+ /** @param {{ type: unknown, id: unknown, types?: readonly string[] | null, scopeName?: string, relationshipName: string }} options */
8
+ export function getPolymorphicLinkage ({ type, id, types, scopeName, relationshipName }) {
9
+ if (type == null || id == null) return null
10
+ if (typeof type !== 'string' || !Array.isArray(types) || !types.includes(type)) {
11
+ throw wrapUnexpectedError(new Error(`Undeclared target type '${type}'`), {
12
+ message: `Invalid stored relationship '${scopeName}.${relationshipName}'`,
13
+ context: { scopeName, relationshipName, phase: 'relationshipData' }
14
+ })
12
15
  }
16
+ return { type, id: String(id) }
17
+ }
13
18
 
14
- if (relDef.type === 'hasMany' || relDef.type === 'manyToMany') {
15
- return 'many'
16
- }
19
+ /**
20
+ * @template {StorageFieldDefinition} Definition
21
+ * @param {{ outputRelationships?: Record<string, Definition> }} schemaInfo
22
+ * @param {string} relationshipName
23
+ */
24
+ export const findRelationshipDefinition = (schemaInfo, relationshipName) => {
25
+ const relationships = schemaInfo.outputRelationships || {}
26
+ return Object.hasOwn(relationships, relationshipName) ? relationships[relationshipName] || null : null
27
+ }
28
+
29
+ /** @param {StorageFieldDefinition | null} [relDef] */
30
+ export function getRelationshipCardinality (relDef) {
31
+ if (!relDef) return null
17
32
 
18
- return null
33
+ if (RELATIONSHIP_CAPABILITIES.attributeKinds.some(kind => relDef[kind])) return 'one'
34
+ const cardinalities = RELATIONSHIP_CAPABILITIES.declaredCardinalities
35
+ return typeof relDef.type === 'string' && Object.hasOwn(cardinalities, relDef.type)
36
+ ? cardinalities[/** @type {keyof typeof cardinalities} */ (relDef.type)]
37
+ : null
19
38
  }
20
39
 
40
+ /** @param {{ relationshipName: string, relDef?: StorageFieldDefinition | null, data: unknown, fieldPath?: string }} options */
21
41
  export function validateRelationshipDataCardinality ({
22
42
  relationshipName,
23
43
  relDef,