json-rest-api 1.0.29 → 2.1.0

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