json-rest-api 1.0.28 → 2.0.0

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