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,278 @@
1
+ ---
2
+ title: "File uploads"
3
+ chapter: 26
4
+ chapter_label: "26"
5
+ ---
6
+
7
+ # 26. File uploads
8
+
9
+ `FileHandlingPlugin` connects declared file fields to storage adapters. The
10
+ Express connector supports real Busboy 1.x and Formidable 3.x multipart parsers.
11
+ Fastify currently accepts JSON resource documents and rejects multipart with 415.
12
+
13
+ File attributes contain the storage adapter's URL or handle as a string.
14
+ Ordinary file columns currently use binary SQL storage; their UTF-8 bytes are
15
+ decoded before returning the public value. Canonical file slots store text.
16
+ Invalid UTF-8 fails with a normalization error instead of silently corrupting
17
+ the handle. A custom getter owns decoding its database representation; the
18
+ response still follows the file-handle contract. Blob attributes retain their
19
+ separate binary representation. This correction requires no schema migration.
20
+
21
+ ## Quick Start
22
+
23
+ Install the chosen optional parser along with the connector and storage driver:
24
+
25
+ ```sh
26
+ npm install json-rest-api express knex better-sqlite3 busboy
27
+ ```
28
+
29
+ The following example uses an in-memory database and a local upload directory:
30
+
31
+ ```js
32
+ import { JsonRestApi } from 'json-rest-api'
33
+ import express from 'express'
34
+ import knex from 'knex'
35
+ import { RestApiPlugin, RestApiKnexPlugin, FileHandlingPlugin, ExpressPlugin } from 'json-rest-api'
36
+ import { LocalStorage } from 'json-rest-api/plugins/storage/local-storage.js'
37
+
38
+ const app = express()
39
+ const db = knex({ client: 'better-sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true })
40
+ const storage = new LocalStorage({ directory: './uploads', fileBaseUrl: '/uploads' })
41
+ const api = new JsonRestApi({ name: 'uploads' })
42
+
43
+ await api.use(RestApiPlugin, { format: 'plain', returning: 'full' })
44
+ await api.use(RestApiKnexPlugin, { knex: db })
45
+ await api.use(FileHandlingPlugin)
46
+ await api.use(ExpressPlugin, {
47
+ mountPath: '/api',
48
+ fileParser: 'busboy',
49
+ fileParserOptions: { limits: { fileSize: 10 * 1024 * 1024, files: 2 } }
50
+ })
51
+ await api.addResource('images', {
52
+ schema: {
53
+ id: { type: 'id' },
54
+ title: { type: 'string', required: true },
55
+ image: { type: 'file', storage, accepts: ['image/png'], maxSize: '10mb', required: true }
56
+ }
57
+ })
58
+ await api.resources.images.createKnexTable()
59
+
60
+ app.use('/uploads', express.static('./uploads'))
61
+ api.http.express.mount(app)
62
+ const server = app.listen(3000)
63
+ ```
64
+
65
+ Install `FileHandlingPlugin` before `ExpressPlugin`, then declare resources.
66
+ An unavailable parser or invalid detector factory fails setup; it no longer
67
+ silently disables uploads. Close the HTTP server and destroy `db` during your
68
+ application's shutdown. Choose the application's own file-serving policy; the
69
+ example serves public files through Express static middleware.
70
+
71
+ ```sh
72
+ curl -H 'Accept: application/vnd.api+json' \
73
+ -F 'title=Example' -F 'image=@photo.png;type=image/png' \
74
+ http://localhost:3000/api/images
75
+ ```
76
+
77
+ Multipart field names match resource attribute and file-field names. Express
78
+ builds the resource envelope using the route's type. The response is a full
79
+ JSON:API document containing the stored file URL, regardless of programmatic
80
+ plain/JSON:API defaults.
81
+
82
+ ## Schema Configuration
83
+
84
+ | File field option | Meaning |
85
+ | --- | --- |
86
+ | `type: 'file'` | Store the resulting URL in this attribute |
87
+ | `storage` | Adapter implementing `upload(file)`; `delete(url)` enables rollback cleanup |
88
+ | `accepts` | Match the part's reported MIME type; exact types, `image/*`, or `['*']` |
89
+ | `maxSize` | Inclusive maximum in bytes, or a string such as `'10mb'` or `'1.5gb'` |
90
+ | `required` | Ordinary resource schema requirement for creation/replacement; PATCH may omit it |
91
+
92
+ Schema size and MIME checks run after parsing and before uploading to storage.
93
+ Parser limits bound the data accepted during parsing. A numeric `maxSize` of
94
+ zero permits only an empty file. MIME metadata checks do not inspect file bytes.
95
+
96
+ Resource POST, PUT and PATCH support multipart. PUT takes its identity from the
97
+ URL, just like a regular resource call. PATCH may update only text fields while
98
+ preserving the stored URL of an omitted file. Manage relationships through the
99
+ normal resource/relationship methods; multipart fields carry attributes/files.
100
+
101
+ Repeated text fields become arrays. `tags[]` entries are grouped under `tags`;
102
+ indexed bracket suffixes also append in arrival order. Text remains text until
103
+ the resource schema applies its normal validation/coercion. Unknown attributes
104
+ and unknown uploaded file fields are rejected. One uploaded file per file field
105
+ is supported; repeating a file field produces 400 instead of keeping the first
106
+ or last file. Distinct declared file fields can be uploaded together.
107
+
108
+ ## Protocol Configuration
109
+
110
+ `fileParser` accepts `'busboy'`, `'formidable'`, or a factory returning a detector
111
+ with `name`, `detect(params)` and `parse(params)`. The factory receives
112
+ `fileParserOptions`. `enableFileUploads: false` disables the connector's multipart
113
+ handling. Express's `requestSizeLimit` controls JSON parsing; use parser options
114
+ for multipart limits.
115
+
116
+ ### Busboy
117
+
118
+ ```js
119
+ await api.use(ExpressPlugin, {
120
+ mountPath: '/api',
121
+ fileParser: 'busboy',
122
+ fileParserOptions: {
123
+ limits: { fileSize: 1024 * 1024, files: 2, fields: 10, fieldSize: 64 * 1024, parts: 12 }
124
+ }
125
+ })
126
+ ```
127
+
128
+ The detector uses the current Busboy factory and file-info event API. Files are
129
+ buffered in memory. Limits are inclusive: a file of exactly `fileSize` bytes is
130
+ accepted; one extra byte is rejected. Truncated fields and parts/files/fields
131
+ limit events reject the whole request. See [Busboy's API](https://github.com/mscdex/busboy#api)
132
+ for parser options.
133
+
134
+ ### Formidable
135
+
136
+ ```sh
137
+ npm install formidable
138
+ ```
139
+
140
+ ```js
141
+ await api.use(ExpressPlugin, {
142
+ mountPath: '/api',
143
+ fileParser: 'formidable',
144
+ fileParserOptions: {
145
+ uploadDir: './upload-temp',
146
+ maxFileSize: 1024 * 1024,
147
+ maxTotalFileSize: 2 * 1024 * 1024,
148
+ maxFiles: 2,
149
+ maxFields: 10,
150
+ maxFieldsSize: 64 * 1024
151
+ }
152
+ })
153
+ ```
154
+
155
+ Each request gets a private directory beneath `uploadDir`, which defaults to
156
+ the operating system's temp directory. Completed files are read into buffers;
157
+ the request directory is removed before parsing returns, including on error or
158
+ cancellation. Other requests and unrelated files in the parent directory are
159
+ preserved. The result has no persistent `filepath` or cleanup callback. See
160
+ [Formidable's options](https://github.com/node-formidable/formidable#options).
161
+
162
+ Both detectors buffer their final results; neither streams directly into the
163
+ storage adapter. Empty files are accepted unless schema validation excludes
164
+ them. Default limits are:
165
+
166
+ | Limit | Busboy | Formidable |
167
+ | --- | --- | --- |
168
+ | Per file | 10 MiB | 10 MiB |
169
+ | Files | 10 | 10 |
170
+ | Text fields | 100 | 100 |
171
+ | Text bytes | 64 KiB per field | 6.25 MiB total |
172
+ | Combined file bytes | Bounded by file count and per-file size | 100 MiB |
173
+ | Parts | 110 | Bounded by file/field counts |
174
+
175
+ ## Storage Adapters
176
+
177
+ `upload(file)` receives `filename`, `mimetype`, `size` and a `data` Buffer, and
178
+ returns a stored URL. Implement `delete(url)` to remove a newly uploaded file
179
+ when the upload's managed transaction rolls back. That deletion should be
180
+ idempotent for URLs produced by the adapter.
181
+
182
+ `LocalStorage` writes actual files. Its URL option is `fileBaseUrl`; supported
183
+ naming strategies are `hash`, `timestamp`, `original` and `custom`. Use `nameGenerator` with the custom naming strategy.
184
+ Filename generation is not a reservation. Uploads reserve their destination
185
+ with exclusive creation and select another name if a concurrent upload has
186
+ claimed it. Existing files, directories and symlinks are occupied names.
187
+ Concurrent uploads therefore cannot overwrite one another, including with
188
+ `original` or custom names. Availability/permission errors propagate instead
189
+ of being treated as evidence that a filename is unused.
190
+
191
+ Buffer uploads write to the reserved file; temporary-file uploads stream into
192
+ it and remove their source only after writing and closing succeed. A failed
193
+ upload attempts to close and remove only its reserved destination. If that
194
+ cleanup also fails, an `AggregateError` retains the original failure in `cause`
195
+ and lists it followed by cleanup failures in `errors`; the destination may
196
+ remain for reconciliation. The adapter does not retry a write or rerun a custom
197
+ name generator after partial failure. Only an occupied-name reservation is retried.
198
+
199
+ For remote object storage, provide an adapter implementing the same `upload`
200
+ and `delete` methods. The library includes `LocalStorage` only.
201
+
202
+ Custom detectors can still return `file.cleanup()`. `FileHandlingPlugin` awaits
203
+ those callbacks after processing, including validation failures. Built-in
204
+ Formidable cleanup now happens inside the detector instead.
205
+
206
+ ## Errors and cleanup
207
+
208
+ | Result | HTTP status |
209
+ | --- | ---: |
210
+ | Malformed multipart, missing boundary, repeated file field | 400 |
211
+ | Parser byte/count/part limit | 413 |
212
+ | Declared MIME/size requirement or resource schema violation | 422 |
213
+ | Unexpected detector, parser or storage failure | 500 |
214
+ | Typed access failure from storage | 403 |
215
+
216
+ A matched detector's failure propagates; the library does not try another parser
217
+ on an already consumed request. Typed parser errors retain their original cause
218
+ where available. A later validation failure rolls back an owned database write
219
+ and deletes tracked new uploads. Tests also disconnect real HTTP clients after
220
+ partial disk writes and verify temporary-file removal and database rollback.
221
+
222
+ Unexpected detector/parser/upload failures retain their original thrown value
223
+ as `cause`, with phase and field/detector context. Typed API errors retain their
224
+ identity. Storage failures are no longer converted into schema validation
225
+ errors; MIME, size and ordinary resource validation still return 422.
226
+
227
+ Custom temporary-file cleanup and uploaded-file deletion are best effort.
228
+ Failures are retained in the operation's `context.cleanupErrors` as
229
+ `{ phase, field, error }`, using `temporaryFileCleanup` or `uploadedFileCleanup`.
230
+ A failed warning log is recorded as `logging` with its `during` phase. Cleanup
231
+ continues with later files, and early cleanup diagnostics survive a subsequent
232
+ write failure. Failed upload deletions remain in `context.fileHandlingUploads`
233
+ for inspection and reconciliation; the library does not silently retry them.
234
+
235
+ Uploads are tracked against their creating transaction. Rollback cleanup selects
236
+ that transaction's uploads, so reusing a context cannot delete an earlier
237
+ transaction's committed files. Confirmed commit releases its tracking entries.
238
+ Standalone writes own their transaction. To group writes, pass the handle from
239
+ `api.transaction` to each operation and use a separate context for each one.
240
+ Individual operations finish while that unit is pending; the outer helper waits
241
+ for commit or rollback and the enlisted completion hooks. Rollback cleans the
242
+ operations in reverse order, including repeated uploads to the same record.
243
+ A failed completion chain does not prevent later operations' chains from running.
244
+ File deletion diagnostics remain on the operation's context; rejected completion
245
+ hooks also appear in the owner's error or indexed `cleanupErrors`.
246
+
247
+ Raw Knex transactions cannot own library writes. See the
248
+ [migration guide](33-migrating-to-v2.md) for the callback form.
249
+
250
+ If a commit wrapper rejects after the driver has completed the transaction,
251
+ the failure handler does not attempt rollback or run rollback-file cleanup.
252
+ Driver completion alone does not distinguish every committed, rolled-back or
253
+ uncertain outcome; it is not a guarantee that replaying the write is safe.
254
+
255
+ ### Ownership after commit
256
+
257
+ File fields store opaque URLs; several records, resources or external callers
258
+ can refer to the same object. The library owns cleanup of parser temporary files
259
+ and newly uploaded objects until their transaction completes. Confirmed rollback
260
+ attempts to delete new uploads; confirmed commit releases upload tracking and
261
+ leaves the stored objects under application ownership.
262
+
263
+ PATCH/PUT replacement and DELETE do not delete previously committed files,
264
+ even after the last reference in that resource disappears. For example, if two
265
+ documents use `/uploads/shared.png`, replacing one document's attachment keeps
266
+ that file available to the other. Rolling back the replacement deletes only the
267
+ new upload and restores the old field value.
268
+
269
+ Applications that need garbage collection must track ownership and references
270
+ across their own consumers, then call the storage adapter's `delete(url)` after
271
+ they establish that the object is no longer needed. A successful record deletion
272
+ alone does not establish that. This also applies to intermediate uploads from
273
+ repeated writes in a transaction that commits. There is no automatic reference
274
+ registry or background garbage collector.
275
+
276
+ Run `npm run test:multipart` for native parser and resource tests on both storage
277
+ modes. The full gate also exercises multipart resource operations under Express
278
+ 4 and 5. See the [migration guide](33-migrating-to-v2.md) before updating callers.
@@ -0,0 +1,277 @@
1
+ ---
2
+ title: "Bulk operations"
3
+ chapter: 27
4
+ chapter_label: "27"
5
+ ---
6
+
7
+ # 27. Bulk operations
8
+
9
+ `BulkOperationsPlugin` provides `bulkPost`, `bulkPatch` and `bulkDelete`. Entries
10
+ run through their normal resource lifecycle, sequentially. A bulk request can
11
+ reduce network overhead; it does not turn resource writes into one batched SQL
12
+ statement or skip permissions, validation, setters and hooks.
13
+
14
+ POST/PATCH use the same output-only `format` and `returning` options as individual writes.
15
+ Results contain `meta`, optional indexed `errors`, and `data` for full/minimal
16
+ responses. `returning: 'none'` omits data. DELETE returns its summary and deleted
17
+ IDs in `meta.deleted`.
18
+
19
+ ## Runnable example
20
+
21
+ Insert the following seven JavaScript blocks into the
22
+ [starting script](03-running-example.md), after installing storage and
23
+ before declaring resources or starting the server, on a fresh database. Later
24
+ snippets in this guide are fragments illustrating existing application state.
25
+
26
+ ```javascript
27
+ import { BulkOperationsPlugin } from 'json-rest-api/plugins/core/bulk-operations-plugin.js'
28
+
29
+ await api.use(BulkOperationsPlugin, { maxBulkOperations: 100, defaultAtomic: true })
30
+ await api.addResource('books', {
31
+ schema: { title: { type: 'string', required: true } }
32
+ })
33
+ await api.resources.books.createKnexTable()
34
+ ```
35
+
36
+ ```javascript
37
+ const created = await api.resources.books.bulkPost({
38
+ data: [{ title: 'Alpha' }, { title: 'Beta' }], format: 'plain'
39
+ })
40
+ console.log(created.data, created.meta)
41
+ ```
42
+
43
+ The two records are returned in input order. The summary is
44
+ `{ total: 2, succeeded: 2, failed: 0, atomic: true }`.
45
+ For JSON:API bulk creation, pass `document: { data: [...] }`, where each array
46
+ entry is a JSON:API resource object with `type`, attributes and relationships.
47
+ Supply either the plain `data` array or `document`, never both. Input selection
48
+ does not determine the output format.
49
+
50
+ ```javascript
51
+ const updated = await api.resources.books.bulkPatch({
52
+ operations: created.data.map(book => ({ id: book.id, data: { title: `${book.title} revised` } })),
53
+ format: 'plain', returning: 'minimal'
54
+ })
55
+ console.log(updated.data, updated.meta)
56
+ ```
57
+
58
+ PATCH entries select input individually: `{ id, data: plainValues }` or
59
+ `{ id, document: { data: resourceObject } }`. Each entry accepts exactly one input
60
+ key; a batch may contain both kinds. `format` selects output for the batch.
61
+ Minimal output contains identifiers.
62
+
63
+ ```javascript
64
+ const partial = await api.resources.books.bulkPost({
65
+ data: [{ title: 'Gamma' }, {}], format: 'plain', atomic: false
66
+ })
67
+ console.log(partial.data, partial.errors, partial.meta)
68
+ ```
69
+
70
+ Gamma is stored, while the second entry fails required-title validation. The
71
+ summary reports one success and one failure; `errors[0].index` is 1. Successful
72
+ data entries are compacted, so their array positions do not represent failed
73
+ input slots. Use the indexed errors to identify rejected inputs.
74
+
75
+ ```javascript
76
+ let atomicError
77
+ try {
78
+ await api.resources.books.bulkPost({
79
+ data: [{ title: 'Not retained' }, {}], format: 'plain', atomic: true
80
+ })
81
+ } catch (error) { atomicError = error }
82
+ const afterAtomicFailure = await api.resources.books.query({ format: 'plain' })
83
+ console.log(atomicError?.transactionOutcome, afterAtomicFailure.data.map(book => book.title))
84
+ ```
85
+
86
+ The owned batch rejects with rolled-back outcome, and Not retained is absent.
87
+ The earlier, separately completed calls remain stored.
88
+
89
+ ```javascript
90
+ const gamma = partial.data[0]
91
+ let managedError
92
+ try {
93
+ await api.transaction(async transaction => {
94
+ await api.resources.books.bulkPatch({
95
+ operations: [{ id: gamma.id, data: { title: 'Pending title' } }],
96
+ format: 'plain', returning: 'none', atomic: true, transaction
97
+ })
98
+ throw new Error('Cancel this unit')
99
+ })
100
+ } catch (error) { managedError = error }
101
+ const afterManagedRollback = await api.resources.books.get({ id: gamma.id, format: 'plain' })
102
+ console.log(managedError?.message, afterManagedRollback.title)
103
+ ```
104
+
105
+ The outer callback rejection restores Gamma. The batch borrows the managed
106
+ transaction and cannot independently commit its changes.
107
+
108
+ ```javascript
109
+ const deleted = await api.resources.books.bulkDelete({ ids: created.data.map(book => book.id), atomic: true })
110
+ const remaining = await api.resources.books.query({ format: 'plain' })
111
+ console.log(deleted.meta, remaining.data)
112
+ ```
113
+
114
+ The delete summary names the two deleted IDs. Gamma remains.
115
+
116
+ ## Configuration Options
117
+
118
+ Pass these options directly to `api.use(BulkOperationsPlugin, options)`:
119
+
120
+ | Option | Type | Default | Description |
121
+ |--------|------|---------|-------------|
122
+ | `maxBulkOperations` | positive safe integer | 100 | Maximum number of records that can be processed in a single request |
123
+ | `defaultAtomic` | boolean | true | Whether operations are atomic (all-or-nothing) by default |
124
+
125
+ Invalid values and unknown option names fail during installation. Remove
126
+ `batchSize` and `enableOptimizations`: they did not provide SQL batching, and the
127
+ disconnected optimization handler has been removed. Do not wrap options inside
128
+ a `'bulk-operations'` key. See [the migration steps](33-migrating-to-v2.md#bulk-writes).
129
+
130
+ ## Failures and diagnostics
131
+
132
+ Atomic failures preserve the original error if rollback also rejects. The
133
+ outer batch attempts rollback once for an unfinished transaction it owns and stores
134
+ secondary rollback errors in the supplied context's `cleanupErrors` array;
135
+ `context.error` retains the original failure. Both diagnostic fields reset on
136
+ the next bulk call. A rejected rollback does not confirm that data was restored.
137
+ Managed completion follows the [transaction contract](19-managed-transactions.md).
138
+
139
+ Child cleanup diagnostics are now available on the batch context, including
140
+ warnings from successful entries. Each has a zero-based `bulkIndex` alongside
141
+ its `phase`, original `error` and any file information. For example:
142
+
143
+ ```js
144
+ const context = {};
145
+ const result = await api.resources.books.bulkPatch({
146
+ operations: [{ id: '42', data: { title: 'Updated title' } }],
147
+ format: 'plain',
148
+ atomic: false
149
+ }, context);
150
+
151
+ for (const diagnostic of context.cleanupErrors || []) {
152
+ console.error(diagnostic.bulkIndex, diagnostic.phase, diagnostic.error);
153
+ }
154
+ ```
155
+
156
+ The batch response keeps its existing indexed primary errors; secondary cleanup
157
+ errors are retained on the context. `meta.failed` counts rejected calls, not
158
+ rolled-back writes: an `afterCommit` hook can fail after its data is stored.
159
+
160
+ Remaining child uploads are retained in `context.fileHandlingUploads`, also
161
+ tagged with `bulkIndex` and retaining their storage and transaction references.
162
+ They can include a failed deletion or committed files whose tracking-release
163
+ hook did not run. Do not delete or retry them solely because they are tracked.
164
+ Reusing the batch context clears old error diagnostics and keeps unresolved
165
+ upload tracking without copying it into new children. Write errors expose
166
+ `transactionOutcome`; managed completion-chain diagnostics also identify
167
+ `operationIndex`, `scopeName` and `method`. Broader integration remains in progress.
168
+
169
+ ## Version conditions
170
+
171
+ For a resource with an explicit stored `versionField`, bulk PATCH and DELETE
172
+ accept `expectedVersions`: one revision token for each entry, in the same order
173
+ as `operations` or `ids`. Omit the entire array for unconditional writes. Each
174
+ token must be a non-empty string of at most 128 characters; null entries and
175
+ partially conditional arrays are not supported. The library validates the
176
+ array before starting any child operation, including in non-atomic mode.
177
+
178
+ ```js
179
+ const current = await api.resources.books.get({ id: '42', format: 'plain' })
180
+ await api.resources.books.bulkPatch({
181
+ operations: [{ id: current.id, data: { title: 'Updated title' } }],
182
+ expectedVersions: [current.revision],
183
+ format: 'plain'
184
+ })
185
+ ```
186
+
187
+ Use the revision field selected by your resource configuration. It belongs in
188
+ `expectedVersions`, not in the submitted attributes. Successful updates rotate
189
+ the revision even when no conditions were supplied. Read full responses or
190
+ fetch the record again to obtain its new revision.
191
+
192
+ A stale condition raises `REST_API_VERSION_CONFLICT`. An owned atomic batch
193
+ rolls back all its changes. A non-atomic batch retains successful children and
194
+ reports the failed child's index and transaction outcome in `errors`. Caller
195
+ transactions retain their normal ownership rules below. Missing or inaccessible
196
+ rows retain the single-write not-found behavior.
197
+
198
+ HTTP bulk PATCH/DELETE use the same `expectedVersions` property in the request
199
+ body, alongside `operations` or `ids`. Atomic version conflicts map to HTTP 409;
200
+ malformed condition arrays map to 422. These are body conditions, not HTTP
201
+ `If-Match` validators. Bulk creation rejects revision conditions, and all bulk
202
+ methods reject the singular `expectedVersion` option.
203
+
204
+ ## Transaction ownership
205
+
206
+ Pass `transaction` in the first argument to bulkPost, bulkPatch or bulkDelete,
207
+ with `atomic: true`. Each entry uses that same transaction, including its
208
+ relationship changes and full-response reads. The bulk call neither commits nor
209
+ rolls it back. Other calls and raw SQL can join the same transaction.
210
+
211
+ ```js
212
+ await api.transaction(async transaction => {
213
+ return api.resources.books.bulkPatch({
214
+ operations: [{ id: '42', data: { title: 'Updated title' } }],
215
+ format: 'plain',
216
+ returning: 'minimal',
217
+ atomic: true,
218
+ transaction
219
+ })
220
+ })
221
+ ```
222
+
223
+ The managed helper rolls back the whole unit after callback rejection or a
224
+ participating write/observed SQL failure, even when caught. A rejected batch can
225
+ leave changes pending until the helper finishes; its individual operations do
226
+ not complete the transaction or create savepoints. Raw transactions and child
227
+ savepoints cannot be passed to library writes. Raw SQL can join the managed
228
+ callback through its actual Knex handle.
229
+
230
+ Without a supplied transaction, an atomic batch owns one transaction for all
231
+ entries; a non-atomic batch lets each entry own its transaction. Passing a
232
+ transaction with `atomic: false` rejects before any child or SQL runs. This also
233
+ applies when `defaultAtomic` is false: explicitly select `atomic: true` to join
234
+ an outer transaction. A completed transaction rejects through the normal write
235
+ path and is never silently replaced with a new one.
236
+
237
+ Managed completion defers enlisted completion chains to the owner. See
238
+ [managed transactions](19-managed-transactions.md) and
239
+ [transaction outcomes](20-transaction-outcomes.md) for file/event integration,
240
+ completion failures and uncertain outcomes.
241
+
242
+ ## HTTP API Usage
243
+
244
+ Express and Fastify expose bulk endpoints. Install the connector and bulk
245
+ plugin **before declaring resources**, then mount/start the application.
246
+ Authenticated transport context reaches every ordinary resource write and its
247
+ permission checks. HTTP always selects JSON:API/full responses: POST returns
248
+ 201, PATCH and DELETE return 200 with the batch summary. `atomic` query values
249
+ must be exactly `true` or `false`; malformed values produce 422 without writes.
250
+ The optional plugin reserves `/RESOURCE/bulk` for these methods, including when
251
+ the resource accepts opaque IDs. This bulk protocol is not an implementation of
252
+ the JSON:API Atomic Operations extension.
253
+
254
+
255
+ The bulk HTTP request bodies are:
256
+
257
+ | Method and path | Body fields |
258
+ | --- | --- |
259
+ | `POST /api/books/bulk` | `data`: array of JSON:API resource objects |
260
+ | `PATCH /api/books/bulk` | `operations`: array of `{ id, data }`, where `data` is a resource object |
261
+ | `DELETE /api/books/bulk` | `ids`: array of resource IDs |
262
+
263
+ For example:
264
+
265
+ ```json
266
+ {
267
+ "operations": [
268
+ { "id": "1", "data": { "type": "books", "id": "1", "attributes": { "title": "Revised" } } }
269
+ ]
270
+ }
271
+ ```
272
+
273
+ Select `?atomic=false` for independent child transactions, or omit it for the
274
+ configured default. Non-atomic error summaries require application handling;
275
+ a successful HTTP status does not mean every child call succeeded. In
276
+ particular, inspect each error's transaction outcome before retrying a mutation
277
+ that may already have committed. See [transaction outcomes](20-transaction-outcomes.md).