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,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
  }