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.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +733 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +103 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +37 -0
- package/index.js +3 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +70 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +120 -56
- package/plugins/core/lib/anyapi/stored-definition.js +109 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/anyapi.d.ts +30 -0
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +65 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- 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).
|