json-rest-api 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/docs/API.md +262 -54
- package/docs/GUIDE/01-getting-started.md +1 -1
- package/docs/GUIDE/07-polymorphic-relationships.md +4 -2
- package/docs/GUIDE/09-pagination-and-sorting.md +4 -2
- package/docs/GUIDE/13-hooks-and-lifecycle.md +34 -6
- package/docs/GUIDE/14-custom-resource-methods.md +3 -2
- package/docs/GUIDE/16-autofiltering.md +54 -2
- package/docs/GUIDE/17-row-policies.md +15 -5
- package/docs/GUIDE/19-managed-transactions.md +8 -9
- package/docs/GUIDE/20-transaction-outcomes.md +3 -4
- package/docs/GUIDE/21-schema-and-migrations.md +7 -1
- package/docs/GUIDE/22-version-field-migrations.md +3 -3
- package/docs/GUIDE/27-many-row-writes.md +458 -0
- package/docs/GUIDE/28-socketio.md +10 -2
- package/docs/GUIDE/30-backend-capabilities.md +42 -2
- package/docs/GUIDE/31-positioning.md +3 -1
- package/docs/GUIDE/33-migrating-to-v2.md +2 -2
- package/docs/GUIDE/34-scoped-reads-and-locks.md +464 -0
- package/docs/GUIDE/35-migrating-to-v3.md +325 -0
- package/docs/GUIDE/index.md +5 -2
- package/docs/QUICKSTART.md +2 -2
- package/docs/contributing.md +1 -1
- package/index.d.ts +8 -1
- package/index.js +4 -0
- package/lib/rest-api-errors.js +79 -0
- package/lib/runtime/json-rest-api.js +18 -3
- package/lib/system-context.js +42 -0
- package/package.json +3 -2
- package/plugins/core/bulk-operations-plugin.js +54 -436
- package/plugins/core/connectors/lib/transport-http-helpers.js +34 -3
- package/plugins/core/file-handling-plugin.js +17 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +3 -2
- package/plugins/core/lib/anyapi/canonical-link-store.js +5 -2
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +56 -60
- package/plugins/core/lib/dbIntrospection.js +106 -6
- package/plugins/core/lib/querying/aggregate-query.js +215 -22
- package/plugins/core/lib/querying/include-query-helpers.js +11 -13
- package/plugins/core/lib/querying/include-to-one.js +54 -59
- package/plugins/core/lib/querying/knex-pagination-helpers.js +9 -2
- package/plugins/core/lib/querying/query-constraint.d.ts +11 -0
- package/plugins/core/lib/querying/query-constraint.js +45 -0
- package/plugins/core/lib/querying/read-only-query.js +92 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +14 -0
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +40 -3
- package/plugins/core/lib/storage/canonical-data-helpers.js +103 -39
- package/plugins/core/lib/storage/many-row-data-helpers.js +738 -0
- package/plugins/core/lib/storage/ordinary-data-helpers.js +63 -79
- package/plugins/core/lib/storage/single-row-writes.js +71 -0
- package/plugins/core/lib/storage/storage-adapter.js +18 -8
- package/plugins/core/lib/storage/storage-mapping.js +4 -1
- package/plugins/core/lib/storage/storage-types.d.ts +21 -3
- package/plugins/core/lib/writing/many-to-many-manipulations.js +10 -5
- package/plugins/core/lib/writing/relationship-processor.js +33 -37
- package/plugins/core/rest-api-anyapi-knex-plugin.js +3 -0
- package/plugins/core/rest-api-autofilter-plugin.js +137 -10
- package/plugins/core/rest-api-knex-plugin.js +2 -0
- package/plugins/core/rest-api-plugin-methods/aggregate.js +3 -1
- package/plugins/core/rest-api-plugin-methods/common.js +144 -84
- package/plugins/core/rest-api-plugin-methods/delete-many.js +132 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +22 -0
- package/plugins/core/rest-api-plugin-methods/get.js +48 -42
- package/plugins/core/rest-api-plugin-methods/many-row-common.js +274 -0
- package/plugins/core/rest-api-plugin-methods/patch-many.js +333 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +3 -1
- package/plugins/core/rest-api-plugin-methods/post-many.js +295 -0
- package/plugins/core/rest-api-plugin-methods/put.js +5 -1
- package/plugins/core/rest-api-plugin-methods/scoped-reads.js +221 -0
- package/plugins/core/rest-api-plugin.js +17 -0
- package/plugins/core/rest-api-positioning-plugin.js +22 -4
- package/plugins/core/rest-api-query-projections-plugin.js +9 -1
- package/plugins/core/rest-api-row-policy-plugin.js +4 -0
- package/plugins/core/socketio-plugin.js +80 -2
- package/types/autofilter.d.ts +5 -1
- package/types/data-access-methods.d.ts +141 -0
- package/types/errors.d.ts +45 -0
- package/types/hook-context.d.ts +21 -0
- package/types/plugin-options.d.ts +3 -1
- package/types/resource-methods.d.ts +27 -4
- package/types/row-policy.d.ts +3 -0
- package/types/runtime.d.ts +7 -1
- package/docs/GUIDE/27-bulk-operations.md +0 -277
- package/types/bulk-methods.d.ts +0 -63
package/types/errors.d.ts
CHANGED
|
@@ -116,3 +116,48 @@ export class RestApiTemporalDataError extends RestApiError {
|
|
|
116
116
|
details: { field?: string; resourceType?: string; fieldType?: string; source: string }
|
|
117
117
|
toJSON(): SerializedError & Pick<RestApiTemporalDataError, 'code' | 'type' | 'statusCode' | 'details'>
|
|
118
118
|
}
|
|
119
|
+
|
|
120
|
+
export class RestApiBulkWriteUnsupportedError extends RestApiError {
|
|
121
|
+
constructor(options?: { resourceType?: string; operation?: string; reason?: string; hooks?: string[] })
|
|
122
|
+
code: 'REST_API_BULK_WRITE_UNSUPPORTED'
|
|
123
|
+
type: 'rest_api_bulk_write_unsupported'
|
|
124
|
+
statusCode: 422
|
|
125
|
+
/** `hooks` names the per-record hooks that caused the refusal; HTTP responses leave details out. */
|
|
126
|
+
details: { resourceType?: string; operation?: string; reason?: string; hooks?: string[] }
|
|
127
|
+
toJSON(): SerializedError & Pick<RestApiBulkWriteUnsupportedError, 'code' | 'type' | 'statusCode' | 'details'>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Reports only how many requested records are missing or outside the caller's scope.
|
|
131
|
+
export class RestApiRecordsNotFoundError extends RestApiResourceError {
|
|
132
|
+
constructor(options?: { resourceType?: string; missing?: number })
|
|
133
|
+
subtype: 'not_found'
|
|
134
|
+
details: { resourceType?: string; missing?: number }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class RestApiBulkInsertIdsError extends RestApiError {
|
|
138
|
+
constructor(options?: { resourceType?: string; expected?: number; verified?: number })
|
|
139
|
+
code: 'REST_API_BULK_INSERT_IDS'
|
|
140
|
+
type: 'rest_api_bulk_insert_ids'
|
|
141
|
+
details: { resourceType?: string; expected?: number; verified?: number }
|
|
142
|
+
toJSON(): SerializedError & Pick<RestApiBulkInsertIdsError, 'code' | 'type' | 'details'>
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export class RestApiLockOutsideTransactionError extends RestApiValidationError {
|
|
146
|
+
constructor(options?: { resourceType?: string; operation?: string })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export class RestApiScopedQueryReadOnlyError extends RestApiError {
|
|
150
|
+
constructor(options?: { resourceType?: string; method?: string })
|
|
151
|
+
code: 'REST_API_SCOPED_QUERY_READ_ONLY'
|
|
152
|
+
type: 'rest_api_scoped_query_read_only'
|
|
153
|
+
details: { resourceType?: string; method?: string }
|
|
154
|
+
toJSON(): SerializedError & Pick<RestApiScopedQueryReadOnlyError, 'code' | 'type' | 'details'>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export class RestApiRowLimitError extends RestApiError {
|
|
158
|
+
constructor(options?: { resourceType?: string; rowLimit?: number; operation?: 'queryAll' | 'lockWhere' })
|
|
159
|
+
code: 'REST_API_ROW_LIMIT'
|
|
160
|
+
type: 'rest_api_row_limit'
|
|
161
|
+
details: { resourceType?: string; rowLimit?: number; operation: 'queryAll' | 'lockWhere' }
|
|
162
|
+
toJSON(): SerializedError & Pick<RestApiRowLimitError, 'code' | 'type' | 'details'>
|
|
163
|
+
}
|
package/types/hook-context.d.ts
CHANGED
|
@@ -169,10 +169,31 @@ export interface HookContextMap<Context extends object = Record<string, unknown>
|
|
|
169
169
|
finishPostRelationship: RelationshipHookContext<Context, 'postRelationship'>
|
|
170
170
|
finishPatchRelationship: RelationshipHookContext<Context, 'patchRelationship'>
|
|
171
171
|
finishDeleteRelationship: RelationshipHookContext<Context, 'deleteRelationship'>
|
|
172
|
+
// Many-row operations: once per call. ids are the affected records' ids as strings
|
|
173
|
+
// (for a create, set from afterDataCallPostMany; undefined for rows an ignore skipped).
|
|
174
|
+
beforeProcessingPostMany: ManyRowHookContext<Context, 'postMany'>
|
|
175
|
+
beforeDataCallPostMany: ManyRowHookContext<Context, 'postMany'>
|
|
176
|
+
afterDataCallPostMany: ManyRowHookContext<Context, 'postMany'>
|
|
177
|
+
beforeProcessingPatchMany: ManyRowHookContext<Context, 'patchMany' | 'patchWhereIds' | 'patchWhere'>
|
|
178
|
+
beforeDataCallPatchMany: ManyRowHookContext<Context, 'patchMany' | 'patchWhereIds' | 'patchWhere'>
|
|
179
|
+
afterDataCallPatchMany: ManyRowHookContext<Context, 'patchMany' | 'patchWhereIds' | 'patchWhere'>
|
|
180
|
+
beforeDataCallDeleteMany: ManyRowHookContext<Context, 'deleteMany' | 'deleteWhere'>
|
|
181
|
+
afterDataCallDeleteMany: ManyRowHookContext<Context, 'deleteMany' | 'deleteWhere'>
|
|
172
182
|
afterCommit: HookContext<Context>
|
|
173
183
|
afterRollback: HookContext<Context>
|
|
174
184
|
}
|
|
175
185
|
|
|
186
|
+
export type ManyRowHookContext<Context extends object = Record<string, unknown>, Method extends string = string> = HookContext<Context> & {
|
|
187
|
+
readonly method: Method
|
|
188
|
+
readonly scopeName: string
|
|
189
|
+
readonly transaction: ManagedTransaction
|
|
190
|
+
// JSON:API documents of the rows, in input order (creates and patches).
|
|
191
|
+
readonly inputRecords?: Array<{ data: { id?: InputResourceId; attributes?: Record<string, unknown>; relationships?: Record<string, unknown> } }>
|
|
192
|
+
// Patches: fields some row sets with a knex.raw value (those values are not in inputRecords).
|
|
193
|
+
readonly rawFields?: string[]
|
|
194
|
+
readonly ids?: Array<string | undefined>
|
|
195
|
+
}
|
|
196
|
+
|
|
176
197
|
export type KnownHookName = keyof HookContextMap
|
|
177
198
|
export type HookArguments<Event extends KnownHookName, Context extends object = Record<string, unknown>> =
|
|
178
199
|
Omit<RuntimeArguments, 'context'> & { context: HookContextMap<Context>[Event] }
|
|
@@ -25,6 +25,8 @@ export interface HttpConnectorOptions {
|
|
|
25
25
|
export interface ResourceVersionOptions {
|
|
26
26
|
// Compilation validates that this names a stored, untransformed string attribute.
|
|
27
27
|
versionField?: string
|
|
28
|
+
// A stored, non-nullable integer advanced by one on every update.
|
|
29
|
+
lockVersionField?: string
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export type CorsOrigin = string | RegExp | ((origin: string) => boolean | Promise<boolean>) | CorsOrigin[]
|
|
@@ -57,8 +59,8 @@ export interface RestApiAnyapiKnexPluginOptions extends RestApiKnexPluginOptions
|
|
|
57
59
|
schemaCheckInterval?: number
|
|
58
60
|
}
|
|
59
61
|
export interface BulkOperationsPluginOptions {
|
|
62
|
+
// Entries one HTTP bulk request may carry (default 1000).
|
|
60
63
|
maxBulkOperations?: number
|
|
61
|
-
defaultAtomic?: boolean
|
|
62
64
|
}
|
|
63
65
|
export interface LabelPluginOptions {
|
|
64
66
|
preferNameFields?: string[]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Knex } from 'knex'
|
|
2
2
|
import type { ManagedTransactionParam } from './transactions.js'
|
|
3
|
+
import type { DataAccessMethods } from './data-access-methods.js'
|
|
3
4
|
import type {
|
|
4
5
|
CollectionResult, DirectResourceId, InputResourceId, InputResourceIdentifier, ResourceFormat,
|
|
5
6
|
SingleResourceResult, WriteResult, WriteReturning
|
|
@@ -24,10 +25,32 @@ export interface QueryParams extends SelectionParams {
|
|
|
24
25
|
page?: PageParams
|
|
25
26
|
}
|
|
26
27
|
export type AggregateFunction = 'count' | 'countDistinct' | 'sum' | 'avg' | 'min' | 'max'
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
type AggregateScalar = string | number | boolean | Date
|
|
29
|
+
// A measure's condition: a value (null tests for null), a list (IN), or operators; every entry must hold.
|
|
30
|
+
export type AggregateCondition = AggregateScalar | null | AggregateScalar[] | {
|
|
31
|
+
eq?: AggregateScalar | null
|
|
32
|
+
ne?: AggregateScalar | null
|
|
33
|
+
gt?: AggregateScalar
|
|
34
|
+
gte?: AggregateScalar
|
|
35
|
+
lt?: AggregateScalar
|
|
36
|
+
lte?: AggregateScalar
|
|
37
|
+
in?: AggregateScalar[]
|
|
38
|
+
notIn?: AggregateScalar[]
|
|
39
|
+
}
|
|
40
|
+
export type AggregateWhere = Record<string, AggregateCondition>
|
|
41
|
+
// One function per measure; only count accepts '*'. where makes the measure conditional (CASE inside the function).
|
|
42
|
+
export type AggregateMeasure = ({ count: string } | { [Function in Exclude<AggregateFunction, 'count'>]: { [Name in Function]: string } }[Exclude<AggregateFunction, 'count'>]) & { where?: AggregateWhere }
|
|
43
|
+
// A date or dateTime field grouped by period; the group value is the period's first day, 'YYYY-MM-DD' (weeks start on Monday).
|
|
44
|
+
export interface AggregateBucket {
|
|
45
|
+
field: string
|
|
46
|
+
bucket: 'day' | 'week' | 'month' | 'year'
|
|
47
|
+
// 'UTC' (default), a fixed offset such as '+10:00', or an IANA name (PostgreSQL; MySQL with time zone tables).
|
|
48
|
+
timezone?: string
|
|
49
|
+
// Output name; default the field name.
|
|
50
|
+
as?: string
|
|
51
|
+
}
|
|
29
52
|
export interface AggregateParams extends TransactionParam {
|
|
30
|
-
groupBy?: string
|
|
53
|
+
groupBy?: Array<string | AggregateBucket>
|
|
31
54
|
measures: Record<string, AggregateMeasure>
|
|
32
55
|
queryParams?: {
|
|
33
56
|
filters?: Record<string, unknown>
|
|
@@ -89,7 +112,7 @@ export interface ResourceCoreMethods<
|
|
|
89
112
|
DefaultFormat extends ResourceFormat = 'plain',
|
|
90
113
|
DefaultReturning extends WriteReturning = 'full',
|
|
91
114
|
Context extends object = object
|
|
92
|
-
> {
|
|
115
|
+
> extends DataAccessMethods<Fields, Input, Type, DefaultFormat, Context> {
|
|
93
116
|
get<Format extends ResourceFormat = DefaultFormat>(
|
|
94
117
|
params: ReadOptions<Format> & { id: DirectResourceId }, context?: Context
|
|
95
118
|
): Promise<SingleResourceResult<Format, Fields, Type>>
|
package/types/row-policy.d.ts
CHANGED
|
@@ -5,7 +5,10 @@ export interface RowPolicyParams<Context extends object = Record<string, unknown
|
|
|
5
5
|
context: Context
|
|
6
6
|
scopeName: string
|
|
7
7
|
tableName?: string
|
|
8
|
+
// 'report', 'bulk-write', 'lock' and 'claim' join the read purposes of earlier releases.
|
|
8
9
|
queryPurpose: string
|
|
10
|
+
// True only for a context made by createSystemContext.
|
|
11
|
+
system: boolean
|
|
9
12
|
db?: StorageDatabase
|
|
10
13
|
isAnyApi: boolean
|
|
11
14
|
storageAdapter?: StorageAdapter | null
|
package/types/runtime.d.ts
CHANGED
|
@@ -33,10 +33,14 @@ export interface RuntimeArguments<Context extends object = Record<string, unknow
|
|
|
33
33
|
runHooks(event: string, context?: Record<string, unknown>): Promise<boolean>
|
|
34
34
|
/** Whether runHooks(event) for this resource, or for scope when given, would call any handler. */
|
|
35
35
|
hasHooks(event: string, scope?: RuntimeResource): boolean
|
|
36
|
+
/** Handlers of these events that would run for this resource (or scope) without { batch: true }. */
|
|
37
|
+
perRecordHooks(events: readonly string[], scope?: RuntimeResource): Array<{ event: string; name: string }>
|
|
36
38
|
}
|
|
37
39
|
export type RuntimeMethod<Context extends object = Record<string, unknown>> = (args: RuntimeArguments<Context>) => unknown
|
|
38
40
|
export type HookPlacement = { beforeFunction?: string; afterFunction?: never } | { beforeFunction?: never; afterFunction?: string }
|
|
39
|
-
|
|
41
|
+
// batch: true marks a per-record handler that many-row operations may skip: its work
|
|
42
|
+
// is done by a batch hook, or does not apply to them. Without it they refuse the resource.
|
|
43
|
+
type HookDefinition<Handler> = Handler | (HookPlacement & { functionName?: string; batch?: boolean; handler: Handler })
|
|
40
44
|
export type RuntimeHook<Context extends object = Record<string, unknown>> = HookDefinition<RuntimeMethod<Context>>
|
|
41
45
|
export type RuntimeHooks<Context extends object = Record<string, unknown>> = {
|
|
42
46
|
[Event in KnownHookName]?: HookDefinition<HookHandler<Event, Context>>
|
|
@@ -60,6 +64,8 @@ export interface RuntimeResourceOptions<Context extends object = Record<string,
|
|
|
60
64
|
sortableFields?: string[]
|
|
61
65
|
defaultSort?: string[]
|
|
62
66
|
versionField?: string
|
|
67
|
+
// A stored integer that every library update of a row advances by one.
|
|
68
|
+
lockVersionField?: string
|
|
63
69
|
rowPolicy?: ResourceRowPolicy<Context>
|
|
64
70
|
autofilter?: ResourceAutoFilter<Context>
|
|
65
71
|
indexes?: Array<{ name?: string; columns: string[]; unique?: boolean }>
|
|
@@ -1,277 +0,0 @@
|
|
|
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).
|
package/types/bulk-methods.d.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import type { ManagedTransaction } from './transactions.js'
|
|
2
|
-
import type { TransactionOutcome } from './errors.js'
|
|
3
|
-
import type { DirectResourceId, InputResourceId, JsonApiResource, PlainResource, ResourceFormat, ResourceIdentifier, WriteReturning } from './representations.js'
|
|
4
|
-
import type { JsonApiWriteDocument, RemovedResourceOptions, ResourceWriteInput, WriteOptions } from './resource-methods.js'
|
|
5
|
-
|
|
6
|
-
export type BulkTransactionOptions<DefaultAtomic extends boolean = true> =
|
|
7
|
-
{ atomic: true; transaction?: ManagedTransaction } |
|
|
8
|
-
{ atomic: false; transaction?: never } |
|
|
9
|
-
{ atomic?: undefined; transaction?: DefaultAtomic extends true ? ManagedTransaction : never }
|
|
10
|
-
export interface BulkError {
|
|
11
|
-
index: number
|
|
12
|
-
id?: DirectResourceId
|
|
13
|
-
status: 'error'
|
|
14
|
-
error: { code: unknown; message: string; details?: unknown; transactionOutcome: TransactionOutcome }
|
|
15
|
-
}
|
|
16
|
-
export interface BulkMeta {
|
|
17
|
-
total: number
|
|
18
|
-
succeeded: number
|
|
19
|
-
failed: number
|
|
20
|
-
atomic: boolean
|
|
21
|
-
}
|
|
22
|
-
export type BulkWriteResult<Format extends ResourceFormat, Returning extends WriteReturning, Fields extends object, Type extends string> =
|
|
23
|
-
{ meta: BulkMeta; errors?: BulkError[] } & (Returning extends 'none'
|
|
24
|
-
? { data?: never }
|
|
25
|
-
: { data: Array<Returning extends 'minimal' ? ResourceIdentifier<Type> : Format extends 'plain' ? PlainResource<Fields> : JsonApiResource<Fields, Type>> })
|
|
26
|
-
export interface BulkDeleteResult {
|
|
27
|
-
meta: BulkMeta & { deleted: DirectResourceId[] }
|
|
28
|
-
errors?: BulkError[]
|
|
29
|
-
}
|
|
30
|
-
export type BulkPostInput<Input extends object, Type extends string> =
|
|
31
|
-
{ data: Array<Partial<Input> & { id?: InputResourceId }>; document?: never } |
|
|
32
|
-
{ data?: never; document: Omit<JsonApiWriteDocument<Input, Type>, 'data'> & { data: JsonApiWriteDocument<Input, Type>['data'][] } }
|
|
33
|
-
export type BulkPatchInput<Input extends object, Type extends string> = {
|
|
34
|
-
id: DirectResourceId
|
|
35
|
-
} & ResourceWriteInput<Input, Type>
|
|
36
|
-
export interface BulkResourceMethods<
|
|
37
|
-
Fields extends object = Record<string, unknown>, Input extends object = Fields,
|
|
38
|
-
Type extends string = string, DefaultFormat extends ResourceFormat = 'plain',
|
|
39
|
-
DefaultReturning extends WriteReturning = 'full', DefaultAtomic extends boolean = true,
|
|
40
|
-
Context extends object = object
|
|
41
|
-
> {
|
|
42
|
-
bulkPost<Format extends ResourceFormat = DefaultFormat, Returning extends WriteReturning = DefaultReturning>(
|
|
43
|
-
params: Omit<WriteOptions<Format, Returning>, 'queryParams'> & BulkTransactionOptions<DefaultAtomic> & BulkPostInput<Input, Type> & {
|
|
44
|
-
expectedVersion?: never
|
|
45
|
-
expectedVersions?: never
|
|
46
|
-
}, context?: Context
|
|
47
|
-
): Promise<BulkWriteResult<Format, Returning, Fields, Type>>
|
|
48
|
-
bulkPatch<Format extends ResourceFormat = DefaultFormat, Returning extends WriteReturning = DefaultReturning>(
|
|
49
|
-
params: Omit<WriteOptions<Format, Returning>, 'queryParams'> & BulkTransactionOptions<DefaultAtomic> & {
|
|
50
|
-
operations: BulkPatchInput<Input, Type>[]
|
|
51
|
-
expectedVersion?: never
|
|
52
|
-
expectedVersions?: string[]
|
|
53
|
-
}, context?: Context
|
|
54
|
-
): Promise<BulkWriteResult<Format, Returning, Fields, Type>>
|
|
55
|
-
bulkDelete(
|
|
56
|
-
params: RemovedResourceOptions & BulkTransactionOptions<DefaultAtomic> & {
|
|
57
|
-
ids: DirectResourceId[]
|
|
58
|
-
format?: ResourceFormat
|
|
59
|
-
expectedVersion?: never
|
|
60
|
-
expectedVersions?: string[]
|
|
61
|
-
}, context?: Context
|
|
62
|
-
): Promise<BulkDeleteResult>
|
|
63
|
-
}
|