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
@@ -0,0 +1,86 @@
1
+ ---
2
+ title: "Labels"
3
+ chapter: 18
4
+ chapter_label: "18"
5
+ ---
6
+
7
+ # 18. Labels
8
+
9
+ Install `LabelPlugin` after `RestApiPlugin` and before adding resources:
10
+
11
+ ```js
12
+ import { LabelPlugin } from 'json-rest-api'
13
+
14
+ await api.use(LabelPlugin, { preferNameFields: ['name', 'title'] })
15
+ ```
16
+
17
+ It adds a computed string `label` to resources that do not already declare one.
18
+ The label is derived during reads and is not a stored column. GET, query, full
19
+ write responses and included resources use the same computation in both plain
20
+ and JSON:API formats.
21
+
22
+ Candidates are derived during schema compilation, after attribute and search
23
+ enrichment. Canonical `addKnexFields` rebuilds them from the new configuration,
24
+ so a field addition retains the label and an added preferred/global-search
25
+ source becomes eligible. Reads and descriptor-only refreshes reuse the compiled
26
+ candidates. Ordinary Knex field-addition helpers perform DDL only; changing
27
+ their runtime declarations still requires resource initialization.
28
+
29
+ The plugin considers, in order:
30
+
31
+ 1. The first global-search field that resolves to a public stored field on this
32
+ resource. A search alias uses its `actualField`; a joined search path is not
33
+ a local label source.
34
+ 2. String fields listed in `preferNameFields`, in the configured order.
35
+ 3. The first public stored string field in the schema.
36
+ 4. The resource's logical ID.
37
+
38
+ Null and undefined values fall through to the next candidate. Empty strings,
39
+ zero and false are retained and converted to strings. Getters run before label
40
+ computation. Hidden, normally hidden and virtual fields, belongs-to foreign keys
41
+ and polymorphic backing fields are not automatic label sources. This keeps the default label from exposing private values or depending
42
+ on a transient write input.
43
+
44
+ Sparse fieldsets can select just the label:
45
+
46
+ ```js
47
+ await api.resources.books.query({
48
+ format: 'jsonapi',
49
+ queryParams: { fields: { books: 'label' } }
50
+ })
51
+ ```
52
+
53
+ The existing computed-field dependency mechanism fetches the candidate values
54
+ and removes unrequested dependencies from the result. Null fallbacks therefore
55
+ work with sparse fieldsets too. Pass `{ disable: true }` when installing the
56
+ plugin to disable automatic labels.
57
+
58
+ ## Authored labels and record identity
59
+
60
+ An explicitly declared stored or computed `label` remains authoritative. The
61
+ plugin does not remove its schema definition or replace its computation. For
62
+ example, a resource may declare:
63
+
64
+ ```js
65
+ label: {
66
+ type: 'string',
67
+ computed: true,
68
+ dependencies: ['title'],
69
+ compute: ({ id, attributes }) => `${id}: ${attributes.title ?? ''}`
70
+ }
71
+ ```
72
+
73
+ Computed callbacks receive the current resource's public logical `id` separately
74
+ from `attributes`. This also works for custom `idProperty` names, collections
75
+ and included resources; the ID is not taken from the parent request. Direct
76
+ callers of `enrichAttributes` can supply its `id` argument explicitly.
77
+
78
+ Computed fields honor `hidden` and `normallyHidden` like stored fields: hidden
79
+ fields are omitted, and normally hidden fields appear only when requested.
80
+ Authors who intentionally derive a public value from normally hidden dependencies should
81
+ declare that computed field explicitly and review the information it returns.
82
+
83
+ The shared label suite (source checkout: `tests/conformance-labels.test.js`) checks these
84
+ contracts on regular and canonical storage. The
85
+ [migration guide](33-migrating-to-v2.md#generated-labels-and-computed-visibility)
86
+ describes corrections from the earlier behavior.
@@ -0,0 +1,179 @@
1
+ ---
2
+ title: "Managed transactions"
3
+ chapter: 19
4
+ chapter_label: "19"
5
+ ---
6
+
7
+ # 19. Managed transactions
8
+
9
+ Use `api.transaction()` to group resource operations and raw SQL in one explicit
10
+ unit. The helper owns commit, rollback and completion hooks. Pass its transaction
11
+ to every participating operation; the library does not infer ambient ownership.
12
+
13
+ ## Callback and result
14
+
15
+ ```js
16
+ const result = await api.transaction(async transaction => {
17
+ const book = await api.resources.books.post({
18
+ data: { title: 'New book' },
19
+ format: 'plain',
20
+ transaction
21
+ })
22
+ await transaction('audit_entries').insert({ book_id: book.id })
23
+ return book
24
+ })
25
+ ```
26
+
27
+ `api.transaction(callback, context = {})` acquires one top-level transaction,
28
+ awaits the callback, completes the transaction, awaits its completion work and
29
+ returns the callback's value unchanged. The second argument is mutable diagnostic
30
+ context, consistent with other API methods; it is not a second options parser.
31
+ Invalid callbacks fail before acquisition. The callback receives the actual
32
+ Knex transaction, so existing first-argument `transaction` forwarding and raw SQL
33
+ remain straightforward. No transaction is inferred from process-global or async
34
+ ambient state. Callers must await all work before the callback returns.
35
+ Individual resource calls inside the callback resolve while the unit is still
36
+ pending; only the outer helper waits for commit and completion hooks. Both
37
+ synchronous callback values and awaited promises are supported, including
38
+ undefined. The helper does not clone or serialize the callback result.
39
+
40
+ The helper owns completion. The callback must not invoke commit/rollback,
41
+ transaction-control SQL or DDL that implicitly completes its transaction. A
42
+ handle completed outside the helper is rejected with an unknown outcome unless
43
+ the library has actual completion evidence; it must not trigger speculative
44
+ rollback cleanup or commit notifications. SQL/connection error evidence also
45
+ cannot be replaced by a later successful empty COMMIT.
46
+
47
+ ## Participation and failure
48
+
49
+ Ordinary resource and relationship calls without a transaction use the same
50
+ completion mechanism for their own transaction. Atomic bulk uses it for the
51
+ whole batch. Calls receiving its transaction participate and never complete it
52
+ individually. Non-atomic bulk with an outer transaction continues to reject.
53
+
54
+ The final contract requires library writes to use a library-owned transaction.
55
+ Standalone transactions created with `knex.transaction()` remain usable for raw
56
+ SQL, but passing them into library writes will reject before partial work.
57
+ Migrate those units to `api.transaction`; its callback still receives a real
58
+ Knex handle. This replaces the previous ambiguous borrowed-write mode in which
59
+ notifications/uploads can wait forever for an unobserved owner. Read-only/raw
60
+ storage use is not a competing event-delivery mode.
61
+
62
+ Canonical `addKnexFields()` batches use this helper too. Direct registry writes
63
+ that receive a transaction require a managed handle. Their caller still
64
+ invalidates the registry descriptor cache after confirmed commit; this does not
65
+ publish a new compiled resource schema. Raw descriptor reads and schema/data
66
+ migration helpers retain their separate documented transaction contracts.
67
+
68
+ Any rejected participating write marks the unit failed, even if the callback
69
+ catches the error and returns normally. Observed SQL failures also prevent a
70
+ later commit from masking aborted work. A rejected callback or failed unit rolls
71
+ back; no automatic retries or savepoints restore it. A failure before acquisition
72
+ has outcome none. Participant errors retain their pending/none snapshot, and an
73
+ outer error records the owner's eventual outcome without rewriting its cause.
74
+ If the callback rejects, its rejection is primary. If it returns after catching
75
+ failures, the first recorded participant/SQL failure is primary instead.
76
+
77
+ A native lock timeout, deadlock or stale SQLite snapshot is reported to the
78
+ caller. The library does not replay the callback, field setters or hooks.
79
+ Effects performed outside the database before the conflict can therefore remain
80
+ once even though the database write rolls back. Retry only a new complete unit
81
+ whose external effects your application can safely repeat or reconcile; never
82
+ retry just the failed statement inside an already failed unit.
83
+
84
+ ## Completion work
85
+
86
+ Each accepted resource/relationship operation enlists its existing completion
87
+ hook chain once. This includes nested resource calls made by relationship
88
+ implementation; helper-internal SQL does not create synthetic operations. Bulk
89
+ or transaction owners do not also run a duplicate resource hook chain.
90
+
91
+ After confirmed commit, invoke enlisted `afterCommit` chains in enlistment
92
+ order. After confirmed rollback, invoke `afterRollback` chains in reverse order.
93
+ A chain retains its normal local stop-on-error behavior; failure in one chain
94
+ does not prevent other enlisted operations' completion work from being attempted.
95
+ Collect secondary failures in owner diagnostics. Retain the original callback/
96
+ write failure on rollback; after commit, report the first completion failure as
97
+ the primary error with outcome committed. Unconfirmed completion runs neither
98
+ set of outcome-dependent hooks.
99
+
100
+ Enlistment happens when the library accepts an operation into the transaction.
101
+ Overlapping operations can finish their writes in another order; that does not
102
+ change completion-hook order. Internal finalizers, currently bulk diagnostic
103
+ collection, run after those chains in registration order even when a chain or
104
+ an earlier finalizer fails. They also collect diagnostics after unknown
105
+ completion; they must not infer commit or run outcome-dependent side effects.
106
+ The first post-commit failure remains primary across hooks and finalizers.
107
+
108
+ Use the existing diagnostic shape on the owner's context: `error` is the primary
109
+ failure and `cleanupErrors` is an ordered array of `{ phase, error }` entries.
110
+ Completion-chain entries additionally identify `operationIndex`, `scopeName`
111
+ and `method`; phases distinguish `rollback`, `afterRollback` and `afterCommit`.
112
+ Secondary finalizer failures use phase `finalization` and `finalizerIndex`.
113
+ The error carrier retains its cause even when any of these values are frozen or
114
+ non-Error throws. Do not serialize transaction handles or raw cleanup objects
115
+ into HTTP errors.
116
+
117
+ An operation's mutable context remains in use until its owner completes. Use a
118
+ separate context object per operation; reject reuse through library calls while
119
+ that context is still enlisted. Do not copy arbitrary contexts, replay mutations
120
+ into reused objects, or rebuild scope-aware hook dispatch in the transaction
121
+ helper. Completion hooks receive their operation's context, with the observed
122
+ owner outcome added at completion. Previously thrown errors remain immutable
123
+ snapshots. Application code must not mutate an enlisted context concurrently.
124
+ The private owner record controls commit and rollback. Changing the diagnostic
125
+ `shouldCommit` flag cannot transfer ownership to a participant or stop an
126
+ implicit owner from completing. During rollback, a failed operation retains its
127
+ original error; an operation that succeeded before the unit failed receives the
128
+ owner's error in its rollback context.
129
+
130
+ File cleanup/tracking and Socket.IO delivery use these existing hooks. Uploaded
131
+ files are cleaned only after confirmed rollback, and notifications run only
132
+ after confirmed outer commit. Bulk diagnostics aggregate unresolved child state
133
+ after finalization; the parent must not delete a child's files a second time.
134
+ Tests cover repeated changes to the same resource, multiple uploads,
135
+ callback/SQL failures and failing completion hooks. File-specific failures and
136
+ unresolved upload tracking remain on each operation's context; rejected hook
137
+ chains are reported on the owner. After confirmed commit, stored objects are
138
+ application-owned. Replacement and deletion preserve previously committed files:
139
+ opaque URLs may be shared by other records or external consumers. Rollback
140
+ removes only new uploads belonging to the failed unit. See the
141
+ [file ownership contract](26-file-uploads.md#ownership-after-commit)
142
+ for application-managed garbage collection.
143
+
144
+ Socket.IO notices are per-operation invalidations, without coalescing: a
145
+ sequential create/update/delete can emit all three after commit. Callback
146
+ rollback discards the queue. The selected tests check pending silence, ordered
147
+ notices, real committed/rolled-back rows, visibility filtering and a later
148
+ chain flushing once after the first chain fails. These are awaited in-memory
149
+ notifications, not durable delivery or an exactly-once guarantee.
150
+
151
+ Notification capture order is the order in which operations reach the Socket.IO
152
+ finish hook's queue. For overlapping writes it can differ from enlistment order.
153
+ The first successful delivery chain drains that queue without reordering it.
154
+ Real polling/WebSocket tests deliberately pause the first enlisted write so the
155
+ second finishes first, and verify both distinct orders.
156
+
157
+ ## Nesting and application ownership
158
+
159
+ An application-created Knex transaction is unmanaged from this library's point
160
+ of view: the application decides when it finally commits or rolls back. That
161
+ final outcome controls after-commit hooks and rollback cleanup, so passing an
162
+ arbitrary Knex transaction into resource writes is rejected. Use
163
+ `api.transaction` to group resource operations and raw SQL under one owner.
164
+
165
+ A savepoint is a rollback checkpoint inside an outer transaction. Releasing it
166
+ does not commit that outer transaction. Resource writes therefore cannot treat
167
+ savepoint success as permission to send notifications or run after-commit hooks.
168
+
169
+ Every `api.transaction` call starts an independent top-level unit. There is no
170
+ implicit joining or nested callback overload. To compose work within one unit,
171
+ pass its transaction to ordinary functions and resource methods. Managed library
172
+ writes do not support child savepoints; reject them explicitly instead of
173
+ treating savepoint release as outer commit. The current owned-completion guard
174
+ already rejects a custom factory's savepoint; its caller-owned parent survives.
175
+
176
+ When migrating an application's transaction wrapper, delegate to `api.transaction`
177
+ and forward the received handle. Port raw transaction creation, context reuse and
178
+ error handling together. Preserve domain scope resolution in application code;
179
+ keep completion ownership in one place.
@@ -0,0 +1,207 @@
1
+ ---
2
+ title: "Transaction outcomes"
3
+ chapter: 20
4
+ chapter_label: "20"
5
+ ---
6
+
7
+ # 20. Transaction outcomes
8
+
9
+ Resource, relationship, bulk and canonical registry write errors carry the
10
+ outcome of the transaction they represent. HTTP and non-atomic bulk errors
11
+ preserve that outcome. The states below describe the evidence available when
12
+ the failure is reported.
13
+
14
+ [Managed transactions](19-managed-transactions.md) explains ownership and
15
+ completion ordering. These guarantees cannot recover an error swallowed by
16
+ an application extension.
17
+
18
+ ## The five states
19
+
20
+ | State | Exact meaning |
21
+ | --- | --- |
22
+ | `none` | No transaction was acquired or accepted for this operation. This includes rejection before transaction setup. It does not certify that external or nontransactional work had no effects. |
23
+ | `pending` | The operation participated in a transaction whose owner has not completed it. The owner still must decide its outcome. A database error may have made further SQL unusable; pending does not mean the owner can safely continue writing. |
24
+ | `committed` | The backend confirmed commit of the represented outer transaction. Later hook, file, notification or logging failure does not change this outcome. Releasing a savepoint does not establish outer commit. |
25
+ | `rolledBack` | The backend confirmed rollback of the represented transaction. Later cleanup failure does not undo that confirmation. This says nothing about external effects performed before rollback. |
26
+ | `unknown` | A transaction was acquired or accepted, but the available evidence cannot establish its outcome or leave it confidently pending. Examples include lost completion acknowledgements, failed completion/rollback, and an accepted handle completed outside its owner without observed outcome. |
27
+
28
+ Use these values consistently. Do not add
29
+ synonyms for driver states or infer them from exception messages. Ownership is
30
+ separate from outcome: an individual resource operation borrows its caller's
31
+ transaction, including a transaction managed by an outer library operation.
32
+
33
+ ## Evidence and transitions
34
+
35
+ Transaction setup starts with `none`. Successful acquisition or acceptance of
36
+ an unfinished transaction moves to `pending`. An operation that merely borrows
37
+ that transaction leaves it pending; its own success or rejection does not commit
38
+ or roll back the outer unit. A rejected statement may require the owner to roll
39
+ back even if the driver's handle remains usable-looking.
40
+
41
+ The private transaction owner controls completion; a hook changing the mutable
42
+ `shouldCommit` diagnostic flag neither takes ownership nor suppresses an implicit
43
+ owner's commit or rollback. Participating operation errors stay pending snapshots
44
+ even when the outer helper later reports rolledBack.
45
+
46
+ Confirmed commit or rollback establishes `committed` or `rolledBack`. A failed
47
+ completion attempt without confirmation establishes `unknown`. If a subsequent
48
+ rollback is actually confirmed, the reported outcome can be `rolledBack`; a
49
+ cleanup attempt or a resolved wrapper alone is insufficient. Once commit is
50
+ confirmed, later failure remains committed and must not trigger rollback.
51
+
52
+ Knex 3.1.0 exposes a specific trap: `commit()` and `rollback()` can resolve even
53
+ when their control query failed, while `executionPromise` rejects. Its
54
+ `isCompleted()` flag is set when completion starts and after failures; the flag
55
+ alone proves neither commit nor rollback. The current shared completion helpers
56
+ now await `executionPromise` before reporting successful completion to hooks or
57
+ the registry. Driver-path regressions cover rejected SQL and injected lost
58
+ acknowledgements after real execution.
59
+
60
+ The shared helper also rejects a PostgreSQL COMMIT response reporting ROLLBACK.
61
+ Both Knex promises can resolve in this case, but the backend has confirmed that
62
+ the writes were rolled back. The existing failure path consumes that confirmation
63
+ once, without sending another ROLLBACK. Resource rollback hooks can then clean
64
+ uploaded files; commit hooks and registry cache publication do not run.
65
+
66
+ Owned completion now requires a top-level transaction. A custom transaction
67
+ factory returning a savepoint is rejected before release; the existing failure
68
+ path rolls back that savepoint and leaves its caller-owned parent active.
69
+ No commit hook or committed registry cache publication follows that rejection.
70
+ The managed implementation rejects raw transactions and savepoints supplied to
71
+ library writes before acceptance, with outcome none. A managed handle completed
72
+ outside its owner after acceptance instead has outcome unknown. Connection
73
+ failure during commit must remain unknown unless available evidence establishes
74
+ the outcome. The error remains a snapshot even if the application later
75
+ reconciles the database state.
76
+
77
+ ## When commit acknowledgement is lost
78
+
79
+ A connection can fail before COMMIT reaches the database or after the database
80
+ commits but before the caller receives confirmation. A rejected promise or HTTP
81
+ 500 alone cannot distinguish those cases. `unknown` deliberately preserves that
82
+ uncertainty; it is not a synonym for rolledBack.
83
+
84
+ | Observed evidence | Reported outcome and behavior |
85
+ | --- | --- |
86
+ | Commit and driver completion acknowledged, then an after-commit hook fails | `committed`; retain the failure, never attempt rollback of the committed write. |
87
+ | Commit control statement or its completion promise rejects without acknowledged outcome | `unknown`; no speculative commit/rollback hooks. A driver's completed flag alone does not establish success. |
88
+ | PostgreSQL acknowledges COMMIT as ROLLBACK | `rolledBack`; run rollback completion work without issuing a second rollback. |
89
+ | Rollback is acknowledged, then cleanup fails | `rolledBack`; retain the primary write error and secondary cleanup diagnostics. |
90
+ | Rollback itself cannot be acknowledged | `unknown`; keep the original failure and rollback diagnostic. |
91
+
92
+ The Knex storage plugins and owned AnyAPI registry operations retain the
93
+ physical connection lease until the
94
+ transaction completion promise settles. A failed completion marks that connection
95
+ disposed before releasing it, so even a request already waiting for the pool
96
+ cannot borrow its unfinished transaction. A failed BEGIN also rejects before the
97
+ application callback runs. Acknowledged completion keeps normal connection reuse.
98
+
99
+ Owned PostgreSQL `alterKnexFields` also uses the managed completion runner.
100
+ A deep-helper call given an existing transaction uses a savepoint and leaves
101
+ completion to that caller. SQLite table rebuilds are Knex-managed internally;
102
+ the library holds their connection lease through error cleanup, rolling back a
103
+ transaction the driver reports still active and restoring foreign-key enforcement
104
+ before release. If cleanup cannot establish a reusable connection, it is disposed.
105
+ See the [schema guide](21-schema-and-migrations.md) for DDL-specific
106
+ ownership, cleanup errors and dialect restrictions.
107
+
108
+ If a completion method rejects without settling its driver completion promise,
109
+ an owner finishing with outcome `unknown` also discards its unreleased connection
110
+ lease. A later driver promise settlement cannot release the lease twice. The raw
111
+ driver promise may remain unsettled; application completion follows the library
112
+ call and its outcome, rather than waiting on that internal promise.
113
+
114
+ Connection release is cleanup, separate from the driver's SQL-completion promise.
115
+ A release error after acknowledged COMMIT keeps outcome `committed`; it becomes a
116
+ post-commit error and later completion hooks/finalizers are still attempted. A
117
+ release error after confirmed rollback keeps `rolledBack`, preserves the original
118
+ failure, and adds a `connectionRelease` diagnostic to the owner's cleanup errors.
119
+ Failed BEGIN also retains its original cause if releasing its lease fails.
120
+
121
+ Disposal does not establish whether a write committed, and does not replay it.
122
+ Knex destroys the disposed connection during pool validation or idle cleanup;
123
+ this is not a guarantee of immediate server-side lock release. SQLite `:memory:`
124
+ data belongs to its connection and is lost when that connection is discarded;
125
+ use a file-backed database when data must survive connection replacement.
126
+
127
+ After `unknown`, reconcile using application-owned operation identity and
128
+ persisted state before deciding whether to issue a new write. Do not treat a
129
+ previously captured participant error as the owner's final outcome. Do not infer
130
+ that files in upload tracking are safe to delete: they may belong to committed
131
+ rows. Outcome-dependent hooks are skipped when the outcome is unknown, so
132
+ notifications and other in-memory completion work may require reconciliation as
133
+ well. There is no durable outbox, automatic retry or exactly-once delivery.
134
+
135
+ Database rollback does not undo email, remote calls or other effects performed
136
+ outside that database. Even `rolledBack` permits a safe retry only when the
137
+ application can repeat or reconcile those effects. See the
138
+ [file ownership contract](26-file-uploads.md#ownership-after-commit).
139
+
140
+ The completion tests inject failures at the actual driver query boundary before
141
+ and after executing COMMIT/ROLLBACK, inspect persisted rows afterward, and
142
+ assert no outcome hooks run for unknown completion. They cover real supported
143
+ databases, but do not simulate every network partition, server failover or
144
+ physical connection-loss sequence. This contract reports available evidence;
145
+ it does not promise to resolve an acknowledgement the client never received.
146
+
147
+ ## Selected error carrier
148
+
149
+ Writes reject with `RestApiWriteError`, exported from the package root. Its
150
+ read-only `transactionOutcome` is one of the five strings above. `cause` retains
151
+ the original thrown value, including a frozen error, null or undefined; a fresh
152
+ wrapper is created for each failed operation. Read errors retain their existing
153
+ contract. The runtime propagates null and undefined throws. Errors already lost
154
+ inside an extension's own catch cannot be restored by the write boundary.
155
+
156
+ The wrapper retains ordinary data fields used for classification: `code`,
157
+ `type`, `subtype`, `details`, `statusCode`, `status`, `path`, `parameter` and
158
+ `validation`, including inherited status fields from HTTP error classes. It does
159
+ not execute diagnostic getters. Without an original code,
160
+ its code is `REST_API_WRITE`. The original error class is available through
161
+ `cause`; the wrapper itself has class `RestApiWriteError`. Its JSON representation
162
+ omits the cause, stack and transaction handle while retaining diagnostic fields.
163
+
164
+ An atomic child's wrapper can be the outer wrapper's cause. Both snapshots are
165
+ retained: for example, child pending and outer rolledBack. Existing code/subtype
166
+ classification can usually remain at the outer error; code that checks original
167
+ error identity or `instanceof` needs migration. HTTP mapping preserves status
168
+ classification and adds `meta.transactionOutcome` to each JSON:API error.
169
+ Non-atomic bulk entries expose `error.transactionOutcome` individually.
170
+
171
+ ## Errors, batches and later observations
172
+
173
+ Outcome evidence must survive a secondary cleanup error without replacing the
174
+ primary failure or its cause. The selected wrapper carries frozen errors and
175
+ non-Error throws without mutating the original value.
176
+ Completion hooks and internal diagnostic finalizers are attempted in their
177
+ documented order despite earlier completion failures. After commit, the first
178
+ such failure remains primary. Finalizer errors after rollback or unknown
179
+ completion remain secondary; they do not change the observed outcome or cause
180
+ speculative outcome hooks to run. See the
181
+ [managed completion contract](19-managed-transactions.md#completion-work).
182
+ Only safe, serializable outcome fields belong in transport errors. Transaction,
183
+ connection, storage and raw cleanup objects remain internal diagnostics.
184
+
185
+ An outcome attached to an error is a snapshot, not a live view. A borrowed
186
+ operation can report pending and its owner later report rolledBack. Later
187
+ confirmation belongs to the owner's completion result; do not mutate previously
188
+ reported failures or their causes to rewrite that history.
189
+
190
+ An atomic batch has one transaction outcome. A non-atomic batch has separate
191
+ outcomes for its entries; it must not claim one shared committed/rolledBack
192
+ outcome from aggregate success counts. Its `meta.failed` counts rejected calls,
193
+ including possible post-commit failures. Each rejected entry carries its own
194
+ outcome; mixed results do not introduce a sixth transaction state.
195
+
196
+ None of these states is a general retry instruction. Repeating a committed or
197
+ unknown write can duplicate effects. Pending work belongs to its owner. Even a
198
+ confirmed rollback cannot undo an email, upload, external API call or other
199
+ effect outside the database transaction. No automatic replay of arbitrary hooks
200
+ or setters is part of this contract.
201
+
202
+ ### Dispatcher failures
203
+
204
+ Resource dispatch and hook execution preserve the original thrown value, including
205
+ null and undefined. The runtime does not add automatic failure logging or wrap plugin
206
+ installation errors. The transaction layer attaches write outcomes and records failed
207
+ cleanup diagnostics without replacing the primary cause.