json-rest-api 1.0.29 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +634 -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 +100 -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 +3827 -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 +36 -0
- package/index.js +1 -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 +64 -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 +213 -164
- 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 +25 -16
- 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 +116 -2485
- 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/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 +53 -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
|
@@ -1,23 +1,83 @@
|
|
|
1
1
|
import { RestApiValidationError } from '../../lib/rest-api-errors.js'
|
|
2
|
+
import { beginWriteTransaction, commitTransaction, onTransactionFinished, rollbackAfterError, withWriteOutcome } from '../../lib/error-context.js'
|
|
3
|
+
import { rejectRemovedOptions, resolveFormat, resolveReturning } from './lib/querying-writing/response-options.js'
|
|
4
|
+
import { selectWriteInput } from './lib/querying-writing/request-contracts.js'
|
|
5
|
+
|
|
6
|
+
function validateAtomic (atomic, field = 'atomic') {
|
|
7
|
+
if (typeof atomic !== 'boolean') {
|
|
8
|
+
throw new RestApiValidationError(`${field} must be a boolean`, { fields: [field] })
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function validateTransactionMode (atomic, transaction) {
|
|
13
|
+
if (!atomic && transaction) {
|
|
14
|
+
throw new RestApiValidationError('A caller transaction requires atomic bulk operations', { fields: ['transaction'] })
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveBulkVersions (params, records, schemaInfo) {
|
|
19
|
+
if (params.expectedVersion !== undefined) {
|
|
20
|
+
throw new RestApiValidationError('Bulk writes require expectedVersions aligned with the batch', { fields: ['expectedVersion'] })
|
|
21
|
+
}
|
|
22
|
+
const versions = params.expectedVersions
|
|
23
|
+
if (versions === undefined) return undefined
|
|
24
|
+
if (!records || !schemaInfo?.versionField || !Array.isArray(versions) || versions.length !== records.length ||
|
|
25
|
+
Array.from(versions).some(token => typeof token !== 'string' || !token.length || token.length > 128)) {
|
|
26
|
+
throw new RestApiValidationError('expectedVersions requires a versioned update/delete batch with one non-empty token of at most 128 characters per item', { fields: ['expectedVersions'] })
|
|
27
|
+
}
|
|
28
|
+
return versions.slice()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function retainChildCleanup (context, recordContext, bulkIndex) {
|
|
32
|
+
let copiedErrors = 0
|
|
33
|
+
let copiedUploads = []
|
|
34
|
+
const collect = () => {
|
|
35
|
+
const errors = recordContext.cleanupErrors || []
|
|
36
|
+
if (errors.length > copiedErrors) {
|
|
37
|
+
const entries = context.cleanupErrors ||= []
|
|
38
|
+
for (const entry of errors.slice(copiedErrors)) entries.push({ ...entry, bulkIndex })
|
|
39
|
+
copiedErrors = errors.length
|
|
40
|
+
}
|
|
41
|
+
if (copiedUploads.length) context.fileHandlingUploads = context.fileHandlingUploads.filter(entry => !copiedUploads.includes(entry))
|
|
42
|
+
copiedUploads = (recordContext.fileHandlingUploads || []).map(entry => ({ ...entry, bulkIndex }))
|
|
43
|
+
if (copiedUploads.length) (context.fileHandlingUploads ||= []).push(...copiedUploads)
|
|
44
|
+
}
|
|
45
|
+
collect()
|
|
46
|
+
onTransactionFinished(recordContext.transaction, collect)
|
|
47
|
+
}
|
|
2
48
|
|
|
3
49
|
export const BulkOperationsPlugin = {
|
|
4
50
|
name: 'bulk-operations',
|
|
5
51
|
dependencies: ['rest-api'],
|
|
6
52
|
|
|
7
|
-
async install ({ api, log, addHook,
|
|
53
|
+
async install ({ api, log, addHook, addResourceMethod, helpers, pluginOptions }) {
|
|
8
54
|
const bulkOptions = pluginOptions || {}
|
|
9
55
|
const {
|
|
10
56
|
maxBulkOperations = 100,
|
|
11
|
-
defaultAtomic = true
|
|
12
|
-
batchSize = 100,
|
|
13
|
-
enableOptimizations = true
|
|
57
|
+
defaultAtomic = true
|
|
14
58
|
} = bulkOptions
|
|
59
|
+
const unknown = Object.keys(bulkOptions).filter(name => !['maxBulkOperations', 'defaultAtomic'].includes(name))
|
|
60
|
+
if (unknown.length) {
|
|
61
|
+
throw new RestApiValidationError(`Unsupported bulk options: ${unknown.join(', ')}. Pass maxBulkOperations and defaultAtomic directly.`, { fields: unknown })
|
|
62
|
+
}
|
|
63
|
+
if (!Number.isSafeInteger(maxBulkOperations) || maxBulkOperations < 1) {
|
|
64
|
+
throw new RestApiValidationError('maxBulkOperations must be a positive safe integer', { fields: ['maxBulkOperations'] })
|
|
65
|
+
}
|
|
66
|
+
validateAtomic(defaultAtomic, 'defaultAtomic')
|
|
15
67
|
|
|
16
68
|
log.info('Installing Bulk Operations plugin', { maxBulkOperations, defaultAtomic })
|
|
17
69
|
|
|
18
70
|
// Add bulk methods to each scope
|
|
19
|
-
|
|
20
|
-
const {
|
|
71
|
+
addResourceMethod('bulkPost', withWriteOutcome(async ({ scope, vars, params, context }) => {
|
|
72
|
+
const { atomic = defaultAtomic } = params
|
|
73
|
+
rejectRemovedOptions(params)
|
|
74
|
+
const inputKey = selectWriteInput(params)
|
|
75
|
+
const inputRecords = inputKey === 'document' ? params.document.data : params.data
|
|
76
|
+
validateAtomic(atomic)
|
|
77
|
+
validateTransactionMode(atomic, params.transaction)
|
|
78
|
+
resolveBulkVersions(params)
|
|
79
|
+
const format = resolveFormat(params.format, vars.format)
|
|
80
|
+
const returning = resolveReturning(params.returning, vars.returning)
|
|
21
81
|
|
|
22
82
|
// Validate bulk size
|
|
23
83
|
if (!Array.isArray(inputRecords) || inputRecords.length === 0) {
|
|
@@ -41,69 +101,65 @@ export const BulkOperationsPlugin = {
|
|
|
41
101
|
const results = []
|
|
42
102
|
const errors = []
|
|
43
103
|
let transaction = null
|
|
104
|
+
const ownsTransaction = !params.transaction
|
|
44
105
|
|
|
45
106
|
try {
|
|
46
107
|
// Start transaction if atomic mode
|
|
47
|
-
if (atomic
|
|
48
|
-
transaction = await helpers.newTransaction
|
|
108
|
+
if (atomic) {
|
|
109
|
+
transaction = await beginWriteTransaction(context, params.transaction, helpers.newTransaction)
|
|
49
110
|
}
|
|
50
111
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
inputRecord: inputRecord.data ? inputRecord : { data: inputRecord },
|
|
70
|
-
transaction
|
|
71
|
-
}, recordContext)
|
|
112
|
+
for (const [recordIndex, inputRecord] of inputRecords.entries()) {
|
|
113
|
+
const recordContext = {
|
|
114
|
+
...context,
|
|
115
|
+
cleanupErrors: undefined,
|
|
116
|
+
fileHandlingUploads: undefined,
|
|
117
|
+
bulkOperation: true,
|
|
118
|
+
bulkIndex: recordIndex
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
// Use the existing post method with transaction
|
|
122
|
+
const result = await scope.post({
|
|
123
|
+
...(inputKey === 'document'
|
|
124
|
+
? { document: { ...params.document, data: inputRecord } }
|
|
125
|
+
: { data: inputRecord }),
|
|
126
|
+
transaction,
|
|
127
|
+
format,
|
|
128
|
+
returning
|
|
129
|
+
}, recordContext)
|
|
72
130
|
|
|
73
|
-
|
|
131
|
+
results.push({
|
|
132
|
+
index: recordIndex,
|
|
133
|
+
status: 'success',
|
|
134
|
+
data: format === 'jsonapi' ? result?.data : result
|
|
135
|
+
})
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (atomic) {
|
|
138
|
+
throw error
|
|
139
|
+
} else {
|
|
140
|
+
// In non-atomic mode, collect errors
|
|
141
|
+
errors.push({
|
|
74
142
|
index: recordIndex,
|
|
75
|
-
status: '
|
|
76
|
-
|
|
143
|
+
status: 'error',
|
|
144
|
+
error: {
|
|
145
|
+
code: error.code || 'UNKNOWN_ERROR',
|
|
146
|
+
message: error.message,
|
|
147
|
+
details: error.details,
|
|
148
|
+
transactionOutcome: error.transactionOutcome
|
|
149
|
+
}
|
|
77
150
|
})
|
|
78
|
-
} catch (error) {
|
|
79
|
-
if (atomic) {
|
|
80
|
-
// In atomic mode, rollback and throw
|
|
81
|
-
if (transaction) await transaction.rollback()
|
|
82
|
-
throw error
|
|
83
|
-
} else {
|
|
84
|
-
// In non-atomic mode, collect errors
|
|
85
|
-
errors.push({
|
|
86
|
-
index: recordIndex,
|
|
87
|
-
status: 'error',
|
|
88
|
-
error: {
|
|
89
|
-
code: error.code || 'UNKNOWN_ERROR',
|
|
90
|
-
message: error.message,
|
|
91
|
-
details: error.details
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
}
|
|
95
151
|
}
|
|
96
|
-
}
|
|
152
|
+
} finally { retainChildCleanup(context, recordContext, recordIndex) }
|
|
97
153
|
}
|
|
98
154
|
|
|
99
155
|
// Commit transaction if atomic
|
|
100
|
-
if (transaction) {
|
|
101
|
-
await transaction
|
|
156
|
+
if (transaction && ownsTransaction) {
|
|
157
|
+
await commitTransaction(transaction, context)
|
|
102
158
|
}
|
|
103
159
|
|
|
104
160
|
// Build response
|
|
105
161
|
return {
|
|
106
|
-
|
|
162
|
+
...(returning === 'none' ? {} : { data: results.map(r => r.data) }),
|
|
107
163
|
errors: errors.length > 0 ? errors : undefined,
|
|
108
164
|
meta: {
|
|
109
165
|
total: inputRecords.length,
|
|
@@ -113,16 +169,18 @@ export const BulkOperationsPlugin = {
|
|
|
113
169
|
}
|
|
114
170
|
}
|
|
115
171
|
} catch (error) {
|
|
116
|
-
|
|
117
|
-
if (transaction && !transaction.isCompleted()) {
|
|
118
|
-
await transaction.rollback()
|
|
119
|
-
}
|
|
172
|
+
await rollbackAfterError(error, context, ownsTransaction ? transaction : null)
|
|
120
173
|
throw error
|
|
121
174
|
}
|
|
122
|
-
})
|
|
175
|
+
}))
|
|
123
176
|
|
|
124
|
-
|
|
177
|
+
addResourceMethod('bulkPatch', withWriteOutcome(async ({ scope, vars, params, context }) => {
|
|
125
178
|
const { operations, atomic = defaultAtomic } = params
|
|
179
|
+
rejectRemovedOptions(params)
|
|
180
|
+
validateAtomic(atomic)
|
|
181
|
+
validateTransactionMode(atomic, params.transaction)
|
|
182
|
+
const format = resolveFormat(params.format, vars.format)
|
|
183
|
+
const returning = resolveReturning(params.returning, vars.returning)
|
|
126
184
|
|
|
127
185
|
// Validate operations
|
|
128
186
|
if (!Array.isArray(operations) || operations.length === 0) {
|
|
@@ -143,76 +201,69 @@ export const BulkOperationsPlugin = {
|
|
|
143
201
|
})
|
|
144
202
|
}
|
|
145
203
|
|
|
204
|
+
const expectedVersions = resolveBulkVersions(params, operations, vars.schemaInfo)
|
|
146
205
|
const results = []
|
|
147
206
|
const errors = []
|
|
148
207
|
let transaction = null
|
|
208
|
+
const ownsTransaction = !params.transaction
|
|
149
209
|
|
|
150
210
|
try {
|
|
151
|
-
if (atomic
|
|
152
|
-
transaction = await helpers.newTransaction
|
|
211
|
+
if (atomic) {
|
|
212
|
+
transaction = await beginWriteTransaction(context, params.transaction, helpers.newTransaction)
|
|
153
213
|
}
|
|
154
214
|
|
|
155
215
|
for (let i = 0; i < operations.length; i++) {
|
|
156
216
|
const operation = operations[i]
|
|
157
217
|
|
|
158
218
|
// Validate operation structure
|
|
159
|
-
if (!operation.id ||
|
|
219
|
+
if (!operation || operation.id === undefined || operation.id === null) {
|
|
160
220
|
errors.push({
|
|
161
221
|
index: i,
|
|
162
222
|
status: 'error',
|
|
163
223
|
error: {
|
|
164
224
|
code: 'INVALID_OPERATION',
|
|
165
|
-
message: 'Operation must include id and data',
|
|
166
|
-
details: { operation }
|
|
225
|
+
message: 'Operation must include id and exactly one of data or document',
|
|
226
|
+
details: { operation },
|
|
227
|
+
transactionOutcome: 'none'
|
|
167
228
|
}
|
|
168
229
|
})
|
|
169
230
|
if (atomic) {
|
|
170
|
-
if (transaction) await transaction.rollback()
|
|
171
231
|
throw new RestApiValidationError('Invalid operation structure', {
|
|
172
232
|
fields: [`operations[${i}]`],
|
|
173
233
|
violations: [{
|
|
174
234
|
field: `operations[${i}]`,
|
|
175
235
|
rule: 'required_fields',
|
|
176
|
-
message: 'Operation must include id and data'
|
|
236
|
+
message: 'Operation must include id and exactly one of data or document'
|
|
177
237
|
}]
|
|
178
238
|
})
|
|
179
239
|
}
|
|
180
240
|
continue
|
|
181
241
|
}
|
|
182
242
|
|
|
243
|
+
const recordContext = {
|
|
244
|
+
...context,
|
|
245
|
+
cleanupErrors: undefined,
|
|
246
|
+
fileHandlingUploads: undefined,
|
|
247
|
+
bulkOperation: true,
|
|
248
|
+
bulkIndex: i
|
|
249
|
+
}
|
|
183
250
|
try {
|
|
184
|
-
const recordContext = {
|
|
185
|
-
...context,
|
|
186
|
-
bulkOperation: true,
|
|
187
|
-
bulkIndex: i
|
|
188
|
-
}
|
|
189
|
-
|
|
190
251
|
const result = await scope.patch({
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
transaction
|
|
252
|
+
...operation,
|
|
253
|
+
expectedVersion: expectedVersions?.[i],
|
|
254
|
+
transaction,
|
|
255
|
+
format,
|
|
256
|
+
returning
|
|
194
257
|
}, recordContext)
|
|
195
258
|
|
|
196
|
-
// If patch doesn't return a full record, fetch the updated record.
|
|
197
|
-
const resultId = result?.data?.id || result?.id || operation.id
|
|
198
|
-
let resultData = result?.data
|
|
199
|
-
if (!resultData && resultId) {
|
|
200
|
-
const fetchedRecord = await scope.get({
|
|
201
|
-
id: resultId,
|
|
202
|
-
transaction
|
|
203
|
-
}, recordContext)
|
|
204
|
-
resultData = fetchedRecord.data
|
|
205
|
-
}
|
|
206
|
-
|
|
207
259
|
results.push({
|
|
208
260
|
index: i,
|
|
209
261
|
id: operation.id,
|
|
210
262
|
status: 'success',
|
|
211
|
-
data:
|
|
263
|
+
data: format === 'jsonapi' ? result?.data : result
|
|
212
264
|
})
|
|
213
265
|
} catch (error) {
|
|
214
266
|
if (atomic) {
|
|
215
|
-
if (transaction) await transaction.rollback()
|
|
216
267
|
throw error
|
|
217
268
|
} else {
|
|
218
269
|
errors.push({
|
|
@@ -222,19 +273,20 @@ export const BulkOperationsPlugin = {
|
|
|
222
273
|
error: {
|
|
223
274
|
code: error.code || 'UNKNOWN_ERROR',
|
|
224
275
|
message: error.message,
|
|
225
|
-
details: error.details
|
|
276
|
+
details: error.details,
|
|
277
|
+
transactionOutcome: error.transactionOutcome
|
|
226
278
|
}
|
|
227
279
|
})
|
|
228
280
|
}
|
|
229
|
-
}
|
|
281
|
+
} finally { retainChildCleanup(context, recordContext, i) }
|
|
230
282
|
}
|
|
231
283
|
|
|
232
|
-
if (transaction) {
|
|
233
|
-
await transaction
|
|
284
|
+
if (transaction && ownsTransaction) {
|
|
285
|
+
await commitTransaction(transaction, context)
|
|
234
286
|
}
|
|
235
287
|
|
|
236
288
|
return {
|
|
237
|
-
|
|
289
|
+
...(returning === 'none' ? {} : { data: results.map(r => r.data) }),
|
|
238
290
|
errors: errors.length > 0 ? errors : undefined,
|
|
239
291
|
meta: {
|
|
240
292
|
total: operations.length,
|
|
@@ -244,15 +296,17 @@ export const BulkOperationsPlugin = {
|
|
|
244
296
|
}
|
|
245
297
|
}
|
|
246
298
|
} catch (error) {
|
|
247
|
-
|
|
248
|
-
await transaction.rollback()
|
|
249
|
-
}
|
|
299
|
+
await rollbackAfterError(error, context, ownsTransaction ? transaction : null)
|
|
250
300
|
throw error
|
|
251
301
|
}
|
|
252
|
-
})
|
|
302
|
+
}))
|
|
253
303
|
|
|
254
|
-
|
|
304
|
+
addResourceMethod('bulkDelete', withWriteOutcome(async ({ scope, vars, params, context }) => {
|
|
255
305
|
const { ids, atomic = defaultAtomic } = params
|
|
306
|
+
rejectRemovedOptions(params)
|
|
307
|
+
validateAtomic(atomic)
|
|
308
|
+
validateTransactionMode(atomic, params.transaction)
|
|
309
|
+
if (params.format !== undefined) resolveFormat(params.format)
|
|
256
310
|
|
|
257
311
|
// Validate IDs
|
|
258
312
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
@@ -273,28 +327,32 @@ export const BulkOperationsPlugin = {
|
|
|
273
327
|
})
|
|
274
328
|
}
|
|
275
329
|
|
|
330
|
+
const expectedVersions = resolveBulkVersions(params, ids, vars.schemaInfo)
|
|
276
331
|
const results = []
|
|
277
332
|
const errors = []
|
|
278
333
|
let transaction = null
|
|
334
|
+
const ownsTransaction = !params.transaction
|
|
279
335
|
|
|
280
336
|
try {
|
|
281
|
-
if (atomic
|
|
282
|
-
transaction = await helpers.newTransaction
|
|
337
|
+
if (atomic) {
|
|
338
|
+
transaction = await beginWriteTransaction(context, params.transaction, helpers.newTransaction)
|
|
283
339
|
}
|
|
284
340
|
|
|
285
341
|
// Process deletes
|
|
286
342
|
for (let i = 0; i < ids.length; i++) {
|
|
287
343
|
const id = ids[i]
|
|
288
344
|
|
|
345
|
+
const recordContext = {
|
|
346
|
+
...context,
|
|
347
|
+
cleanupErrors: undefined,
|
|
348
|
+
fileHandlingUploads: undefined,
|
|
349
|
+
bulkOperation: true,
|
|
350
|
+
bulkIndex: i
|
|
351
|
+
}
|
|
289
352
|
try {
|
|
290
|
-
const recordContext = {
|
|
291
|
-
...context,
|
|
292
|
-
bulkOperation: true,
|
|
293
|
-
bulkIndex: i
|
|
294
|
-
}
|
|
295
|
-
|
|
296
353
|
await scope.delete({
|
|
297
354
|
id,
|
|
355
|
+
expectedVersion: expectedVersions?.[i],
|
|
298
356
|
transaction
|
|
299
357
|
}, recordContext)
|
|
300
358
|
|
|
@@ -305,7 +363,6 @@ export const BulkOperationsPlugin = {
|
|
|
305
363
|
})
|
|
306
364
|
} catch (error) {
|
|
307
365
|
if (atomic) {
|
|
308
|
-
if (transaction) await transaction.rollback()
|
|
309
366
|
throw error
|
|
310
367
|
} else {
|
|
311
368
|
errors.push({
|
|
@@ -315,15 +372,16 @@ export const BulkOperationsPlugin = {
|
|
|
315
372
|
error: {
|
|
316
373
|
code: error.code || 'UNKNOWN_ERROR',
|
|
317
374
|
message: error.message,
|
|
318
|
-
details: error.details
|
|
375
|
+
details: error.details,
|
|
376
|
+
transactionOutcome: error.transactionOutcome
|
|
319
377
|
}
|
|
320
378
|
})
|
|
321
379
|
}
|
|
322
|
-
}
|
|
380
|
+
} finally { retainChildCleanup(context, recordContext, i) }
|
|
323
381
|
}
|
|
324
382
|
|
|
325
|
-
if (transaction) {
|
|
326
|
-
await transaction
|
|
383
|
+
if (transaction && ownsTransaction) {
|
|
384
|
+
await commitTransaction(transaction, context)
|
|
327
385
|
}
|
|
328
386
|
|
|
329
387
|
return {
|
|
@@ -337,23 +395,24 @@ export const BulkOperationsPlugin = {
|
|
|
337
395
|
errors: errors.length > 0 ? errors : undefined
|
|
338
396
|
}
|
|
339
397
|
} catch (error) {
|
|
340
|
-
|
|
341
|
-
await transaction.rollback()
|
|
342
|
-
}
|
|
398
|
+
await rollbackAfterError(error, context, ownsTransaction ? transaction : null)
|
|
343
399
|
throw error
|
|
344
400
|
}
|
|
345
|
-
})
|
|
401
|
+
}))
|
|
346
402
|
|
|
347
403
|
// Hook into scope creation to add bulk routes
|
|
348
|
-
addHook('
|
|
349
|
-
// Note: context is not available here during route registration
|
|
350
|
-
// The urlPrefix will be calculated per-request in the handler
|
|
404
|
+
addHook('resource:added', 'bulkOperationsRoutes', { beforeFunction: 'registerScopeRoutes' }, async ({ context: { scopeName } }) => {
|
|
351
405
|
const urlPrefix = api.vars.transport?.mountPath || ''
|
|
352
406
|
const scopePath = `${urlPrefix}/${scopeName}`
|
|
353
407
|
|
|
354
408
|
// Create route handlers
|
|
355
409
|
const createBulkRouteHandler = (method) => {
|
|
356
|
-
return async ({ context, body,
|
|
410
|
+
return async ({ context, body, queryString }) => {
|
|
411
|
+
const query = Object.fromEntries(new URLSearchParams(queryString))
|
|
412
|
+
rejectRemovedOptions(query)
|
|
413
|
+
if (query.atomic !== undefined && query.atomic !== 'true' && query.atomic !== 'false') {
|
|
414
|
+
throw new RestApiValidationError('atomic must be true or false', { fields: ['atomic'] })
|
|
415
|
+
}
|
|
357
416
|
// Parse query params for atomic mode override
|
|
358
417
|
const atomic = query?.atomic !== undefined
|
|
359
418
|
? query.atomic === 'true'
|
|
@@ -363,13 +422,16 @@ export const BulkOperationsPlugin = {
|
|
|
363
422
|
if (method === 'bulkPost') {
|
|
364
423
|
// For bulk create, expect array in data field (JSON:API style)
|
|
365
424
|
params = {
|
|
366
|
-
|
|
425
|
+
document: body,
|
|
367
426
|
atomic
|
|
368
427
|
}
|
|
369
428
|
} else if (method === 'bulkPatch') {
|
|
370
429
|
// For bulk update, expect operations array
|
|
430
|
+
const operations = body?.operations || body
|
|
371
431
|
params = {
|
|
372
|
-
operations:
|
|
432
|
+
operations: Array.isArray(operations)
|
|
433
|
+
? operations.map(operation => ({ id: operation?.id, document: { data: operation?.data } }))
|
|
434
|
+
: operations,
|
|
373
435
|
atomic
|
|
374
436
|
}
|
|
375
437
|
} else if (method === 'bulkDelete') {
|
|
@@ -380,7 +442,14 @@ export const BulkOperationsPlugin = {
|
|
|
380
442
|
}
|
|
381
443
|
}
|
|
382
444
|
|
|
383
|
-
|
|
445
|
+
const result = await api.resources[scopeName][method]({
|
|
446
|
+
...params,
|
|
447
|
+
expectedVersion: body?.expectedVersion,
|
|
448
|
+
expectedVersions: body?.expectedVersions,
|
|
449
|
+
format: 'jsonapi',
|
|
450
|
+
returning: 'full'
|
|
451
|
+
}, context)
|
|
452
|
+
return { statusCode: method === 'bulkPost' ? 201 : 200, body: result }
|
|
384
453
|
}
|
|
385
454
|
}
|
|
386
455
|
|
|
@@ -406,68 +475,6 @@ export const BulkOperationsPlugin = {
|
|
|
406
475
|
log.debug(`Added bulk operation routes for scope: ${scopeName}`)
|
|
407
476
|
})
|
|
408
477
|
|
|
409
|
-
// Add optimized bulk insert for Knex if available
|
|
410
|
-
if (enableOptimizations) {
|
|
411
|
-
addHook('beforeBulkPost', 'optimizedBulkInsert', {}, async ({ scope, params, context }) => {
|
|
412
|
-
// Check if we can use optimized path
|
|
413
|
-
if (params.atomic && api.knex?.instance && context.bulkOperation === undefined) {
|
|
414
|
-
// This is the main bulk operation, not individual records
|
|
415
|
-
const { inputRecords } = params
|
|
416
|
-
|
|
417
|
-
// Transform records for direct insertion
|
|
418
|
-
const recordsToInsert = []
|
|
419
|
-
for (const inputRecord of inputRecords) {
|
|
420
|
-
// Run validation
|
|
421
|
-
const validated = await scope.validateInput({
|
|
422
|
-
inputRecord,
|
|
423
|
-
operation: 'create'
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
// Transform to database format
|
|
427
|
-
const dbRecord = await scope.transformForDatabase({
|
|
428
|
-
record: validated,
|
|
429
|
-
operation: 'create'
|
|
430
|
-
})
|
|
431
|
-
|
|
432
|
-
recordsToInsert.push(dbRecord)
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Perform bulk insert
|
|
436
|
-
const knex = api.knex.instance
|
|
437
|
-
const tableName = scope.vars.schemaInfo.tableName
|
|
438
|
-
|
|
439
|
-
try {
|
|
440
|
-
const inserted = await knex(tableName)
|
|
441
|
-
.insert(recordsToInsert)
|
|
442
|
-
.returning('*')
|
|
443
|
-
|
|
444
|
-
// Transform back to API format
|
|
445
|
-
const results = []
|
|
446
|
-
for (const record of inserted) {
|
|
447
|
-
const apiRecord = await scope.transformFromDatabase({ record })
|
|
448
|
-
results.push(apiRecord)
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Return optimized result
|
|
452
|
-
return {
|
|
453
|
-
data: results,
|
|
454
|
-
meta: {
|
|
455
|
-
total: inputRecords.length,
|
|
456
|
-
succeeded: results.length,
|
|
457
|
-
failed: 0,
|
|
458
|
-
atomic: true,
|
|
459
|
-
optimized: true
|
|
460
|
-
},
|
|
461
|
-
skipDefault: true // Tell the default handler to skip
|
|
462
|
-
}
|
|
463
|
-
} catch (error) {
|
|
464
|
-
// Fall back to default handling
|
|
465
|
-
log.warn('Optimized bulk insert failed, falling back to default', { error: error.message })
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
})
|
|
469
|
-
}
|
|
470
|
-
|
|
471
478
|
log.info('Bulk Operations plugin installed successfully')
|
|
472
479
|
}
|
|
473
480
|
}
|