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,119 +1,19 @@
1
+ import { rejectRemovedOptions } from './response-options.js'
2
+ import { RestApiPayloadError } from '../../../../lib/rest-api-errors.js'
3
+
1
4
  /**
2
- * Parses URL query strings into JSON:API compliant parameter objects
3
- *
4
- * @param {string} queryString - The query string part of URL (without ?)
5
- * @returns {object} Parsed query parameters in JSON:API format
6
- *
7
- * @example
8
- * // Input: Basic include and sort
9
- * const query = parseJsonApiQuery('include=author&sort=-created_at');
10
- *
11
- * // Output: Parsed into arrays
12
- * // {
13
- * // include: ['author'], // Split by comma
14
- * // fields: {},
15
- * // filters: {},
16
- * // sort: ['-created_at'], // Split by comma
17
- * // page: {}
18
- * // }
19
- *
20
- * @example
21
- * // Input: Filters with bracket notation
22
- * const query = parseJsonApiQuery('filter[status]=published&filter[author_id]=123');
23
- *
24
- * // Output: Extracted filter keys
25
- * // {
26
- * // include: [],
27
- * // fields: {},
28
- * // filters: {
29
- * // status: 'published', // filter[status] → filters.status
30
- * // author_id: '123' // Values kept as strings
31
- * // },
32
- * // sort: [],
33
- * // page: {}
34
- * // }
35
- *
36
- * @example
37
- * // Input: Sparse fieldsets
38
- * const query = parseJsonApiQuery('fields[articles]=title,body&fields[users]=name');
39
- *
40
- * // Output: Fields kept as comma-separated strings
41
- * // {
42
- * // include: [],
43
- * // fields: {
44
- * // articles: 'title,body', // NOT split into array
45
- * // users: 'name' // Validation expects strings
46
- * // },
47
- * // filters: {},
48
- * // sort: [],
49
- * // page: {}
50
- * // }
51
- *
52
- * @example
53
- * // Input: Pagination with numeric conversion
54
- * const query = parseJsonApiQuery('page[size]=20&page[number]=3&page[cursor]=abc123');
5
+ * Parse JSON:API query parameters independently of a framework's query parser.
6
+ * Ordinary filters and sparse fields retain strings; filter[field][json] carries
7
+ * an explicit JSON value. Only page size/number are otherwise
8
+ * converted to numbers; opaque cursors keep their exact spelling. Retired
9
+ * controls fail clearly. Repeated keys use the final value.
55
10
  *
56
- * // Output: Numbers converted, strings preserved
57
- * // {
58
- * // include: [],
59
- * // fields: {},
60
- * // filters: {},
61
- * // sort: [],
62
- * // page: {
63
- * // size: 20, // "20" → 20 (numeric)
64
- * // number: 3, // "3" → 3 (numeric)
65
- * // cursor: 'abc123' // Non-numeric stays string
66
- * // }
67
- * // }
68
- *
69
- * @example
70
- * // Input: Complex real-world query
71
- * const query = parseJsonApiQuery(
72
- * 'include=author,comments.user&' +
73
- * 'filter[status]=published&' +
74
- * 'filter[created_after]=2024-01-01&' +
75
- * 'fields[articles]=title,summary&' +
76
- * 'sort=-created_at,title&' +
77
- * 'page[size]=10'
78
- * );
79
- *
80
- * // Output: All parameters properly categorized
81
- * // {
82
- * // include: ['author', 'comments.user'],
83
- * // fields: { articles: 'title,summary' },
84
- * // filters: {
85
- * // status: 'published',
86
- * // created_after: '2024-01-01'
87
- * // },
88
- * // sort: ['-created_at', 'title'],
89
- * // page: { size: 10 }
90
- * // }
91
- *
92
- * @description
93
- * Used by:
94
- * - express-plugin parses req.query with this
95
- * - http-plugin parses URL query strings with this
96
- * - websocket-plugin parses message query parameters with this
97
- *
98
- * Purpose:
99
- * - Provides uniform JSON:API query parsing across all transports
100
- * - Handles bracket notation without regex complexity
101
- * - Converts data types appropriately (numbers for pagination)
102
- * - Ignores non-JSON:API parameters gracefully
103
- * - Returns consistent structure even for empty input
104
- *
105
- * Data flow:
106
- * 1. Uses URLSearchParams for reliable parsing
107
- * 2. Categorizes parameters by their prefix pattern
108
- * 3. Extracts bracketed keys (filter[x] → x)
109
- * 4. Splits comma-separated values for include/sort
110
- * 5. Converts numeric strings for pagination
111
- * 6. Returns normalized structure for REST API
11
+ * @param {string} queryString - Query string without the leading question mark
12
+ * @returns {object} Include/sort arrays and fields/filters/page maps
112
13
  */
113
14
  export function parseJsonApiQuery (queryString) {
114
15
  if (!queryString) {
115
16
  return {
116
- include: [],
117
17
  fields: {},
118
18
  filters: {},
119
19
  sort: [],
@@ -122,8 +22,8 @@ export function parseJsonApiQuery (queryString) {
122
22
  }
123
23
 
124
24
  const params = new URLSearchParams(queryString)
25
+ rejectRemovedOptions(Object.fromEntries(params))
125
26
  const result = {
126
- include: [],
127
27
  fields: {},
128
28
  filters: {},
129
29
  sort: [],
@@ -131,6 +31,15 @@ export function parseJsonApiQuery (queryString) {
131
31
  }
132
32
 
133
33
  for (const [key, value] of params) {
34
+ const jsonFilter = key.match(/^filter\[(.+)\]\[json\]$/)
35
+ if (jsonFilter) {
36
+ let parsed
37
+ try { parsed = JSON.parse(value) } catch {
38
+ throw new RestApiPayloadError(`Invalid JSON value for query parameter '${key}'`, { parameter: key, expected: 'valid JSON' })
39
+ }
40
+ Object.defineProperty(result.filters, jsonFilter[1], { value: parsed, enumerable: true, writable: true, configurable: true })
41
+ continue
42
+ }
134
43
  if (key === 'include') {
135
44
  // Parse include (comma-separated string to array)
136
45
  result.include = value.split(',').map(s => s.trim()).filter(s => s.length > 0)
@@ -141,21 +50,22 @@ export function parseJsonApiQuery (queryString) {
141
50
  // Parse filter[key] = value into filters: { key: value }
142
51
  const filterKey = key.slice(7, -1) // Remove 'filter[' and ']'
143
52
  if (filterKey) {
144
- result.filters[filterKey] = value
53
+ Object.defineProperty(result.filters, filterKey, { value, enumerable: true, writable: true, configurable: true })
145
54
  }
146
55
  } else if (key.startsWith('fields[') && key.endsWith(']')) {
147
56
  // Parse fields[type] = fields into fields: { type: "field1,field2" }
148
57
  // Keep as comma-separated string to match REST API validation expectations
149
58
  const fieldType = key.slice(7, -1) // Remove 'fields[' and ']'
150
59
  if (fieldType) {
151
- result.fields[fieldType] = value
60
+ Object.defineProperty(result.fields, fieldType, { value, enumerable: true, writable: true, configurable: true })
152
61
  }
153
62
  } else if (key.startsWith('page[') && key.endsWith(']')) {
154
63
  // Parse page[size] = 10 into page: { size: 10 }
155
64
  const pageKey = key.slice(5, -1) // Remove 'page[' and ']'
156
65
  if (pageKey) {
157
- // Convert to number if it's a valid number, otherwise keep as string
158
- result.page[pageKey] = isNaN(value) ? value : parseInt(value, 10)
66
+ // Keep fractional values intact so the existing integer contract rejects them.
67
+ const parsed = ['size', 'number'].includes(pageKey) && value.trim() !== '' && !isNaN(value) ? Number(value) : value
68
+ Object.defineProperty(result.page, pageKey, { value: parsed, enumerable: true, writable: true, configurable: true })
159
69
  }
160
70
  }
161
71
  // Ignore other query parameters that don't match JSON:API patterns
@@ -177,12 +87,12 @@ const stringifyQueryValue = (value) => {
177
87
  }
178
88
 
179
89
  const appendCommaList = (params, key, value) => {
180
- if (!hasSerializableValue(value)) return
90
+ if (!hasSerializableValue(value) && !(key === 'include' && Array.isArray(value))) return
181
91
  const serialized = Array.isArray(value)
182
92
  ? value.map(stringifyQueryValue).filter((entry) => entry.length > 0).join(',')
183
93
  : stringifyQueryValue(value)
184
94
 
185
- if (serialized) {
95
+ if (serialized || key === 'include') {
186
96
  params.set(key, serialized)
187
97
  }
188
98
  }
@@ -191,7 +101,12 @@ const appendScopedParams = (params, publicName, values) => {
191
101
  if (!values || typeof values !== 'object' || Array.isArray(values)) return
192
102
 
193
103
  for (const [key, value] of Object.entries(values)) {
194
- if (!key || !hasSerializableValue(value)) continue
104
+ if (!key || value === undefined) continue
105
+ if (publicName === 'filter' && (value === null || (typeof value === 'object' && !(value instanceof Date)))) {
106
+ params.set(`${publicName}[${key}][json]`, JSON.stringify(value))
107
+ continue
108
+ }
109
+ if (!hasSerializableValue(value)) continue
195
110
  params.set(`${publicName}[${key}]`, stringifyQueryValue(value))
196
111
  }
197
112
  }
@@ -1,23 +1,13 @@
1
+ // @ts-check
2
+
3
+ import { errorMessage } from '../../../../lib/error-context.js'
4
+ import { formatDiagnosticValue } from '../../../../lib/error-formatter.js'
5
+
1
6
  /**
2
7
  * Parses database version string to comparable format
3
8
  *
4
9
  * @param {string} versionString - Raw version string from database
5
- * @returns {Object|null} Parsed version with major, minor, patch
6
- *
7
- * @example
8
- * // Input: MySQL version
9
- * parseVersion("8.0.33");
10
- * // Output: { major: 8, minor: 0, patch: 33 }
11
- *
12
- * @example
13
- * // Input: PostgreSQL version with extra info
14
- * parseVersion("14.5 (Ubuntu 14.5-1.pgdg20.04+1)");
15
- * // Output: { major: 14, minor: 5, patch: 0 }
16
- *
17
- * @example
18
- * // Input: MariaDB version
19
- * parseVersion("10.6.12-MariaDB");
20
- * // Output: { major: 10, minor: 6, patch: 12 }
10
+ * @returns {{major: number, minor: number, patch: number}|null} Parsed version
21
11
  */
22
12
  const parseVersion = (versionString) => {
23
13
  // MySQL: "8.0.33"
@@ -29,237 +19,173 @@ const parseVersion = (versionString) => {
29
19
  if (!match) return null
30
20
 
31
21
  return {
32
- major: parseInt(match[1]),
33
- minor: parseInt(match[2]),
34
- patch: match[3] ? parseInt(match[3]) : 0
22
+ major: Number(match[1]),
23
+ minor: Number(match[2]),
24
+ patch: match[3] ? Number(match[3]) : 0
35
25
  }
36
26
  }
37
27
 
38
28
  /**
39
29
  * Checks if the database supports window functions based on version
40
30
  *
41
- * @param {Object} knex - Knex instance
31
+ * @param {import('../storage/storage-types.js').StorageDatabase} knex - Knex instance
32
+ * @param {{ version: string }} [databaseInfo] - Already fetched version
33
+ * @param {import('../storage/storage-types.js').CapabilityLogger} [log] - Configured diagnostic sink
42
34
  * @returns {Promise<boolean>} True if window functions are supported
43
- *
44
- * @example
45
- * // Input: MySQL 8.0.33
46
- * const supports = await supportsWindowFunctions(knex);
47
- * // Output: true (MySQL 8.0+ supports window functions)
48
- *
49
- * @example
50
- * // Input: MySQL 5.7.42
51
- * const supports = await supportsWindowFunctions(knex);
52
- * // Output: false (MySQL < 8.0 doesn't support window functions)
53
- *
54
- * @example
55
- * // Input: PostgreSQL (any modern version)
56
- * const supports = await supportsWindowFunctions(knex);
57
- * // Output: true (PostgreSQL 8.4+ supports window functions)
58
- *
59
- * @example
60
- * // Input: SQLite 3.24.0
61
- * const supports = await supportsWindowFunctions(knex);
62
- * // Output: false (SQLite needs 3.25.0+)
63
- *
64
- * @description
65
- * Used by:
66
- * - buildWindowedIncludeSubquery to validate before using window functions
67
- * - rest-api-knex-plugin to determine query strategies
68
- *
69
- * Purpose:
70
- * - Prevents runtime errors by checking feature support
71
- * - Enables fallback strategies for older databases
72
- * - Provides clear error messages about version requirements
73
- *
74
- * Data flow:
75
- * 1. Identifies database client type from Knex config
76
- * 2. For version-dependent databases, queries version string
77
- * 3. Parses version and compares against known thresholds
78
- * 4. Returns boolean indicating support
79
- * 5. Falls back to false if version can't be determined
80
- *
81
- * Version requirements:
82
- * - PostgreSQL: 8.4+ (all modern versions)
83
- * - MySQL: 8.0+
84
- * - MariaDB: 10.2+
85
- * - SQLite: 3.25.0+
86
- * - SQL Server: 2005+ (all versions)
87
- * - Oracle: Supported (long-standing feature)
88
35
  */
89
- export const supportsWindowFunctions = async (knex) => {
90
- try {
91
- const client = knex.client.config.client
92
-
93
- let versionString
94
-
95
- switch (client) {
96
- case 'pg':
97
- case 'postgresql':
98
- // PostgreSQL 8.4+ supports window functions (all modern versions)
99
- return true
36
+ export const supportsWindowFunctions = async (knex, databaseInfo, log) => {
37
+ const client = knex.client.config.client
38
+ if (['pg', 'postgresql', 'mssql', 'oracledb'].includes(client)) return true
39
+ if (!['mysql', 'mysql2', 'sqlite3', 'better-sqlite3'].includes(client) && !client.includes('maria')) return false
40
+
41
+ const info = databaseInfo || await getDatabaseInfo(knex, log)
42
+ const parsed = typeof info.version === 'string' ? parseVersion(info.version) : null
43
+ if (!parsed) return false
44
+ if (['mysql', 'mysql2'].includes(client)) return parsed.major >= 8
45
+ if (client.includes('sqlite')) return parsed.major > 3 || (parsed.major === 3 && parsed.minor >= 25)
46
+ return info.version.toLowerCase().includes('mariadb') &&
47
+ (parsed.major > 10 || (parsed.major === 10 && parsed.minor >= 2))
48
+ }
100
49
 
101
- case 'mysql':
102
- case 'mysql2':
103
- try {
104
- const row = await knex.first(knex.raw('VERSION() as version'))
105
- const mysqlVersion = parseVersion(row.version)
50
+ // Built-in rules; custom field declarations still undergo their own validation.
51
+ /** @type {import('../storage/storage-types.js').SerializationCapabilities} */
52
+ export const SERIALIZATION_CAPABILITIES = Object.freeze({
53
+ customSerializerResult: 'synchronous',
54
+ structuredTypes: Object.freeze(['object', 'array']),
55
+ structuredEncoding: 'json',
56
+ wholeDocumentPredicates: false
57
+ })
58
+
59
+ /** @type {import('../storage/storage-types.js').RelationshipCapabilities} */
60
+ export const RELATIONSHIP_CAPABILITIES = Object.freeze({
61
+ attributeKinds: Object.freeze(['belongsTo', 'belongsToPolymorphic']),
62
+ declaredCardinalities: Object.freeze({ hasOne: 'one', hasMany: 'many', manyToMany: 'many' })
63
+ })
106
64
 
107
- return mysqlVersion && mysqlVersion.major >= 8
108
- } catch (dbError) {
109
- console.warn(`[supportsWindowFunctions] Failed to get MySQL version: ${dbError.message}`)
110
- return false
111
- }
65
+ /**
66
+ * Both storage plugins share one version observation during initialization.
67
+ * @param {import('../storage/storage-types.js').StorageDatabase} knex
68
+ * @param {import('../storage/storage-types.js').CapabilityLogger} [log]
69
+ * @returns {Promise<import('../storage/storage-types.js').DatabaseCapabilities>}
70
+ */
71
+ export const getDatabaseCapabilities = async (knex, log) => {
72
+ const dbInfo = await getDatabaseInfo(knex, log)
73
+ return {
74
+ dbInfo,
75
+ windowFunctions: await supportsWindowFunctions(knex, dbInfo),
76
+ insertResult: getInsertResultMode(knex.client.config.client),
77
+ serialization: SERIALIZATION_CAPABILITIES,
78
+ relationships: RELATIONSHIP_CAPABILITIES,
79
+ temporal: {
80
+ native: getTemporalStorageCapabilities(knex.client.config.client),
81
+ text: getTemporalStorageCapabilities(knex.client.config.client, { textStorage: true })
82
+ },
83
+ schema: getSchemaCapabilities(knex.client.config.client)
84
+ }
85
+ }
112
86
 
113
- case 'sqlite3':
114
- case 'better-sqlite3':
115
- try {
116
- versionString = await knex.raw('SELECT sqlite_version() as version')
117
- const sqliteVersion = versionString[0].version
118
- // SQLite 3.25.0+ supports window functions
119
- const parsed = parseVersion(sqliteVersion)
120
- return parsed && (parsed.major > 3 || (parsed.major === 3 && parsed.minor >= 25))
121
- } catch (dbError) {
122
- console.warn(`[supportsWindowFunctions] Failed to get SQLite version: ${dbError.message}`)
123
- return false
124
- }
87
+ /**
88
+ * @param {unknown} dialect
89
+ * @returns {import('../storage/storage-types.js').InsertResultMode}
90
+ */
91
+ export const getInsertResultMode = dialect => {
92
+ const client = String(dialect || '').toLowerCase()
93
+ if (['mysql', 'mysql2'].includes(client)) return 'insert-id'
94
+ if (['pg', 'postgresql', 'sqlite3', 'better-sqlite3'].includes(client)) return 'rows'
95
+ return 'driver-defined'
96
+ }
125
97
 
126
- case 'mssql':
127
- // SQL Server 2005+ supports window functions
128
- return true
98
+ /**
99
+ * Choose the query form; each caller must still validate its driver's result.
100
+ * @param {import('knex').Knex.QueryBuilder<import('../storage/storage-types.js').StorageRow, unknown>} query
101
+ * @param {string|string[]|Record<string, string|import('knex').Knex.Raw<unknown>>} columns
102
+ * @returns {import('knex').Knex.QueryBuilder<import('../storage/storage-types.js').StorageRow, unknown>}
103
+ */
104
+ export const applyInsertReturning = (query, columns) => {
105
+ if (getInsertResultMode(query.client.config.client) === 'insert-id') return query
106
+ // Knex supports alias dictionaries at runtime; its declarations omit that overload.
107
+ const returning = /** @type {(columns: string|string[]|Record<string, string|import('knex').Knex.Raw<unknown>>) => typeof query} */ (query.returning)
108
+ return returning.call(query, columns)
109
+ }
129
110
 
130
- case 'oracledb':
131
- // Oracle has supported window functions for a long time
132
- return true
111
+ /**
112
+ * Built-in value conversion limits; schemas/custom serializers may differ.
113
+ * @param {unknown} dialect
114
+ * @param {{textStorage?: boolean}} [options]
115
+ * @returns {import('../storage/storage-types.js').TemporalStorageCapabilities}
116
+ */
117
+ export const getTemporalStorageCapabilities = (dialect, { textStorage = false } = {}) => {
118
+ const client = String(dialect || '').toLowerCase()
119
+ const mysql = ['mysql', 'mysql2'].includes(client)
120
+ return {
121
+ dateMinYear: !textStorage && mysql ? 1000 : 0,
122
+ dateTimeMinYear: mysql ? 1000 : 0,
123
+ maxYear: 9999,
124
+ dateTimeFractionDigits: 3,
125
+ timeFractionDigits: !textStorage && (mysql || ['pg', 'postgresql'].includes(client)) ? 6 : null
126
+ }
127
+ }
133
128
 
134
- default:
135
- // For MariaDB, we need to check if it's actually MariaDB or MySQL
136
- if (client.includes('maria')) {
137
- try {
138
- versionString = await knex.raw('SELECT VERSION() as version')
139
- const version = versionString[0].version.toLowerCase()
140
- if (version.includes('mariadb')) {
141
- const mariaVersion = parseVersion(version)
142
- // MariaDB 10.2+ supports window functions
143
- return mariaVersion && (mariaVersion.major > 10 ||
144
- (mariaVersion.major === 10 && mariaVersion.minor >= 2))
145
- }
146
- } catch (dbError) {
147
- console.warn(`[supportsWindowFunctions] Failed to get MariaDB version: ${dbError.message}`)
148
- return false
149
- }
150
- }
151
- return false
152
- }
153
- } catch (error) {
154
- // Log error with context
155
- console.error('[supportsWindowFunctions] Unexpected error checking database capabilities:', {
156
- error: error.message,
157
- stack: error.stack,
158
- client: knex?.client?.config?.client || 'unknown'
159
- })
160
- // If we can't determine, assume no support for safety
161
- return false
129
+ /**
130
+ * Describe the existing field-alteration runner, not all database DDL.
131
+ * @param {unknown} dialect
132
+ * @returns {import('../storage/storage-types.js').SchemaCapabilities}
133
+ */
134
+ export const getSchemaCapabilities = dialect => {
135
+ const client = String(dialect || '').toLowerCase()
136
+ const mysql = /mysql/.test(client)
137
+ const sqlite = client.includes('sqlite')
138
+ const postgres = ['pg', 'postgresql'].includes(client)
139
+ return {
140
+ recognizedDialect: mysql || sqlite || postgres,
141
+ fieldAlteration: mysql ? 'standalone' : sqlite ? 'sqlite-rebuild' : 'transaction',
142
+ callerFieldAlteration: mysql ? 'forbidden' : sqlite ? 'foreign-keys-off' : 'savepoint',
143
+ temporalColumnFractionDigits: mysql || postgres ? 6 : null,
144
+ setValues: mysql
162
145
  }
163
146
  }
164
147
 
165
148
  /**
166
149
  * Gets database info for error messages and capability checks
167
150
  *
168
- * @param {Object} knex - Knex instance
169
- * @returns {Promise<Object>} Database info with client and version
170
- *
171
- * @example
172
- * // Input: MySQL connection
173
- * const info = await getDatabaseInfo(knex);
174
- * // Output: { client: 'MySQL', version: '8.0.33' }
175
- *
176
- * @example
177
- * // Input: PostgreSQL connection
178
- * const info = await getDatabaseInfo(knex);
179
- * // Output: {
180
- * // client: 'PostgreSQL',
181
- * // version: 'PostgreSQL 14.5 (Ubuntu 14.5-1.pgdg20.04+1)'
182
- * // }
183
- *
184
- * @example
185
- * // Input: Connection error case
186
- * const info = await getDatabaseInfo(knexWithError);
187
- * // Output: {
188
- * // client: 'MySQL',
189
- * // version: 'unknown',
190
- * // error: 'Connection refused'
191
- * // }
192
- *
193
- * @description
194
- * Used by:
195
- * - Error messages to show which database/version lacks a feature
196
- * - Capability detection for feature flags
197
- * - Debugging connection issues
198
- *
199
- * Purpose:
200
- * - Provides human-readable database identification
201
- * - Helps users understand version requirements
202
- * - Enables detailed error messages
203
- * - Gracefully handles query failures
204
- *
205
- * Data flow:
206
- * 1. Identifies client type from Knex config
207
- * 2. Runs version query specific to each database
208
- * 3. Returns formatted info object
209
- * 4. Includes error details if version query fails
210
- * 5. Falls back to 'unknown' for unsupported databases
151
+ * @param {import('../storage/storage-types.js').StorageDatabase} knex - Knex instance
152
+ * @param {import('../storage/storage-types.js').CapabilityLogger} [log] - Configured diagnostic sink
153
+ * @returns {Promise<import('../storage/storage-types.js').DatabaseInfo>} Database info with client and version
211
154
  */
212
- export const getDatabaseInfo = async (knex) => {
155
+ export const getDatabaseInfo = async (knex, log) => {
156
+ const client = knex.client.config.client
157
+ let name = client
158
+ if (['mysql', 'mysql2'].includes(client)) name = 'MySQL'
159
+ else if (['sqlite3', 'better-sqlite3'].includes(client)) name = 'SQLite'
160
+ else if (['pg', 'postgresql'].includes(client)) name = 'PostgreSQL'
213
161
  try {
214
- const client = knex.client.config.client
215
-
216
- let versionString
217
- switch (client) {
218
- case 'mysql':
219
- case 'mysql2':
220
- try {
221
- const row = await knex.first(knex.raw('VERSION() as version'))
222
- return { client: 'MySQL', version: row.version }
223
- } catch (dbError) {
224
- console.warn(`[getDatabaseInfo] Failed to get MySQL version: ${dbError.message}`)
225
- return { client: 'MySQL', version: 'unknown', error: dbError.message }
226
- }
227
-
228
- case 'sqlite3':
229
- case 'better-sqlite3':
230
- try {
231
- versionString = await knex.raw('SELECT sqlite_version() as version')
232
- return { client: 'SQLite', version: versionString[0].version }
233
- } catch (dbError) {
234
- console.warn(`[getDatabaseInfo] Failed to get SQLite version: ${dbError.message}`)
235
- return { client: 'SQLite', version: 'unknown', error: dbError.message }
236
- }
237
-
238
- case 'pg':
239
- case 'postgresql':
240
- try {
241
- versionString = await knex.raw('SELECT version() as version')
242
- return { client: 'PostgreSQL', version: versionString.rows[0].version }
243
- } catch (dbError) {
244
- console.warn(`[getDatabaseInfo] Failed to get PostgreSQL version: ${dbError.message}`)
245
- return { client: 'PostgreSQL', version: 'unknown', error: dbError.message }
246
- }
247
-
162
+ /** @type {unknown} */
163
+ let version
164
+ switch (name) {
165
+ case 'MySQL': {
166
+ const row = await knex.first(knex.raw('VERSION() as version'))
167
+ version = row && 'version' in row ? row.version : undefined
168
+ break
169
+ }
170
+ case 'SQLite':
171
+ version = (await knex.raw('SELECT sqlite_version() as version'))?.[0]?.version
172
+ break
173
+ case 'PostgreSQL':
174
+ version = (await knex.raw('SELECT version() as version'))?.rows?.[0]?.version
175
+ break
248
176
  default:
249
- return { client, version: 'unknown' }
177
+ if (!client.includes('maria')) return { client: name, version: 'unknown' }
178
+ version = (await knex.raw('SELECT VERSION() as version'))?.[0]?.version
250
179
  }
180
+ if (typeof version !== 'string') throw new Error('Database version query did not return a string')
181
+ return { client: name, version }
251
182
  } catch (error) {
252
- // Log unexpected errors
253
- console.error('[getDatabaseInfo] Unexpected error getting database info:', {
254
- error: error.message,
255
- stack: error.stack,
256
- client: knex?.client?.config?.client || 'unknown'
257
- })
258
-
259
- return {
260
- client: knex?.client?.config?.client || 'unknown',
261
- version: 'unknown',
262
- error: error.message
263
- }
183
+ // Capability observation is advisory; a failing diagnostic must not replace it.
184
+ try {
185
+ await log?.warn('Database version observation failed', formatDiagnosticValue({
186
+ operation: 'database-capabilities', phase: 'version-observation', backend: client, error
187
+ }))
188
+ } catch {}
189
+ return { client: name, version: 'unknown', error: errorMessage(error) }
264
190
  }
265
191
  }