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,21 +1,461 @@
1
- import { HookedApiError } from 'hooked-api'
1
+ // @ts-check
2
+ /** @import { OwnedTransaction, TransactionOutcome } from './transaction-types.js' */
3
+ /** @import { TransactionContext, TransactionState, TransactionFactory, TransactionCallback, CompletionHook, CleanupDiagnostic, WriteRequest } from './transaction-types.js' */
4
+ import { finishKnexTransactionConnection } from './knex-transaction.js'
5
+ import { RestApiError, RestApiValidationError, RestApiWriteError } from './rest-api-errors.js'
6
+ import { types } from 'node:util'
2
7
 
8
+ /** @type {WeakSet<OwnedTransaction>} */
9
+ const commitRollbacks = new WeakSet()
10
+ /** @type {WeakMap<OwnedTransaction, TransactionState>} */
11
+ const managedTransactions = new WeakMap()
12
+ /** @type {WeakMap<TransactionContext, TransactionState>} */
13
+ const activeContexts = new WeakMap()
14
+
15
+ /**
16
+ * @param {unknown} value
17
+ * @param {object} prototype
18
+ * @returns {boolean}
19
+ */
20
+ function hasPrototype (value, prototype) {
21
+ if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return false
22
+ /** @type {object | null} */
23
+ let current = value
24
+ try {
25
+ while (current !== null) {
26
+ // Ordinary prototype chains cannot cycle; proxies can invent their next link.
27
+ if (types.isProxy(current)) return false
28
+ current = Object.getPrototypeOf(current)
29
+ if (current === prototype) return true
30
+ }
31
+ } catch { /* Unreadable prototypes do not establish identity. */ }
32
+ return false
33
+ }
34
+
35
+ /** @param {unknown} error @returns {error is Error} */
36
+ export function isErrorObject (error) {
37
+ return types.isNativeError(error) || hasPrototype(error, Error.prototype)
38
+ }
39
+
40
+ /**
41
+ * @param {unknown} error
42
+ * @returns {error is RestApiError}
43
+ */
3
44
  export function isRestApiError (error) {
4
- return error instanceof HookedApiError
45
+ return hasPrototype(error, RestApiError.prototype)
46
+ }
47
+
48
+ /**
49
+ * @template {WriteRequest} Request
50
+ * @template Result
51
+ * @param {(request: Request) => Result | Promise<Result>} handler
52
+ * @returns {(request: Request) => Promise<Result>}
53
+ */
54
+ export function withWriteOutcome (handler) {
55
+ return async request => {
56
+ try {
57
+ assertContextAvailable(request.context)
58
+ } catch (error) {
59
+ throw wrapWriteError(error, { transaction: activeContexts.get(request.context)?.transaction })
60
+ }
61
+ Object.assign(request.context, {
62
+ error: undefined,
63
+ cleanupErrors: undefined,
64
+ transaction: undefined,
65
+ transactionOutcome: undefined,
66
+ transactionCommitted: false,
67
+ shouldCommit: false
68
+ })
69
+ try {
70
+ return await handler(request)
71
+ } catch (error) {
72
+ const context = request.context
73
+ const transaction = context.transaction || request.params?.transaction
74
+ const state = transaction ? managedTransactions.get(transaction) : undefined
75
+ const participant = state?.participants.find(operation => operation.context === context)
76
+ if (participant) participant.failed = true
77
+ const ownsTransaction = state ? state.owner === context : context.shouldCommit
78
+ if (ownsTransaction && state?.phase !== 'finished') {
79
+ await rollbackAfterError(error, context, context.transaction)
80
+ }
81
+ throw wrapWriteError(error, context, context.transaction || request.params?.transaction)
82
+ } finally {
83
+ endWriteOperation(request.context)
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * @template {{ context: TransactionContext }} Request
90
+ * @template Result
91
+ * @param {(request: Request) => Result} handler
92
+ * @returns {(request: Request) => Result}
93
+ */
94
+ export function withAvailableContext (handler) {
95
+ return request => {
96
+ assertContextAvailable(request.context)
97
+ return handler(request)
98
+ }
99
+ }
100
+
101
+ /**
102
+ * @param {TransactionContext} context
103
+ */
104
+ function assertContextAvailable (context) {
105
+ const state = activeContexts.get(context)
106
+ if (!state) return
107
+ const error = new RestApiValidationError('Context is still in use by a transaction; use a separate context for each operation', { fields: ['context'] })
108
+ failTransaction(state, error)
109
+ throw error
110
+ }
111
+
112
+ /**
113
+ * @param {TransactionState | undefined} state
114
+ * @param {unknown} error
115
+ */
116
+ function failTransaction (state, error) {
117
+ if (!state || state.phase !== 'pending' || state.failed) return
118
+ state.failed = true
119
+ state.error = error
120
+ }
121
+
122
+ /**
123
+ * @param {OwnedTransaction | undefined} transaction
124
+ */
125
+ function transactionState (transaction) {
126
+ for (let current = transaction; current; current = current.parentTransaction) {
127
+ const state = managedTransactions.get(current)
128
+ if (state) return state
129
+ }
130
+ }
131
+
132
+ /**
133
+ * @param {TransactionContext} context
134
+ * @param {OwnedTransaction | null | undefined} transaction
135
+ * @param {TransactionFactory | undefined} createTransaction
136
+ * @param {CompletionHook} [runHooks]
137
+ */
138
+ export async function beginWriteTransaction (context, transaction, createTransaction, runHooks) {
139
+ /** @type {TransactionState | undefined} */
140
+ let state
141
+ if (transaction) {
142
+ state = managedTransactions.get(transaction)
143
+ if (!state || state.phase !== 'pending' || transaction.isCompleted()) {
144
+ throw new RestApiValidationError('transaction must be an active transaction from api.transaction; raw transactions and savepoints cannot own library writes', { fields: ['transaction'] })
145
+ }
146
+ if (state.failed) throw state.error
147
+ context.transaction = transaction
148
+ context.shouldCommit = false
149
+ } else {
150
+ if (typeof createTransaction !== 'function') throw new RestApiValidationError('No transaction factory is installed')
151
+ transaction = context.transaction = await createTransaction(context)
152
+ if (managedTransactions.has(transaction)) {
153
+ throw new RestApiValidationError('A transaction factory must create a new transaction, not return an existing owner', { fields: ['transaction'] })
154
+ }
155
+ context.shouldCommit = !!transaction
156
+ if (!transaction || transaction.parentTransaction) {
157
+ throw new RestApiValidationError('Owned transactions must be top-level; use api.transaction to compose library writes', { fields: ['transaction'] })
158
+ }
159
+ state = {
160
+ transaction,
161
+ owner: context,
162
+ phase: 'pending',
163
+ failed: false,
164
+ participants: [],
165
+ contexts: new Set(),
166
+ finalizers: [],
167
+ inFlight: new Map()
168
+ }
169
+ managedTransactions.set(transaction, state)
170
+ state.onQueryError = error => failTransaction(state, error)
171
+ transaction.on('query-error', state.onQueryError)
172
+ }
173
+ state.contexts.add(context)
174
+ state.inFlight.set(context, Promise.withResolvers())
175
+ activeContexts.set(context, state)
176
+ if (runHooks) state.participants.push({ context, runHooks, operationIndex: state.participants.length })
177
+ return transaction
178
+ }
179
+
180
+ /**
181
+ * @param {TransactionContext} context
182
+ */
183
+ export function endWriteOperation (context) {
184
+ const state = context.transaction ? managedTransactions.get(context.transaction) : undefined
185
+ state?.inFlight.get(context)?.resolve()
186
+ state?.inFlight.delete(context)
187
+ }
188
+
189
+ /**
190
+ * @param {TransactionContext} context
191
+ */
192
+ export function isTransactionOwner (context) {
193
+ return activeContexts.get(context)?.owner === context
194
+ }
195
+
196
+ /**
197
+ * @param {TransactionState | undefined} state
198
+ * @param {TransactionContext} context
199
+ */
200
+ async function awaitOtherOperations (state, context) {
201
+ if (!state) return
202
+ const pending = []
203
+ for (const [operation, completion] of state.inFlight) {
204
+ if (operation !== context) pending.push(completion.promise)
205
+ }
206
+ await Promise.all(pending)
207
+ }
208
+
209
+ /**
210
+ * @template Result
211
+ * @param {{ params: TransactionCallback<Result>, context: TransactionContext, helpers: { newTransaction?: TransactionFactory } }} request
212
+ */
213
+ export async function transactionMethod ({ params: callback, context, helpers }) {
214
+ if (typeof callback !== 'function') throw new RestApiValidationError('transaction requires a callback', { fields: ['callback'] })
215
+ const transaction = await beginWriteTransaction(context, null, helpers.newTransaction)
216
+ const value = await callback(transaction)
217
+ await commitTransaction(transaction, context)
218
+ return value
219
+ }
220
+
221
+ /**
222
+ * @param {OwnedTransaction} transaction
223
+ * @param {() => unknown | Promise<unknown>} callback
224
+ */
225
+ export function onTransactionFinished (transaction, callback) {
226
+ const state = managedTransactions.get(transaction)
227
+ if (state && state.phase !== 'finished') state.finalizers.push(callback)
228
+ }
229
+
230
+ /**
231
+ * @param {TransactionState | undefined} state
232
+ * @param {TransactionContext} context
233
+ */
234
+ async function finishTransaction (state, context) {
235
+ if (state?.phase === 'finished') return
236
+ if (!state) {
237
+ try {
238
+ await finishKnexTransactionConnection(context.transaction, context.transactionOutcome, context.error)
239
+ } catch (error) {
240
+ (context.cleanupErrors ||= []).push({ phase: 'connectionRelease', error })
241
+ }
242
+ return
243
+ }
244
+ state.phase = 'finishing'
245
+ const outcome = context.transactionOutcome
246
+ /** @type {'afterCommit' | 'afterRollback' | null} */
247
+ let hook = null
248
+ if (outcome === 'committed') {
249
+ hook = 'afterCommit'
250
+ } else if (outcome === 'rolledBack') {
251
+ hook = 'afterRollback'
252
+ }
253
+ let failed = false
254
+ let primary
255
+ /** @param {unknown} error
256
+ * @param {Omit<CleanupDiagnostic, "error">} details
257
+ */
258
+ const retainFailure = (error, details) => {
259
+ if (outcome === 'committed' && !failed) {
260
+ failed = true
261
+ primary = error
262
+ } else {
263
+ (context.cleanupErrors ||= []).push({ ...details, error })
264
+ }
265
+ }
266
+ try {
267
+ try {
268
+ await finishKnexTransactionConnection(state.transaction, outcome, context.error)
269
+ } catch (error) {
270
+ retainFailure(error, { phase: 'connectionRelease' })
271
+ }
272
+ for (const operation of state.contexts) {
273
+ operation.transactionOutcome = outcome || 'unknown'
274
+ operation.transactionCommitted = outcome === 'committed'
275
+ }
276
+ const participants = outcome === 'rolledBack' ? [...state.participants].reverse() : state.participants
277
+ for (const { context: operation, runHooks, operationIndex, failed: operationFailed } of participants) {
278
+ if (!hook) continue
279
+ if (hook === 'afterRollback' && !operationFailed) operation.error = context.error
280
+ try {
281
+ await runHooks(hook)
282
+ } catch (error) {
283
+ if (hook === 'afterCommit') operation.error = error
284
+ retainFailure(error, { phase: hook, operationIndex, scopeName: operation.scopeName, method: operation.method })
285
+ }
286
+ }
287
+ for (const [finalizerIndex, finalize] of state.finalizers.entries()) {
288
+ try {
289
+ await finalize()
290
+ } catch (error) {
291
+ retainFailure(error, { phase: 'finalization', finalizerIndex })
292
+ }
293
+ }
294
+ } finally {
295
+ state.phase = 'finished'
296
+ if (state.onQueryError) state.transaction.off('query-error', state.onQueryError)
297
+ for (const operation of state.contexts) activeContexts.delete(operation)
298
+ state.contexts.clear()
299
+ state.participants.length = 0
300
+ state.finalizers.length = 0
301
+ state.inFlight.clear()
302
+ }
303
+ if (failed) throw primary
304
+ }
305
+
306
+ /**
307
+ * @param {unknown} error
308
+ * @param {TransactionContext} context
309
+ * @param {OwnedTransaction} [transaction]
310
+ */
311
+ export function wrapWriteError (error, context, transaction = context.transaction) {
312
+ context.error = error
313
+ const wrapped = new RestApiWriteError(errorMessage(error), { cause: error, transactionOutcome: getWriteOutcome(context) })
314
+ failTransaction(transactionState(transaction), wrapped)
315
+ return wrapped
316
+ }
317
+
318
+ /**
319
+ * @param {TransactionContext} context
320
+ * @returns {TransactionOutcome}
321
+ */
322
+ export function getWriteOutcome (context) {
323
+ if (context.transactionOutcome) return context.transactionOutcome
324
+ if (!context.transaction) return 'none'
325
+ if (context.transaction.isCompleted?.()) return 'unknown'
326
+ return 'pending'
327
+ }
328
+
329
+ /**
330
+ * @param {TransactionContext & { db?: { client?: { config?: { client?: unknown } } } }} context
331
+ * @param {{ phase: string, method?: string, scopeName?: string, backend?: unknown }} options
332
+ */
333
+ export function getOperationDiagnosticContext (context, {
334
+ phase,
335
+ method = context.method,
336
+ scopeName = context.scopeName,
337
+ backend = context.db?.client?.config?.client
338
+ }) {
339
+ return {
340
+ method: method ?? null,
341
+ scopeName: scopeName ?? null,
342
+ phase,
343
+ backend: typeof backend === 'string' ? backend : null,
344
+ transactionOutcome: getWriteOutcome(context)
345
+ }
346
+ }
347
+
348
+ /**
349
+ * @param {OwnedTransaction} transaction
350
+ * @param {TransactionContext} context
351
+ */
352
+ export async function commitTransaction (transaction, context) {
353
+ if (transaction.parentTransaction) {
354
+ throw new RestApiValidationError('Owned transactions must be top-level; use api.transaction to compose library writes', { fields: ['transaction'] })
355
+ }
356
+ const state = managedTransactions.get(transaction)
357
+ if (state && state.owner !== context) throw new RestApiValidationError('Only the transaction owner can complete it', { fields: ['transaction'] })
358
+ if (state?.failed) throw state.error
359
+ if (state && [...state.inFlight.keys()].some(operation => operation !== context)) {
360
+ state.phase = 'completing'
361
+ await awaitOtherOperations(state, context)
362
+ throw new RestApiValidationError('Await every library operation before returning from the transaction callback', { fields: ['callback'] })
363
+ }
364
+ if (transaction.isCompleted()) throw new RestApiValidationError('Transaction was completed outside its owner', { fields: ['transaction'] })
365
+ if (state) state.phase = 'completing'
366
+ context.transactionOutcome = 'unknown'
367
+ const result = /** @type {unknown} */ (await transaction.commit())
368
+ const response = result && typeof result === 'object' && 'response' in result ? result.response : undefined
369
+ const rolledBack = !!response && typeof response === 'object' && 'command' in response && response.command === 'ROLLBACK'
370
+ if (rolledBack) {
371
+ commitRollbacks.add(transaction)
372
+ context.transactionOutcome = 'rolledBack'
373
+ }
374
+ await transaction.executionPromise
375
+ if (rolledBack) throw new Error('Transaction was rolled back instead of committed')
376
+ context.transactionOutcome = 'committed'
377
+ context.transactionCommitted = true
378
+ await finishTransaction(state, context)
379
+ }
380
+
381
+ /**
382
+ * @param {unknown} error
383
+ * @param {TransactionContext} context
384
+ * @param {OwnedTransaction | undefined} transaction
385
+ */
386
+ export async function rollbackAfterError (error, context, transaction) {
387
+ context.error = error
388
+ context.cleanupErrors ||= []
389
+ if (!transaction) return false
390
+ const state = managedTransactions.get(transaction)
391
+ if (state && state.owner !== context) return false
392
+ if (state?.phase === 'finished') return false
393
+ const participant = state?.participants.find(operation => operation.context === context)
394
+ if (participant) participant.failed = true
395
+ if (state) state.phase = 'completing'
396
+ await awaitOtherOperations(state, context)
397
+
398
+ try {
399
+ // PostgreSQL can acknowledge COMMIT with ROLLBACK after a caught SQL error.
400
+ if (commitRollbacks.delete(transaction)) {
401
+ context.transactionOutcome = 'rolledBack'
402
+ await finishTransaction(state, context)
403
+ return true
404
+ }
405
+ if (transaction.isCompleted?.()) {
406
+ await finishTransaction(state, context)
407
+ return false
408
+ }
409
+ context.transactionOutcome = 'unknown'
410
+ await transaction.rollback()
411
+ await transaction.executionPromise
412
+ context.transactionOutcome = 'rolledBack'
413
+ await finishTransaction(state, context)
414
+ return true
415
+ } catch (error) {
416
+ context.cleanupErrors.push({ phase: 'rollback', error })
417
+ await finishTransaction(state, context)
418
+ return false
419
+ }
5
420
  }
6
421
 
7
422
  /**
8
423
  * Add operational context to unexpected failures without erasing typed API
9
424
  * errors that callers and transports need to classify.
425
+ *
426
+ * @param {unknown} error
427
+ * @param {{ message?: string, context?: unknown }} [options]
10
428
  */
11
429
  export function wrapUnexpectedError (error, { message, context } = {}) {
12
430
  if (isRestApiError(error)) {
13
431
  return error
14
432
  }
15
433
 
16
- const wrappedError = new Error(`${message}: ${error.message}`, {
434
+ const wrappedError = new Error(`${message}: ${errorMessage(error)}`, {
17
435
  cause: error
18
436
  })
19
- wrappedError.context = context
20
- return wrappedError
437
+ return Object.assign(wrappedError, { context })
438
+ }
439
+
440
+ /**
441
+ * @param {unknown} error
442
+ */
443
+ export function errorMessage (error) {
444
+ try {
445
+ if (getBinaryDiagnostic(error)) return 'Binary value thrown'
446
+ const message = /** @type {unknown} */ (Reflect.get(Object(error), 'message'))
447
+ return typeof message === 'string' ? message : String(error)
448
+ } catch {
449
+ return 'Non-Error value thrown'
450
+ }
451
+ }
452
+
453
+ /** @param {unknown} value */
454
+ export function getBinaryDiagnostic (value) {
455
+ if (ArrayBuffer.isView(value)) {
456
+ const type = hasPrototype(value, Buffer.prototype) ? 'Buffer' : 'ArrayBufferView'
457
+ return { type, byteLength: value.byteLength }
458
+ }
459
+ if (types.isSharedArrayBuffer(value)) return { type: 'SharedArrayBuffer', byteLength: value.byteLength }
460
+ if (types.isAnyArrayBuffer(value)) return { type: 'ArrayBuffer', byteLength: value.byteLength }
21
461
  }