json-rest-api 1.0.29 → 2.0.0

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