json-rest-api 1.0.13 → 1.0.15
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/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +13 -0
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +9 -5
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +8 -8
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- package/tests/virtual-fields.test.js +164 -164
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
import { RestApiValidationError, RestApiResourceError } from '../../lib/rest-api-errors.js'
|
|
1
|
+
import { RestApiValidationError, RestApiResourceError } from '../../lib/rest-api-errors.js'
|
|
2
2
|
|
|
3
3
|
export const BulkOperationsPlugin = {
|
|
4
4
|
name: 'bulk-operations',
|
|
5
5
|
dependencies: ['rest-api'],
|
|
6
6
|
|
|
7
|
-
async install({ api, log, addHook, addScopeMethod, helpers, pluginOptions }) {
|
|
8
|
-
const bulkOptions = pluginOptions || {}
|
|
7
|
+
async install ({ api, log, addHook, addScopeMethod, helpers, pluginOptions }) {
|
|
8
|
+
const bulkOptions = pluginOptions || {}
|
|
9
9
|
const {
|
|
10
10
|
maxBulkOperations = 100,
|
|
11
11
|
defaultAtomic = true,
|
|
12
12
|
batchSize = 100,
|
|
13
13
|
enableOptimizations = true
|
|
14
|
-
} = bulkOptions
|
|
14
|
+
} = bulkOptions
|
|
15
15
|
|
|
16
|
-
log.info('Installing Bulk Operations plugin', { maxBulkOperations, defaultAtomic })
|
|
16
|
+
log.info('Installing Bulk Operations plugin', { maxBulkOperations, defaultAtomic })
|
|
17
17
|
|
|
18
18
|
// Add bulk methods to each scope
|
|
19
19
|
addScopeMethod('bulkPost', async ({ scope, scopeName, params, context, runHooks }) => {
|
|
20
|
-
const { inputRecords, atomic = defaultAtomic } = params
|
|
21
|
-
|
|
20
|
+
const { inputRecords, atomic = defaultAtomic } = params
|
|
21
|
+
|
|
22
22
|
// Validate bulk size
|
|
23
23
|
if (!Array.isArray(inputRecords) || inputRecords.length === 0) {
|
|
24
24
|
throw new RestApiValidationError('Bulk operations require an array of records', {
|
|
25
25
|
fields: ['data'],
|
|
26
26
|
violations: [{ field: 'data', rule: 'required_array', message: 'Must be a non-empty array' }]
|
|
27
|
-
})
|
|
27
|
+
})
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
if (inputRecords.length > maxBulkOperations) {
|
|
31
31
|
throw new RestApiValidationError(`Bulk operations limited to ${maxBulkOperations} records`, {
|
|
32
32
|
fields: ['data'],
|
|
33
|
-
violations: [{
|
|
34
|
-
field: 'data',
|
|
35
|
-
rule: 'max_items',
|
|
36
|
-
message: `Cannot process more than ${maxBulkOperations} records at once`
|
|
33
|
+
violations: [{
|
|
34
|
+
field: 'data',
|
|
35
|
+
rule: 'max_items',
|
|
36
|
+
message: `Cannot process more than ${maxBulkOperations} records at once`
|
|
37
37
|
}]
|
|
38
|
-
})
|
|
38
|
+
})
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const results = []
|
|
42
|
-
const errors = []
|
|
43
|
-
let transaction = null
|
|
41
|
+
const results = []
|
|
42
|
+
const errors = []
|
|
43
|
+
let transaction = null
|
|
44
44
|
|
|
45
45
|
try {
|
|
46
46
|
// Start transaction if atomic mode
|
|
47
47
|
if (atomic && helpers.newTransaction) {
|
|
48
|
-
transaction = await helpers.newTransaction()
|
|
48
|
+
transaction = await helpers.newTransaction()
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Process records in batches
|
|
52
52
|
for (let i = 0; i < inputRecords.length; i += batchSize) {
|
|
53
|
-
const batch = inputRecords.slice(i, i + batchSize)
|
|
54
|
-
|
|
53
|
+
const batch = inputRecords.slice(i, i + batchSize)
|
|
54
|
+
|
|
55
55
|
for (let j = 0; j < batch.length; j++) {
|
|
56
|
-
const recordIndex = i + j
|
|
57
|
-
const inputRecord = batch[j]
|
|
58
|
-
|
|
56
|
+
const recordIndex = i + j
|
|
57
|
+
const inputRecord = batch[j]
|
|
58
|
+
|
|
59
59
|
try {
|
|
60
60
|
// Create individual context for each record
|
|
61
|
-
const recordContext = {
|
|
61
|
+
const recordContext = {
|
|
62
62
|
...context,
|
|
63
63
|
bulkOperation: true,
|
|
64
64
|
bulkIndex: recordIndex
|
|
65
|
-
}
|
|
65
|
+
}
|
|
66
66
|
|
|
67
67
|
// Use the existing post method with transaction
|
|
68
68
|
const result = await scope.post({
|
|
69
69
|
inputRecord: inputRecord.data ? inputRecord : { data: inputRecord },
|
|
70
70
|
transaction
|
|
71
|
-
}, recordContext)
|
|
71
|
+
}, recordContext)
|
|
72
72
|
|
|
73
73
|
results.push({
|
|
74
74
|
index: recordIndex,
|
|
75
75
|
status: 'success',
|
|
76
76
|
data: result.data
|
|
77
|
-
})
|
|
77
|
+
})
|
|
78
78
|
} catch (error) {
|
|
79
79
|
if (atomic) {
|
|
80
80
|
// In atomic mode, rollback and throw
|
|
81
|
-
if (transaction) await transaction.rollback()
|
|
82
|
-
throw error
|
|
81
|
+
if (transaction) await transaction.rollback()
|
|
82
|
+
throw error
|
|
83
83
|
} else {
|
|
84
84
|
// In non-atomic mode, collect errors
|
|
85
85
|
errors.push({
|
|
@@ -90,7 +90,7 @@ export const BulkOperationsPlugin = {
|
|
|
90
90
|
message: error.message,
|
|
91
91
|
details: error.details
|
|
92
92
|
}
|
|
93
|
-
})
|
|
93
|
+
})
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
}
|
|
@@ -98,7 +98,7 @@ export const BulkOperationsPlugin = {
|
|
|
98
98
|
|
|
99
99
|
// Commit transaction if atomic
|
|
100
100
|
if (transaction) {
|
|
101
|
-
await transaction.commit()
|
|
101
|
+
await transaction.commit()
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
// Build response
|
|
@@ -111,51 +111,50 @@ export const BulkOperationsPlugin = {
|
|
|
111
111
|
failed: errors.length,
|
|
112
112
|
atomic
|
|
113
113
|
}
|
|
114
|
-
}
|
|
115
|
-
|
|
114
|
+
}
|
|
116
115
|
} catch (error) {
|
|
117
116
|
// Ensure rollback on error
|
|
118
117
|
if (transaction && !transaction.isCompleted()) {
|
|
119
|
-
await transaction.rollback()
|
|
118
|
+
await transaction.rollback()
|
|
120
119
|
}
|
|
121
|
-
throw error
|
|
120
|
+
throw error
|
|
122
121
|
}
|
|
123
|
-
})
|
|
122
|
+
})
|
|
124
123
|
|
|
125
124
|
addScopeMethod('bulkPatch', async ({ scope, scopeName, params, context, runHooks }) => {
|
|
126
|
-
const { operations, atomic = defaultAtomic } = params
|
|
127
|
-
|
|
125
|
+
const { operations, atomic = defaultAtomic } = params
|
|
126
|
+
|
|
128
127
|
// Validate operations
|
|
129
128
|
if (!Array.isArray(operations) || operations.length === 0) {
|
|
130
129
|
throw new RestApiValidationError('Bulk patch requires an array of operations', {
|
|
131
130
|
fields: ['operations'],
|
|
132
131
|
violations: [{ field: 'operations', rule: 'required_array', message: 'Must be a non-empty array' }]
|
|
133
|
-
})
|
|
132
|
+
})
|
|
134
133
|
}
|
|
135
134
|
|
|
136
135
|
if (operations.length > maxBulkOperations) {
|
|
137
136
|
throw new RestApiValidationError(`Bulk operations limited to ${maxBulkOperations} operations`, {
|
|
138
137
|
fields: ['operations'],
|
|
139
|
-
violations: [{
|
|
140
|
-
field: 'operations',
|
|
141
|
-
rule: 'max_items',
|
|
142
|
-
message: `Cannot process more than ${maxBulkOperations} operations at once`
|
|
138
|
+
violations: [{
|
|
139
|
+
field: 'operations',
|
|
140
|
+
rule: 'max_items',
|
|
141
|
+
message: `Cannot process more than ${maxBulkOperations} operations at once`
|
|
143
142
|
}]
|
|
144
|
-
})
|
|
143
|
+
})
|
|
145
144
|
}
|
|
146
145
|
|
|
147
|
-
const results = []
|
|
148
|
-
const errors = []
|
|
149
|
-
let transaction = null
|
|
146
|
+
const results = []
|
|
147
|
+
const errors = []
|
|
148
|
+
let transaction = null
|
|
150
149
|
|
|
151
150
|
try {
|
|
152
151
|
if (atomic && helpers.newTransaction) {
|
|
153
|
-
transaction = await helpers.newTransaction()
|
|
152
|
+
transaction = await helpers.newTransaction()
|
|
154
153
|
}
|
|
155
154
|
|
|
156
155
|
for (let i = 0; i < operations.length; i++) {
|
|
157
|
-
const operation = operations[i]
|
|
158
|
-
|
|
156
|
+
const operation = operations[i]
|
|
157
|
+
|
|
159
158
|
// Validate operation structure
|
|
160
159
|
if (!operation.id || !operation.data) {
|
|
161
160
|
errors.push({
|
|
@@ -166,42 +165,42 @@ export const BulkOperationsPlugin = {
|
|
|
166
165
|
message: 'Operation must include id and data',
|
|
167
166
|
details: { operation }
|
|
168
167
|
}
|
|
169
|
-
})
|
|
168
|
+
})
|
|
170
169
|
if (atomic) {
|
|
171
|
-
if (transaction) await transaction.rollback()
|
|
170
|
+
if (transaction) await transaction.rollback()
|
|
172
171
|
throw new RestApiValidationError('Invalid operation structure', {
|
|
173
172
|
fields: [`operations[${i}]`],
|
|
174
|
-
violations: [{
|
|
175
|
-
field: `operations[${i}]`,
|
|
176
|
-
rule: 'required_fields',
|
|
177
|
-
message: 'Operation must include id and data'
|
|
173
|
+
violations: [{
|
|
174
|
+
field: `operations[${i}]`,
|
|
175
|
+
rule: 'required_fields',
|
|
176
|
+
message: 'Operation must include id and data'
|
|
178
177
|
}]
|
|
179
|
-
})
|
|
178
|
+
})
|
|
180
179
|
}
|
|
181
|
-
continue
|
|
180
|
+
continue
|
|
182
181
|
}
|
|
183
182
|
|
|
184
183
|
try {
|
|
185
|
-
const recordContext = {
|
|
184
|
+
const recordContext = {
|
|
186
185
|
...context,
|
|
187
186
|
bulkOperation: true,
|
|
188
187
|
bulkIndex: i
|
|
189
|
-
}
|
|
188
|
+
}
|
|
190
189
|
|
|
191
190
|
const result = await scope.patch({
|
|
192
191
|
id: operation.id,
|
|
193
192
|
inputRecord: { data: operation.data },
|
|
194
193
|
transaction
|
|
195
|
-
}, recordContext)
|
|
194
|
+
}, recordContext)
|
|
196
195
|
|
|
197
196
|
// If patch doesn't return full record, fetch it
|
|
198
|
-
let resultData = result.data
|
|
197
|
+
let resultData = result.data
|
|
199
198
|
if (!resultData && result.id) {
|
|
200
199
|
const fetchedRecord = await scope.get({
|
|
201
200
|
id: result.id,
|
|
202
201
|
transaction
|
|
203
|
-
}, recordContext)
|
|
204
|
-
resultData = fetchedRecord.data
|
|
202
|
+
}, recordContext)
|
|
203
|
+
resultData = fetchedRecord.data
|
|
205
204
|
}
|
|
206
205
|
|
|
207
206
|
results.push({
|
|
@@ -209,11 +208,11 @@ export const BulkOperationsPlugin = {
|
|
|
209
208
|
id: operation.id,
|
|
210
209
|
status: 'success',
|
|
211
210
|
data: resultData
|
|
212
|
-
})
|
|
211
|
+
})
|
|
213
212
|
} catch (error) {
|
|
214
213
|
if (atomic) {
|
|
215
|
-
if (transaction) await transaction.rollback()
|
|
216
|
-
throw error
|
|
214
|
+
if (transaction) await transaction.rollback()
|
|
215
|
+
throw error
|
|
217
216
|
} else {
|
|
218
217
|
errors.push({
|
|
219
218
|
index: i,
|
|
@@ -224,13 +223,13 @@ export const BulkOperationsPlugin = {
|
|
|
224
223
|
message: error.message,
|
|
225
224
|
details: error.details
|
|
226
225
|
}
|
|
227
|
-
})
|
|
226
|
+
})
|
|
228
227
|
}
|
|
229
228
|
}
|
|
230
229
|
}
|
|
231
230
|
|
|
232
231
|
if (transaction) {
|
|
233
|
-
await transaction.commit()
|
|
232
|
+
await transaction.commit()
|
|
234
233
|
}
|
|
235
234
|
|
|
236
235
|
return {
|
|
@@ -242,72 +241,71 @@ export const BulkOperationsPlugin = {
|
|
|
242
241
|
failed: errors.length,
|
|
243
242
|
atomic
|
|
244
243
|
}
|
|
245
|
-
}
|
|
246
|
-
|
|
244
|
+
}
|
|
247
245
|
} catch (error) {
|
|
248
246
|
if (transaction && !transaction.isCompleted()) {
|
|
249
|
-
await transaction.rollback()
|
|
247
|
+
await transaction.rollback()
|
|
250
248
|
}
|
|
251
|
-
throw error
|
|
249
|
+
throw error
|
|
252
250
|
}
|
|
253
|
-
})
|
|
251
|
+
})
|
|
254
252
|
|
|
255
253
|
addScopeMethod('bulkDelete', async ({ scope, scopeName, params, context, runHooks }) => {
|
|
256
|
-
const { ids, atomic = defaultAtomic } = params
|
|
257
|
-
|
|
254
|
+
const { ids, atomic = defaultAtomic } = params
|
|
255
|
+
|
|
258
256
|
// Validate IDs
|
|
259
257
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
260
258
|
throw new RestApiValidationError('Bulk delete requires an array of IDs', {
|
|
261
259
|
fields: ['ids'],
|
|
262
260
|
violations: [{ field: 'ids', rule: 'required_array', message: 'Must be a non-empty array' }]
|
|
263
|
-
})
|
|
261
|
+
})
|
|
264
262
|
}
|
|
265
263
|
|
|
266
264
|
if (ids.length > maxBulkOperations) {
|
|
267
265
|
throw new RestApiValidationError(`Bulk operations limited to ${maxBulkOperations} IDs`, {
|
|
268
266
|
fields: ['ids'],
|
|
269
|
-
violations: [{
|
|
270
|
-
field: 'ids',
|
|
271
|
-
rule: 'max_items',
|
|
272
|
-
message: `Cannot process more than ${maxBulkOperations} IDs at once`
|
|
267
|
+
violations: [{
|
|
268
|
+
field: 'ids',
|
|
269
|
+
rule: 'max_items',
|
|
270
|
+
message: `Cannot process more than ${maxBulkOperations} IDs at once`
|
|
273
271
|
}]
|
|
274
|
-
})
|
|
272
|
+
})
|
|
275
273
|
}
|
|
276
274
|
|
|
277
|
-
const results = []
|
|
278
|
-
const errors = []
|
|
279
|
-
let transaction = null
|
|
275
|
+
const results = []
|
|
276
|
+
const errors = []
|
|
277
|
+
let transaction = null
|
|
280
278
|
|
|
281
279
|
try {
|
|
282
280
|
if (atomic && helpers.newTransaction) {
|
|
283
|
-
transaction = await helpers.newTransaction()
|
|
281
|
+
transaction = await helpers.newTransaction()
|
|
284
282
|
}
|
|
285
283
|
|
|
286
284
|
// Process deletes
|
|
287
285
|
for (let i = 0; i < ids.length; i++) {
|
|
288
|
-
const id = ids[i]
|
|
289
|
-
|
|
286
|
+
const id = ids[i]
|
|
287
|
+
|
|
290
288
|
try {
|
|
291
|
-
const recordContext = {
|
|
289
|
+
const recordContext = {
|
|
292
290
|
...context,
|
|
293
291
|
bulkOperation: true,
|
|
294
292
|
bulkIndex: i
|
|
295
|
-
}
|
|
293
|
+
}
|
|
296
294
|
|
|
297
295
|
await scope.delete({
|
|
298
296
|
id,
|
|
299
297
|
transaction
|
|
300
|
-
}, recordContext)
|
|
298
|
+
}, recordContext)
|
|
301
299
|
|
|
302
300
|
results.push({
|
|
303
301
|
index: i,
|
|
304
302
|
id,
|
|
305
303
|
status: 'success'
|
|
306
|
-
})
|
|
304
|
+
})
|
|
307
305
|
} catch (error) {
|
|
308
306
|
if (atomic) {
|
|
309
|
-
if (transaction) await transaction.rollback()
|
|
310
|
-
throw error
|
|
307
|
+
if (transaction) await transaction.rollback()
|
|
308
|
+
throw error
|
|
311
309
|
} else {
|
|
312
310
|
errors.push({
|
|
313
311
|
index: i,
|
|
@@ -318,13 +316,13 @@ export const BulkOperationsPlugin = {
|
|
|
318
316
|
message: error.message,
|
|
319
317
|
details: error.details
|
|
320
318
|
}
|
|
321
|
-
})
|
|
319
|
+
})
|
|
322
320
|
}
|
|
323
321
|
}
|
|
324
322
|
}
|
|
325
323
|
|
|
326
324
|
if (transaction) {
|
|
327
|
-
await transaction.commit()
|
|
325
|
+
await transaction.commit()
|
|
328
326
|
}
|
|
329
327
|
|
|
330
328
|
return {
|
|
@@ -336,85 +334,84 @@ export const BulkOperationsPlugin = {
|
|
|
336
334
|
atomic
|
|
337
335
|
},
|
|
338
336
|
errors: errors.length > 0 ? errors : undefined
|
|
339
|
-
}
|
|
340
|
-
|
|
337
|
+
}
|
|
341
338
|
} catch (error) {
|
|
342
339
|
if (transaction && !transaction.isCompleted()) {
|
|
343
|
-
await transaction.rollback()
|
|
340
|
+
await transaction.rollback()
|
|
344
341
|
}
|
|
345
|
-
throw error
|
|
342
|
+
throw error
|
|
346
343
|
}
|
|
347
|
-
})
|
|
344
|
+
})
|
|
348
345
|
|
|
349
346
|
// Hook into scope creation to add bulk routes
|
|
350
347
|
addHook('afterAddScope', 'bulkOperationsRoutes', {}, async ({ scopeName }) => {
|
|
351
348
|
// Note: context is not available here during route registration
|
|
352
349
|
// The urlPrefix will be calculated per-request in the handler
|
|
353
|
-
const urlPrefix = api.vars.transport?.mountPath || ''
|
|
354
|
-
const scopePath = `${urlPrefix}/${scopeName}
|
|
350
|
+
const urlPrefix = api.vars.transport?.mountPath || ''
|
|
351
|
+
const scopePath = `${urlPrefix}/${scopeName}`
|
|
355
352
|
|
|
356
353
|
// Create route handlers
|
|
357
354
|
const createBulkRouteHandler = (method) => {
|
|
358
355
|
return async ({ context, body, query }) => {
|
|
359
356
|
try {
|
|
360
357
|
// Parse query params for atomic mode override
|
|
361
|
-
const atomic = query?.atomic !== undefined
|
|
362
|
-
? query.atomic === 'true'
|
|
363
|
-
: defaultAtomic
|
|
358
|
+
const atomic = query?.atomic !== undefined
|
|
359
|
+
? query.atomic === 'true'
|
|
360
|
+
: defaultAtomic
|
|
364
361
|
|
|
365
|
-
let params
|
|
362
|
+
let params
|
|
366
363
|
if (method === 'bulkPost') {
|
|
367
364
|
// For bulk create, expect array in data field (JSON:API style)
|
|
368
365
|
params = {
|
|
369
366
|
inputRecords: body?.data || body,
|
|
370
367
|
atomic
|
|
371
|
-
}
|
|
368
|
+
}
|
|
372
369
|
} else if (method === 'bulkPatch') {
|
|
373
370
|
// For bulk update, expect operations array
|
|
374
371
|
params = {
|
|
375
372
|
operations: body?.operations || body,
|
|
376
373
|
atomic
|
|
377
|
-
}
|
|
374
|
+
}
|
|
378
375
|
} else if (method === 'bulkDelete') {
|
|
379
376
|
// For bulk delete, expect IDs array
|
|
380
377
|
params = {
|
|
381
378
|
ids: body?.data || body?.ids || body,
|
|
382
379
|
atomic
|
|
383
|
-
}
|
|
380
|
+
}
|
|
384
381
|
}
|
|
385
382
|
|
|
386
383
|
// Call the scope method
|
|
387
|
-
const result = await api.scopes[scopeName][method](params)
|
|
388
|
-
|
|
389
|
-
return result
|
|
384
|
+
const result = await api.scopes[scopeName][method](params)
|
|
385
|
+
|
|
386
|
+
return result
|
|
390
387
|
} catch (error) {
|
|
391
388
|
// Let transport plugin handle error mapping
|
|
392
|
-
throw error
|
|
389
|
+
throw error
|
|
393
390
|
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
396
393
|
|
|
397
394
|
// Register bulk routes
|
|
398
395
|
await api.addRoute({
|
|
399
396
|
method: 'POST',
|
|
400
397
|
path: `${scopePath}/bulk`,
|
|
401
398
|
handler: createBulkRouteHandler('bulkPost')
|
|
402
|
-
})
|
|
399
|
+
})
|
|
403
400
|
|
|
404
401
|
await api.addRoute({
|
|
405
402
|
method: 'PATCH',
|
|
406
403
|
path: `${scopePath}/bulk`,
|
|
407
404
|
handler: createBulkRouteHandler('bulkPatch')
|
|
408
|
-
})
|
|
405
|
+
})
|
|
409
406
|
|
|
410
407
|
await api.addRoute({
|
|
411
408
|
method: 'DELETE',
|
|
412
409
|
path: `${scopePath}/bulk`,
|
|
413
410
|
handler: createBulkRouteHandler('bulkDelete')
|
|
414
|
-
})
|
|
411
|
+
})
|
|
415
412
|
|
|
416
|
-
log.debug(`Added bulk operation routes for scope: ${scopeName}`)
|
|
417
|
-
})
|
|
413
|
+
log.debug(`Added bulk operation routes for scope: ${scopeName}`)
|
|
414
|
+
})
|
|
418
415
|
|
|
419
416
|
// Add optimized bulk insert for Knex if available
|
|
420
417
|
if (enableOptimizations) {
|
|
@@ -422,40 +419,40 @@ export const BulkOperationsPlugin = {
|
|
|
422
419
|
// Check if we can use optimized path
|
|
423
420
|
if (params.atomic && api.knex?.instance && context.bulkOperation === undefined) {
|
|
424
421
|
// This is the main bulk operation, not individual records
|
|
425
|
-
const { inputRecords } = params
|
|
426
|
-
|
|
422
|
+
const { inputRecords } = params
|
|
423
|
+
|
|
427
424
|
// Transform records for direct insertion
|
|
428
|
-
const recordsToInsert = []
|
|
425
|
+
const recordsToInsert = []
|
|
429
426
|
for (const inputRecord of inputRecords) {
|
|
430
427
|
// Run validation
|
|
431
|
-
const validated = await scope.validateInput({
|
|
432
|
-
inputRecord,
|
|
433
|
-
operation: 'create'
|
|
434
|
-
})
|
|
435
|
-
|
|
428
|
+
const validated = await scope.validateInput({
|
|
429
|
+
inputRecord,
|
|
430
|
+
operation: 'create'
|
|
431
|
+
})
|
|
432
|
+
|
|
436
433
|
// Transform to database format
|
|
437
|
-
const dbRecord = await scope.transformForDatabase({
|
|
434
|
+
const dbRecord = await scope.transformForDatabase({
|
|
438
435
|
record: validated,
|
|
439
436
|
operation: 'create'
|
|
440
|
-
})
|
|
441
|
-
|
|
442
|
-
recordsToInsert.push(dbRecord)
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
recordsToInsert.push(dbRecord)
|
|
443
440
|
}
|
|
444
441
|
|
|
445
442
|
// Perform bulk insert
|
|
446
|
-
const knex = api.knex.instance
|
|
447
|
-
const tableName = scope.vars.schemaInfo.tableName
|
|
448
|
-
|
|
443
|
+
const knex = api.knex.instance
|
|
444
|
+
const tableName = scope.vars.schemaInfo.tableName
|
|
445
|
+
|
|
449
446
|
try {
|
|
450
447
|
const inserted = await knex(tableName)
|
|
451
448
|
.insert(recordsToInsert)
|
|
452
|
-
.returning('*')
|
|
453
|
-
|
|
449
|
+
.returning('*')
|
|
450
|
+
|
|
454
451
|
// Transform back to API format
|
|
455
|
-
const results = []
|
|
452
|
+
const results = []
|
|
456
453
|
for (const record of inserted) {
|
|
457
|
-
const apiRecord = await scope.transformFromDatabase({ record })
|
|
458
|
-
results.push(apiRecord)
|
|
454
|
+
const apiRecord = await scope.transformFromDatabase({ record })
|
|
455
|
+
results.push(apiRecord)
|
|
459
456
|
}
|
|
460
457
|
|
|
461
458
|
// Return optimized result
|
|
@@ -469,15 +466,15 @@ export const BulkOperationsPlugin = {
|
|
|
469
466
|
optimized: true
|
|
470
467
|
},
|
|
471
468
|
skipDefault: true // Tell the default handler to skip
|
|
472
|
-
}
|
|
469
|
+
}
|
|
473
470
|
} catch (error) {
|
|
474
471
|
// Fall back to default handling
|
|
475
|
-
log.warn('Optimized bulk insert failed, falling back to default', { error: error.message })
|
|
472
|
+
log.warn('Optimized bulk insert failed, falling back to default', { error: error.message })
|
|
476
473
|
}
|
|
477
474
|
}
|
|
478
|
-
})
|
|
475
|
+
})
|
|
479
476
|
}
|
|
480
477
|
|
|
481
|
-
log.info('Bulk Operations plugin installed successfully')
|
|
478
|
+
log.info('Bulk Operations plugin installed successfully')
|
|
482
479
|
}
|
|
483
|
-
}
|
|
480
|
+
}
|