hide-a-bed 5.2.7 → 6.0.0-beta.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/.prettierrc +7 -0
- package/README.md +270 -218
- package/dist/cjs/index.cjs +1952 -0
- package/dist/esm/index.mjs +1898 -0
- package/docs/.nojekyll +1 -0
- package/docs/assets/hierarchy.js +1 -0
- package/docs/assets/highlight.css +113 -0
- package/docs/assets/icons.js +18 -0
- package/docs/assets/icons.svg +1 -0
- package/docs/assets/main.js +60 -0
- package/docs/assets/navigation.js +1 -0
- package/docs/assets/search.js +1 -0
- package/docs/assets/style.css +1633 -0
- package/docs/classes/QueryBuilder.html +42 -0
- package/docs/functions/bindConfig.html +4 -0
- package/docs/functions/bulkGet.html +14 -0
- package/docs/functions/bulkGetDictionary.html +10 -0
- package/docs/functions/bulkRemove.html +12 -0
- package/docs/functions/bulkRemoveMap.html +11 -0
- package/docs/functions/bulkSave.html +10 -0
- package/docs/functions/bulkSaveTransaction.html +23 -0
- package/docs/functions/createLock.html +7 -0
- package/docs/functions/createQuery.html +1 -0
- package/docs/functions/get.html +1 -0
- package/docs/functions/getAtRev.html +1 -0
- package/docs/functions/getDBInfo.html +10 -0
- package/docs/functions/patch.html +8 -0
- package/docs/functions/patchDangerously.html +9 -0
- package/docs/functions/put.html +1 -0
- package/docs/functions/query.html +15 -0
- package/docs/functions/queryStream.html +6 -0
- package/docs/functions/remove.html +1 -0
- package/docs/functions/removeLock.html +6 -0
- package/docs/functions/watchDocs.html +9 -0
- package/docs/functions/withRetry.html +6 -0
- package/docs/hierarchy.html +1 -0
- package/docs/index.html +483 -0
- package/docs/interfaces/NetworkError.html +6 -0
- package/docs/interfaces/NotFoundError.html +10 -0
- package/docs/interfaces/RetryOptions.html +10 -0
- package/docs/interfaces/RetryableError.html +10 -0
- package/docs/interfaces/StandardSchemaV1.FailureResult.html +4 -0
- package/docs/interfaces/StandardSchemaV1.Issue.html +6 -0
- package/docs/interfaces/StandardSchemaV1.Options.html +3 -0
- package/docs/interfaces/StandardSchemaV1.PathSegment.html +4 -0
- package/docs/interfaces/StandardSchemaV1.Props.html +10 -0
- package/docs/interfaces/StandardSchemaV1.SuccessResult.html +6 -0
- package/docs/interfaces/StandardSchemaV1.Types.html +6 -0
- package/docs/interfaces/StandardSchemaV1.html +4 -0
- package/docs/modules/StandardSchemaV1.html +1 -0
- package/docs/modules.html +1 -0
- package/docs/types/BoundInstance.html +1 -0
- package/docs/types/BulkGetBound.html +2 -0
- package/docs/types/BulkGetDictionaryBound.html +1 -0
- package/docs/types/BulkGetDictionaryOptions.html +2 -0
- package/docs/types/BulkGetDictionaryResult.html +3 -0
- package/docs/types/BulkGetOptions.html +3 -0
- package/docs/types/BulkGetResponse.html +1 -0
- package/docs/types/CouchConfig-1.html +1 -0
- package/docs/types/CouchConfig.html +1 -0
- package/docs/types/CouchConfigInput.html +1 -0
- package/docs/types/CouchDoc-1.html +1 -0
- package/docs/types/CouchDoc.html +2 -0
- package/docs/types/CouchDocInput.html +2 -0
- package/docs/types/GetAtRevBound.html +1 -0
- package/docs/types/GetBound.html +1 -0
- package/docs/types/GetOptions.html +2 -0
- package/docs/types/LockDoc-1.html +1 -0
- package/docs/types/LockDoc.html +1 -0
- package/docs/types/LockOptions-1.html +1 -0
- package/docs/types/LockOptions.html +1 -0
- package/docs/types/LockOptionsInput.html +1 -0
- package/docs/types/OnInvalidDocAction.html +1 -0
- package/docs/types/OnRow.html +1 -0
- package/docs/types/QueryBound.html +1 -0
- package/docs/types/SimpleViewOptions-1.html +1 -0
- package/docs/types/SimpleViewOptions.html +1 -0
- package/docs/types/StandardSchemaV1.InferInput.html +2 -0
- package/docs/types/StandardSchemaV1.InferOutput.html +2 -0
- package/docs/types/StandardSchemaV1.Result.html +2 -0
- package/docs/types/ViewQueryResponse-1.html +1 -0
- package/docs/types/ViewQueryResponse.html +2 -0
- package/docs/types/ViewQueryResponseValidated.html +2 -0
- package/docs/types/ViewRow-1.html +1 -0
- package/docs/types/ViewRow.html +2 -0
- package/docs/types/ViewRowValidated.html +7 -0
- package/docs/types/ViewString.html +1 -0
- package/docs/types/WatchOptionsInput.html +1 -0
- package/docs/types/WatchOptionsSchema-1.html +1 -0
- package/docs/types/WatchOptionsSchema.html +1 -0
- package/eslint.config.js +15 -0
- package/impl/bindConfig.mts +140 -0
- package/impl/bulkGet.mts +256 -0
- package/impl/bulkGet.test.mts +159 -0
- package/impl/bulkRemove.mts +98 -0
- package/impl/bulkRemove.test.mts +102 -0
- package/impl/bulkSave.mts +286 -0
- package/impl/bulkSave.test.mts +319 -0
- package/impl/get.mts +137 -0
- package/impl/get.test.mts +114 -0
- package/impl/getDBInfo.mts +67 -0
- package/impl/getDBInfo.test.mts +62 -0
- package/impl/patch.mts +134 -0
- package/impl/patch.test.mts +142 -0
- package/impl/put.mts +56 -0
- package/impl/put.test.mts +114 -0
- package/impl/query.mts +224 -0
- package/impl/query.test.mts +280 -0
- package/impl/remove.mts +65 -0
- package/impl/remove.test.mts +82 -0
- package/impl/retry.mts +66 -0
- package/impl/retry.test.mts +77 -0
- package/impl/stream.mts +143 -0
- package/impl/stream.test.mts +205 -0
- package/impl/sugar/lock.mts +103 -0
- package/impl/sugar/lock.test.mts +113 -0
- package/impl/sugar/{watch.mjs → watch.mts} +56 -22
- package/impl/sugar/watch.test.mts +155 -0
- package/impl/utils/errors.mts +130 -0
- package/impl/utils/errors.test.mts +58 -0
- package/impl/utils/logger.mts +62 -0
- package/impl/utils/logger.test.mts +129 -0
- package/impl/utils/mergeNeedleOpts.mts +16 -0
- package/impl/utils/parseRows.mts +117 -0
- package/impl/utils/parseRows.test.mts +183 -0
- package/impl/utils/queryBuilder.mts +173 -0
- package/impl/utils/queryBuilder.test.mts +83 -0
- package/impl/utils/queryString.mts +44 -0
- package/impl/utils/queryString.test.mts +53 -0
- package/impl/{trackedEmitter.mjs → utils/trackedEmitter.mts} +9 -7
- package/impl/utils/transactionErrors.mts +71 -0
- package/index.mts +82 -0
- package/index.test.mts +415 -0
- package/package.json +45 -31
- package/schema/config.mts +81 -0
- package/schema/couch/couch.input.schema.ts +43 -0
- package/schema/couch/couch.output.schema.ts +169 -0
- package/schema/sugar/lock.mts +18 -0
- package/schema/sugar/watch.mts +14 -0
- package/schema/util.mts +8 -0
- package/tsconfig.json +10 -4
- package/tsdown.config.ts +16 -0
- package/typedoc.json +4 -0
- package/types/output/eslint.config.d.ts +3 -0
- package/types/output/eslint.config.d.ts.map +1 -0
- package/types/output/impl/bindConfig.d.mts +174 -0
- package/types/output/impl/bindConfig.d.mts.map +1 -0
- package/types/output/impl/bulkGet.d.mts +75 -0
- package/types/output/impl/bulkGet.d.mts.map +1 -0
- package/types/output/impl/bulkGet.test.d.mts +2 -0
- package/types/output/impl/bulkGet.test.d.mts.map +1 -0
- package/types/output/impl/bulkRemove.d.mts +63 -0
- package/types/output/impl/bulkRemove.d.mts.map +1 -0
- package/types/output/impl/bulkRemove.test.d.mts +2 -0
- package/types/output/impl/bulkRemove.test.d.mts.map +1 -0
- package/types/output/impl/bulkSave.d.mts +64 -0
- package/types/output/impl/bulkSave.d.mts.map +1 -0
- package/types/output/impl/bulkSave.test.d.mts +2 -0
- package/types/output/impl/bulkSave.test.d.mts.map +1 -0
- package/types/output/impl/get.d.mts +20 -0
- package/types/output/impl/get.d.mts.map +1 -0
- package/types/output/impl/get.test.d.mts +2 -0
- package/types/output/impl/get.test.d.mts.map +1 -0
- package/types/output/impl/getDBInfo.d.mts +52 -0
- package/types/output/impl/getDBInfo.d.mts.map +1 -0
- package/types/output/impl/getDBInfo.test.d.mts +2 -0
- package/types/output/impl/getDBInfo.test.d.mts.map +1 -0
- package/types/output/impl/patch.d.mts +45 -0
- package/types/output/impl/patch.d.mts.map +1 -0
- package/types/output/impl/patch.test.d.mts +2 -0
- package/types/output/impl/patch.test.d.mts.map +1 -0
- package/types/output/impl/put.d.mts +5 -0
- package/types/output/impl/put.d.mts.map +1 -0
- package/types/output/impl/put.test.d.mts +2 -0
- package/types/output/impl/put.test.d.mts.map +1 -0
- package/types/output/impl/query.d.mts +47 -0
- package/types/output/impl/query.d.mts.map +1 -0
- package/types/output/impl/query.test.d.mts +2 -0
- package/types/output/impl/query.test.d.mts.map +1 -0
- package/types/output/impl/remove.d.mts +9 -0
- package/types/output/impl/remove.d.mts.map +1 -0
- package/types/output/impl/remove.test.d.mts +2 -0
- package/types/output/impl/remove.test.d.mts.map +1 -0
- package/types/output/impl/retry.d.mts +32 -0
- package/types/output/impl/retry.d.mts.map +1 -0
- package/types/output/impl/retry.test.d.mts +2 -0
- package/types/output/impl/retry.test.d.mts.map +1 -0
- package/types/output/impl/stream.d.mts +13 -0
- package/types/output/impl/stream.d.mts.map +1 -0
- package/types/output/impl/stream.test.d.mts +2 -0
- package/types/output/impl/stream.test.d.mts.map +1 -0
- package/types/output/impl/sugar/lock.d.mts +24 -0
- package/types/output/impl/sugar/lock.d.mts.map +1 -0
- package/types/output/impl/sugar/lock.test.d.mts +2 -0
- package/types/output/impl/sugar/lock.test.d.mts.map +1 -0
- package/types/output/impl/sugar/watch.d.mts +21 -0
- package/types/output/impl/sugar/watch.d.mts.map +1 -0
- package/types/output/impl/sugar/watch.test.d.mts +2 -0
- package/types/output/impl/sugar/watch.test.d.mts.map +1 -0
- package/types/output/impl/utils/errors.d.mts +78 -0
- package/types/output/impl/utils/errors.d.mts.map +1 -0
- package/types/output/impl/utils/errors.test.d.mts +2 -0
- package/types/output/impl/utils/errors.test.d.mts.map +1 -0
- package/types/output/impl/utils/logger.d.mts +11 -0
- package/types/output/impl/utils/logger.d.mts.map +1 -0
- package/types/output/impl/utils/logger.test.d.mts +2 -0
- package/types/output/impl/utils/logger.test.d.mts.map +1 -0
- package/types/output/impl/utils/mergeNeedleOpts.d.mts +53 -0
- package/types/output/impl/utils/mergeNeedleOpts.d.mts.map +1 -0
- package/types/output/impl/utils/parseRows.d.mts +15 -0
- package/types/output/impl/utils/parseRows.d.mts.map +1 -0
- package/types/output/impl/utils/parseRows.test.d.mts +2 -0
- package/types/output/impl/utils/parseRows.test.d.mts.map +1 -0
- package/types/output/impl/utils/queryBuilder.d.mts +68 -0
- package/types/output/impl/utils/queryBuilder.d.mts.map +1 -0
- package/types/output/impl/utils/queryBuilder.test.d.mts +2 -0
- package/types/output/impl/utils/queryBuilder.test.d.mts.map +1 -0
- package/types/output/impl/utils/queryString.d.mts +9 -0
- package/types/output/impl/utils/queryString.d.mts.map +1 -0
- package/types/output/impl/utils/queryString.test.d.mts +2 -0
- package/types/output/impl/utils/queryString.test.d.mts.map +1 -0
- package/types/output/impl/utils/trackedEmitter.d.mts +7 -0
- package/types/output/impl/utils/trackedEmitter.d.mts.map +1 -0
- package/{impl → types/output/impl/utils}/transactionErrors.d.mts +16 -31
- package/types/output/impl/utils/transactionErrors.d.mts.map +1 -0
- package/types/output/index.d.mts +32 -0
- package/types/output/index.d.mts.map +1 -0
- package/types/output/index.test.d.mts +2 -0
- package/types/output/index.test.d.mts.map +1 -0
- package/types/output/schema/config.d.mts +90 -0
- package/types/output/schema/config.d.mts.map +1 -0
- package/types/output/schema/couch/couch.input.schema.d.ts +29 -0
- package/types/output/schema/couch/couch.input.schema.d.ts.map +1 -0
- package/types/output/schema/couch/couch.output.schema.d.ts +113 -0
- package/types/output/schema/couch/couch.output.schema.d.ts.map +1 -0
- package/types/output/schema/sugar/lock.d.mts +19 -0
- package/types/output/schema/sugar/lock.d.mts.map +1 -0
- package/types/output/schema/sugar/watch.d.mts +11 -0
- package/types/output/schema/sugar/watch.d.mts.map +1 -0
- package/types/output/schema/util.d.mts +85 -0
- package/types/output/schema/util.d.mts.map +1 -0
- package/types/output/tsdown.config.d.ts +3 -0
- package/types/output/tsdown.config.d.ts.map +1 -0
- package/types/output/types/standard-schema.d.ts +60 -0
- package/types/output/types/standard-schema.d.ts.map +1 -0
- package/types/standard-schema.ts +76 -0
- package/types/utils.d.ts +1 -0
- package/cjs/impl/bulk.cjs +0 -275
- package/cjs/impl/changes.cjs +0 -67
- package/cjs/impl/crud.cjs +0 -127
- package/cjs/impl/errors.cjs +0 -75
- package/cjs/impl/logger.cjs +0 -70
- package/cjs/impl/patch.cjs +0 -95
- package/cjs/impl/query.cjs +0 -116
- package/cjs/impl/queryBuilder.cjs +0 -163
- package/cjs/impl/retry.cjs +0 -55
- package/cjs/impl/stream.cjs +0 -121
- package/cjs/impl/sugar/lock.cjs +0 -81
- package/cjs/impl/sugar/watch.cjs +0 -159
- package/cjs/impl/trackedEmitter.cjs +0 -54
- package/cjs/impl/transactionErrors.cjs +0 -70
- package/cjs/impl/util.cjs +0 -64
- package/cjs/index.cjs +0 -132
- package/cjs/integration/changes.cjs +0 -76
- package/cjs/integration/disconnect-watch.cjs +0 -52
- package/cjs/integration/watch.cjs +0 -59
- package/cjs/schema/bind.cjs +0 -59
- package/cjs/schema/bulk.cjs +0 -92
- package/cjs/schema/changes.cjs +0 -68
- package/cjs/schema/config.cjs +0 -48
- package/cjs/schema/crud.cjs +0 -77
- package/cjs/schema/patch.cjs +0 -53
- package/cjs/schema/query.cjs +0 -62
- package/cjs/schema/stream.cjs +0 -42
- package/cjs/schema/sugar/lock.cjs +0 -59
- package/cjs/schema/sugar/watch.cjs +0 -42
- package/cjs/schema/util.cjs +0 -39
- package/config.json +0 -5
- package/docs/compiler.png +0 -0
- package/dualmode.config.json +0 -11
- package/impl/bulk.d.mts +0 -11
- package/impl/bulk.d.mts.map +0 -1
- package/impl/bulk.mjs +0 -291
- package/impl/changes.d.mts +0 -12
- package/impl/changes.d.mts.map +0 -1
- package/impl/changes.mjs +0 -53
- package/impl/crud.d.mts +0 -7
- package/impl/crud.d.mts.map +0 -1
- package/impl/crud.mjs +0 -108
- package/impl/errors.d.mts +0 -43
- package/impl/errors.d.mts.map +0 -1
- package/impl/errors.mjs +0 -65
- package/impl/logger.d.mts +0 -32
- package/impl/logger.d.mts.map +0 -1
- package/impl/logger.mjs +0 -59
- package/impl/patch.d.mts +0 -6
- package/impl/patch.d.mts.map +0 -1
- package/impl/patch.mjs +0 -88
- package/impl/query.d.mts +0 -195
- package/impl/query.d.mts.map +0 -1
- package/impl/query.mjs +0 -122
- package/impl/queryBuilder.d.mts +0 -154
- package/impl/queryBuilder.d.mts.map +0 -1
- package/impl/queryBuilder.mjs +0 -175
- package/impl/retry.d.mts +0 -2
- package/impl/retry.d.mts.map +0 -1
- package/impl/retry.mjs +0 -39
- package/impl/stream.d.mts +0 -3
- package/impl/stream.d.mts.map +0 -1
- package/impl/stream.mjs +0 -98
- package/impl/sugar/lock.d.mts +0 -5
- package/impl/sugar/lock.d.mts.map +0 -1
- package/impl/sugar/lock.mjs +0 -70
- package/impl/sugar/watch.d.mts +0 -34
- package/impl/sugar/watch.d.mts.map +0 -1
- package/impl/trackedEmitter.d.mts +0 -8
- package/impl/trackedEmitter.d.mts.map +0 -1
- package/impl/transactionErrors.d.mts.map +0 -1
- package/impl/transactionErrors.mjs +0 -47
- package/impl/util.d.mts +0 -3
- package/impl/util.d.mts.map +0 -1
- package/impl/util.mjs +0 -35
- package/index.d.mts +0 -80
- package/index.d.mts.map +0 -1
- package/index.mjs +0 -141
- package/integration/changes.mjs +0 -60
- package/integration/disconnect-watch.mjs +0 -36
- package/integration/watch.mjs +0 -40
- package/log.txt +0 -580
- package/schema/bind.d.mts +0 -5461
- package/schema/bind.d.mts.map +0 -1
- package/schema/bind.mjs +0 -43
- package/schema/bulk.d.mts +0 -923
- package/schema/bulk.d.mts.map +0 -1
- package/schema/bulk.mjs +0 -83
- package/schema/changes.d.mts +0 -191
- package/schema/changes.d.mts.map +0 -1
- package/schema/changes.mjs +0 -59
- package/schema/config.d.mts +0 -79
- package/schema/config.d.mts.map +0 -1
- package/schema/config.mjs +0 -26
- package/schema/crud.d.mts +0 -491
- package/schema/crud.d.mts.map +0 -1
- package/schema/crud.mjs +0 -64
- package/schema/patch.d.mts +0 -255
- package/schema/patch.d.mts.map +0 -1
- package/schema/patch.mjs +0 -42
- package/schema/query.d.mts +0 -406
- package/schema/query.d.mts.map +0 -1
- package/schema/query.mjs +0 -45
- package/schema/stream.d.mts +0 -211
- package/schema/stream.d.mts.map +0 -1
- package/schema/stream.mjs +0 -23
- package/schema/sugar/lock.d.mts +0 -238
- package/schema/sugar/lock.d.mts.map +0 -1
- package/schema/sugar/lock.mjs +0 -50
- package/schema/sugar/watch.d.mts +0 -127
- package/schema/sugar/watch.d.mts.map +0 -1
- package/schema/sugar/watch.mjs +0 -29
- package/schema/util.d.mts +0 -160
- package/schema/util.d.mts.map +0 -1
- package/schema/util.mjs +0 -35
- package/types/changes-stream.d.ts +0 -11
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test, { suite } from 'node:test'
|
|
3
|
+
import needle from 'needle'
|
|
4
|
+
import type { CouchConfigInput } from '../schema/config.mts'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { RetryableError } from './utils/errors.mts'
|
|
7
|
+
import { bulkGet, bulkGetDictionary } from './bulkGet.mts'
|
|
8
|
+
import { TEST_DB_URL } from '../test/setup-db.mts'
|
|
9
|
+
|
|
10
|
+
const config: CouchConfigInput = {
|
|
11
|
+
couch: TEST_DB_URL
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function ensureDoc(id: string, body: Record<string, unknown>) {
|
|
15
|
+
await needle(
|
|
16
|
+
'put',
|
|
17
|
+
`${TEST_DB_URL}/${id}`,
|
|
18
|
+
{
|
|
19
|
+
_id: id,
|
|
20
|
+
...body
|
|
21
|
+
},
|
|
22
|
+
{ json: true }
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
suite('bulkGet', () => {
|
|
27
|
+
test('integration with pouchdb-server', async t => {
|
|
28
|
+
await ensureDoc('doc-1', { value: 42 })
|
|
29
|
+
await ensureDoc('doc-valid', { count: 7 })
|
|
30
|
+
await ensureDoc('doc-invalid', { count: 'nope' })
|
|
31
|
+
|
|
32
|
+
await t.test('fetches docs and not-found rows', async () => {
|
|
33
|
+
const response = await bulkGet(config, ['doc-1', 'doc-missing'])
|
|
34
|
+
assert.strictEqual(response.rows.length, 2)
|
|
35
|
+
const [first, second] = response.rows
|
|
36
|
+
assert.strictEqual(first?.id, 'doc-1')
|
|
37
|
+
assert.strictEqual(first?.doc?._id, 'doc-1')
|
|
38
|
+
assert.strictEqual(first?.doc?.value, 42)
|
|
39
|
+
assert.strictEqual(second?.error, 'not_found')
|
|
40
|
+
assert.strictEqual(second?.key, 'doc-missing')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
await t.test('supports includeDocs=false via _bulkGetWithOptions', async () => {
|
|
44
|
+
const response = await bulkGet(config, ['doc-1'], {
|
|
45
|
+
includeDocs: false
|
|
46
|
+
})
|
|
47
|
+
assert.strictEqual(response.rows.length, 1)
|
|
48
|
+
const [row] = response.rows
|
|
49
|
+
assert.strictEqual(row?.id, 'doc-1')
|
|
50
|
+
assert.ok(row?.value?.rev)
|
|
51
|
+
assert.ok(!('doc' in (row as Record<string, unknown>)))
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
await t.test('validates documents when schema provided', async () => {
|
|
55
|
+
const schema = z.looseObject({
|
|
56
|
+
_id: z.string(),
|
|
57
|
+
_rev: z.string().optional(),
|
|
58
|
+
count: z.number()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const valid = await bulkGet(config, ['doc-valid'], {
|
|
62
|
+
validate: {
|
|
63
|
+
docSchema: schema
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
assert.strictEqual(valid.rows[0]?.doc?.count, 7)
|
|
67
|
+
|
|
68
|
+
await assert.rejects(
|
|
69
|
+
() =>
|
|
70
|
+
bulkGet(config, ['doc-invalid'], {
|
|
71
|
+
validate: {
|
|
72
|
+
docSchema: schema
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
(err: unknown) => {
|
|
76
|
+
assert.ok(Array.isArray(err))
|
|
77
|
+
assert.match(err[0]?.message, /Invalid input:/)
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
await t.test('skips invalid documents when onInvalidDoc=skip', async () => {
|
|
84
|
+
const schema = z.looseObject({
|
|
85
|
+
_id: z.string(),
|
|
86
|
+
_rev: z.string().optional(),
|
|
87
|
+
count: z.number()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const response = await bulkGet(config, ['doc-valid', 'doc-invalid'], {
|
|
91
|
+
validate: {
|
|
92
|
+
docSchema: schema,
|
|
93
|
+
onInvalidDoc: 'skip'
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
assert.strictEqual(response.rows.length, 1)
|
|
98
|
+
assert.strictEqual(response.rows[0]?.doc?._id, 'doc-valid')
|
|
99
|
+
assert.strictEqual(response.rows[0]?.doc?.count, 7)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
await t.test('throws RetryableError for retryable status codes', async () => {
|
|
103
|
+
const offlineConfig: CouchConfigInput = {
|
|
104
|
+
couch: 'http://localhost:6553/offline-db'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await assert.rejects(
|
|
108
|
+
() => bulkGet(offlineConfig, ['doc-1']),
|
|
109
|
+
(err: unknown) => err instanceof RetryableError && err.statusCode === 503
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
await t.test('bulkGetDictionary groups results', async () => {
|
|
114
|
+
const result = await bulkGetDictionary(config, ['doc-valid', 'doc-missing'])
|
|
115
|
+
assert.deepStrictEqual(Object.keys(result.found), ['doc-valid'])
|
|
116
|
+
assert.strictEqual(result.found['doc-valid'].count, 7)
|
|
117
|
+
assert.deepStrictEqual(Object.keys(result.notFound), ['doc-missing'])
|
|
118
|
+
assert.strictEqual(result.notFound['doc-missing'].error, 'not_found')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
await t.test('bulkGetDictionary with validation schema', async () => {
|
|
122
|
+
const schema = z.looseObject({
|
|
123
|
+
_id: z.string(),
|
|
124
|
+
_rev: z.string().optional(),
|
|
125
|
+
count: z.number()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const result = await bulkGetDictionary(config, ['doc-valid', 'doc-not-there'], {
|
|
129
|
+
validate: {
|
|
130
|
+
docSchema: schema
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
assert.deepStrictEqual(Object.keys(result.found), ['doc-valid'])
|
|
135
|
+
assert.strictEqual(result.found['doc-valid'].count, 7)
|
|
136
|
+
assert.deepStrictEqual(Object.keys(result.notFound), ['doc-not-there'])
|
|
137
|
+
assert.strictEqual(result.notFound['doc-not-there'].error, 'not_found')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
await t.test('bulkGetDictionary skips invalid docs when requested', async () => {
|
|
141
|
+
const schema = z.looseObject({
|
|
142
|
+
_id: z.string(),
|
|
143
|
+
_rev: z.string().optional(),
|
|
144
|
+
count: z.number()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const result = await bulkGetDictionary(config, ['doc-valid', 'doc-invalid'], {
|
|
148
|
+
validate: {
|
|
149
|
+
docSchema: schema,
|
|
150
|
+
onInvalidDoc: 'skip'
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
assert.deepStrictEqual(Object.keys(result.found), ['doc-valid'])
|
|
155
|
+
assert.strictEqual(result.found['doc-valid'].count, 7)
|
|
156
|
+
assert.deepStrictEqual(Object.keys(result.notFound), [])
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { bulkGet } from './bulkGet.mts'
|
|
2
|
+
import { bulkSave } from './bulkSave.mts'
|
|
3
|
+
import { createLogger } from './utils/logger.mts'
|
|
4
|
+
import { remove } from './remove.mts'
|
|
5
|
+
import { CouchDoc } from '../schema/couch/couch.output.schema.ts'
|
|
6
|
+
import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Removes multiple documents from a CouchDB database using the _bulk_docs endpoint.
|
|
10
|
+
* It first retrieves the documents by their IDs, marks them as deleted, and then
|
|
11
|
+
* sends them back to the database for deletion.
|
|
12
|
+
*
|
|
13
|
+
* See https://docs.couchdb.org/en/stable/api/database/bulk-api.html#post--db-_bulk_docs
|
|
14
|
+
*
|
|
15
|
+
* @param configInput - The CouchDB configuration input.
|
|
16
|
+
* @param ids - An array of document IDs to be removed.
|
|
17
|
+
* @returns A promise that resolves to an array of results from the bulk delete operation.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const config: CouchConfigInput = {
|
|
22
|
+
* couch: 'http://localhost:5984/mydb',
|
|
23
|
+
* useConsoleLogger: true
|
|
24
|
+
* };
|
|
25
|
+
* const idsToRemove = ['doc1', 'doc2', 'doc3'];
|
|
26
|
+
* const results = await bulkRemove(config, idsToRemove);
|
|
27
|
+
* console.log(results);
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @throws Will throw an error if the provided configuration is invalid or if the bulk delete operation fails.
|
|
31
|
+
*/
|
|
32
|
+
export const bulkRemove = async (configInput: CouchConfigInput, ids: string[]) => {
|
|
33
|
+
const config = CouchConfig.parse(configInput)
|
|
34
|
+
const logger = createLogger(config)
|
|
35
|
+
logger.info(`Starting bulk remove for ${ids.length} documents`)
|
|
36
|
+
const resp = await bulkGet(config, ids)
|
|
37
|
+
const toRemove: Array<CouchDoc> = []
|
|
38
|
+
resp.rows?.forEach(row => {
|
|
39
|
+
if (!row.doc) return
|
|
40
|
+
try {
|
|
41
|
+
const d = CouchDoc.parse(row.doc)
|
|
42
|
+
d._deleted = true
|
|
43
|
+
toRemove.push(d)
|
|
44
|
+
} catch (e) {
|
|
45
|
+
logger.warn(`Invalid document structure in bulk remove: ${row.id}`, e)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
if (!toRemove.length) return []
|
|
49
|
+
const result = await bulkSave(config, toRemove)
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Removes multiple documents from a CouchDB database by their IDs using individual delete operations.
|
|
55
|
+
* It first retrieves the documents to get their revision IDs, then deletes each document one by one.
|
|
56
|
+
*
|
|
57
|
+
* See https://docs.couchdb.org/en/stable/api/document/common.html#delete--db-docid
|
|
58
|
+
*
|
|
59
|
+
* @param configInput - The CouchDB configuration input.
|
|
60
|
+
* @param ids - An array of document IDs to be removed.
|
|
61
|
+
* @returns A promise that resolves to an array of results from the individual delete operations.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const config: CouchConfigInput = {
|
|
66
|
+
* couch: 'http://localhost:5984/mydb',
|
|
67
|
+
* useConsoleLogger: true
|
|
68
|
+
* };
|
|
69
|
+
* const idsToRemove = ['doc1', 'doc2', 'doc3'];
|
|
70
|
+
* const results = await bulkRemoveMap(config, idsToRemove);
|
|
71
|
+
* console.log(results);
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @throws Will throw an error if the provided configuration is invalid or if any delete operation fails.
|
|
75
|
+
*/
|
|
76
|
+
export const bulkRemoveMap = async (configInput: CouchConfigInput, ids: string[]) => {
|
|
77
|
+
const config = CouchConfig.parse(configInput)
|
|
78
|
+
const logger = createLogger(config)
|
|
79
|
+
logger.info(`Starting bulk remove map for ${ids.length} documents`)
|
|
80
|
+
|
|
81
|
+
const { rows } = await bulkGet(config, ids, { includeDocs: false })
|
|
82
|
+
|
|
83
|
+
const results = []
|
|
84
|
+
for (const row of rows || []) {
|
|
85
|
+
try {
|
|
86
|
+
if (!row.value?.rev) throw new Error(`no rev found for doc ${row.id}`)
|
|
87
|
+
if (!row.id) {
|
|
88
|
+
throw new Error(`no id found for doc ${row}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const result = await remove(config, row.id, row.value.rev)
|
|
92
|
+
results.push(result)
|
|
93
|
+
} catch (e) {
|
|
94
|
+
logger.warn(`Error removing a doc in bulk remove map: ${row.id}`, e)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return results
|
|
98
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test, { suite } from 'node:test'
|
|
3
|
+
import needle from 'needle'
|
|
4
|
+
import type { CouchConfigInput } from '../schema/config.mts'
|
|
5
|
+
import { bulkRemove, bulkRemoveMap } from './bulkRemove.mts'
|
|
6
|
+
import { TEST_DB_URL } from '../test/setup-db.mts'
|
|
7
|
+
|
|
8
|
+
const config: CouchConfigInput = {
|
|
9
|
+
couch: TEST_DB_URL
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type DocBody = Record<string, unknown>
|
|
13
|
+
|
|
14
|
+
async function saveDoc(id: string, body: DocBody) {
|
|
15
|
+
const response = await needle(
|
|
16
|
+
'put',
|
|
17
|
+
`${TEST_DB_URL}/${id}`,
|
|
18
|
+
{
|
|
19
|
+
_id: id,
|
|
20
|
+
...body
|
|
21
|
+
},
|
|
22
|
+
{ json: true }
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if (response.statusCode !== 201 && response.statusCode !== 200) {
|
|
26
|
+
throw new Error(`Failed to save document ${id}: ${response.statusCode}`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return response.body as { rev: string }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function getDoc(id: string) {
|
|
33
|
+
return needle('get', `${TEST_DB_URL}/${id}`, null, { json: true })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
suite('bulkRemove', () => {
|
|
37
|
+
test('it should throw if provided config is invalid', async () => {
|
|
38
|
+
await assert.rejects(async () => {
|
|
39
|
+
// @ts-expect-error testing invalid config
|
|
40
|
+
await bulkRemove({ notAnOption: true, couch: DB_URL, useConsoleLogger: true }, ['doc1'])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
await assert.rejects(async () => {
|
|
44
|
+
// @ts-expect-error testing invalid config
|
|
45
|
+
await bulkRemoveMap({ anotherBadOption: 123, couch: DB_URL, useConsoleLogger: true }, [
|
|
46
|
+
'doc1'
|
|
47
|
+
])
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('integration with pouchdb-server', async t => {
|
|
52
|
+
await t.test('removes documents via _bulk_docs', async () => {
|
|
53
|
+
await saveDoc('bulk-remove-doc-1', { kind: 'test', count: 1 })
|
|
54
|
+
|
|
55
|
+
const results = await bulkRemove(config, ['bulk-remove-doc-1'])
|
|
56
|
+
assert.strictEqual(results.length, 1)
|
|
57
|
+
const [first] = results
|
|
58
|
+
assert.strictEqual(first?.id, 'bulk-remove-doc-1')
|
|
59
|
+
assert.strictEqual(first?.ok, true)
|
|
60
|
+
assert.ok(typeof first?.rev === 'string')
|
|
61
|
+
|
|
62
|
+
const { statusCode, body } = await getDoc('bulk-remove-doc-1')
|
|
63
|
+
assert.strictEqual(statusCode, 404)
|
|
64
|
+
assert.strictEqual(body?.error, 'not_found')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
await t.test('returns empty array when docs are missing', async () => {
|
|
68
|
+
const results = await bulkRemove(config, ['bulk-remove-missing'])
|
|
69
|
+
assert.deepStrictEqual(results, [])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
await t.test('bulkRemoveMap removes each document individually', async () => {
|
|
73
|
+
await saveDoc('bulk-remove-map-doc-1', { kind: 'map', count: 1 })
|
|
74
|
+
|
|
75
|
+
const results = await bulkRemoveMap(config, ['bulk-remove-map-doc-1'])
|
|
76
|
+
assert.strictEqual(results.length, 1)
|
|
77
|
+
const [first] = results
|
|
78
|
+
assert.ok(first)
|
|
79
|
+
assert.strictEqual(first.id, 'bulk-remove-map-doc-1')
|
|
80
|
+
assert.strictEqual(first.ok, true)
|
|
81
|
+
assert.strictEqual(first.statusCode, 200)
|
|
82
|
+
|
|
83
|
+
const { statusCode, body } = await getDoc('bulk-remove-map-doc-1')
|
|
84
|
+
assert.strictEqual(statusCode, 404)
|
|
85
|
+
assert.strictEqual(body?.error, 'not_found')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
await t.test('bulkRemoveMap skips docs without revs', async () => {
|
|
89
|
+
await saveDoc('bulk-remove-map-doc-2', { kind: 'map', count: 2 })
|
|
90
|
+
|
|
91
|
+
const results = await bulkRemoveMap(config, [
|
|
92
|
+
'bulk-remove-map-doc-2',
|
|
93
|
+
'bulk-remove-map-missing'
|
|
94
|
+
])
|
|
95
|
+
assert.strictEqual(results.length, 1)
|
|
96
|
+
const [first] = results
|
|
97
|
+
assert.ok(first)
|
|
98
|
+
assert.strictEqual(first.id, 'bulk-remove-map-doc-2')
|
|
99
|
+
assert.strictEqual(first.ok, true)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import needle from 'needle'
|
|
2
|
+
import { createLogger } from './utils/logger.mts'
|
|
3
|
+
import { mergeNeedleOpts } from './utils/mergeNeedleOpts.mts'
|
|
4
|
+
import { bulkGetDictionary } from './bulkGet.mts'
|
|
5
|
+
import { setupEmitter } from './utils/trackedEmitter.mts'
|
|
6
|
+
import {
|
|
7
|
+
TransactionSetupError,
|
|
8
|
+
TransactionVersionConflictError,
|
|
9
|
+
TransactionBulkOperationError,
|
|
10
|
+
TransactionRollbackError
|
|
11
|
+
} from './utils/transactionErrors.mts'
|
|
12
|
+
import {
|
|
13
|
+
BulkSaveResponse,
|
|
14
|
+
CouchDoc,
|
|
15
|
+
type CouchDocInput
|
|
16
|
+
} from '../schema/couch/couch.output.schema.ts'
|
|
17
|
+
import type { CouchConfigInput } from '../schema/config.mts'
|
|
18
|
+
import { RetryableError } from './utils/errors.mts'
|
|
19
|
+
import { withRetry } from './retry.mts'
|
|
20
|
+
import { put } from './put.mts'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Bulk saves documents to CouchDB using the _bulk_docs endpoint.
|
|
24
|
+
*
|
|
25
|
+
* @see
|
|
26
|
+
* https://docs.couchdb.org/en/stable/api/database/bulk-api.html#db-bulk-docs
|
|
27
|
+
*
|
|
28
|
+
* @param {CouchConfigInput} config - The CouchDB configuration.
|
|
29
|
+
* @param {CouchDocInput[]} docs - An array of documents to save.
|
|
30
|
+
* @returns {Promise<BulkSaveResponse>} - The response from CouchDB after the bulk save operation.
|
|
31
|
+
*
|
|
32
|
+
* @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
|
|
33
|
+
* @throws {Error} When CouchDB returns a non-retryable error payload.
|
|
34
|
+
*/
|
|
35
|
+
export const bulkSave = async (config: CouchConfigInput, docs: CouchDocInput[]) => {
|
|
36
|
+
const logger = createLogger(config)
|
|
37
|
+
|
|
38
|
+
if (docs == null || !docs.length) {
|
|
39
|
+
logger.error('bulkSave called with no docs')
|
|
40
|
+
throw new Error('no docs provided')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
logger.info(`Starting bulk save of ${docs.length} documents`)
|
|
44
|
+
const url = `${config.couch}/_bulk_docs`
|
|
45
|
+
const body = { docs }
|
|
46
|
+
const opts = {
|
|
47
|
+
json: true,
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const mergedOpts = mergeNeedleOpts(config, opts)
|
|
53
|
+
let resp
|
|
54
|
+
try {
|
|
55
|
+
resp = await needle('post', url, body, mergedOpts)
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logger.error('Network error during bulk save:', err)
|
|
58
|
+
RetryableError.handleNetworkError(err)
|
|
59
|
+
}
|
|
60
|
+
if (!resp) {
|
|
61
|
+
logger.error('No response received from bulk save request')
|
|
62
|
+
throw new RetryableError('no response', 503)
|
|
63
|
+
}
|
|
64
|
+
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
65
|
+
logger.warn(`Retryable status code received: ${resp.statusCode}`)
|
|
66
|
+
throw new RetryableError('retryable error during bulk save', resp.statusCode)
|
|
67
|
+
}
|
|
68
|
+
if (resp.statusCode !== 201) {
|
|
69
|
+
logger.error(`Unexpected status code: ${resp.statusCode}`)
|
|
70
|
+
throw new Error('could not save')
|
|
71
|
+
}
|
|
72
|
+
const results = resp?.body || []
|
|
73
|
+
return BulkSaveResponse.parse(results)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type TransactionStatus = 'pending' | 'completed' | 'rolled_back' | 'rollback_failed'
|
|
77
|
+
type TransactionDoc = {
|
|
78
|
+
_id: string
|
|
79
|
+
_rev: string | null | undefined
|
|
80
|
+
type: 'transaction'
|
|
81
|
+
status: TransactionStatus
|
|
82
|
+
changes: CouchDocInput[]
|
|
83
|
+
timestamp: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Performs a bulk save of documents within a transaction context.
|
|
88
|
+
*
|
|
89
|
+
* @remarks
|
|
90
|
+
* This operation ensures that either all documents are saved successfully, or none are, maintaining data consistency.
|
|
91
|
+
* If any document fails to save, the operation will attempt to roll back all changes.
|
|
92
|
+
*
|
|
93
|
+
* The transactionId has to be unique for the lifetime of the app. It is used to prevent two processes from executing the same transaction. It is up to you to craft a transactionId that uniquely represents this transaction, and that also is the same if another process tries to generate it.
|
|
94
|
+
*
|
|
95
|
+
* Exceptions to handle:
|
|
96
|
+
*
|
|
97
|
+
* `TransactionSetupError` Thrown if the transaction document cannot be created. Usually because it already exists
|
|
98
|
+
* `TransactionVersionConflictError` Thrown if there are version conflicts with existing documents.
|
|
99
|
+
* `TransactionBulkOperationError` Thrown if the bulk save operation fails for some documents.
|
|
100
|
+
* `TransactionRollbackError` Thrown if the rollback operation fails after a transaction failure.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* const docsToSave = [
|
|
105
|
+
* { _id: 'doc1', foo: 'bar' },
|
|
106
|
+
* { _id: 'doc2', foo: 'baz' }
|
|
107
|
+
* ];
|
|
108
|
+
*
|
|
109
|
+
* try {
|
|
110
|
+
* const results = await bulkSaveTransaction(config, 'unique-transaction-id', docsToSave);
|
|
111
|
+
* console.log('Bulk save successful:', results);
|
|
112
|
+
* } catch (error) {
|
|
113
|
+
* console.error('Bulk save transaction failed:', error);
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* @param {CouchConfigInput} config - The CouchDB configuration.
|
|
118
|
+
* @param {string} transactionId - A unique identifier for the transaction.
|
|
119
|
+
* @param {CouchDocInput[]} docs - An array of documents to save.
|
|
120
|
+
* @returns {Promise<BulkSaveResponse>} - The transaction save results.
|
|
121
|
+
* @throws {TransactionSetupError} When the transaction document cannot be created.
|
|
122
|
+
* @throws {TransactionVersionConflictError} When there are version conflicts with existing documents.
|
|
123
|
+
* @throws {TransactionBulkOperationError} When the bulk save operation fails for some documents.
|
|
124
|
+
* @throws {TransactionRollbackError} When the rollback operation fails after a transaction failure.
|
|
125
|
+
*/
|
|
126
|
+
export const bulkSaveTransaction = async (
|
|
127
|
+
config: CouchConfigInput,
|
|
128
|
+
transactionId: string,
|
|
129
|
+
docs: CouchDocInput[]
|
|
130
|
+
): Promise<BulkSaveResponse> => {
|
|
131
|
+
const emitter = setupEmitter(config)
|
|
132
|
+
const logger = createLogger(config)
|
|
133
|
+
const retryOptions = {
|
|
134
|
+
maxRetries: config.maxRetries ?? 10,
|
|
135
|
+
initialDelay: config.initialDelay ?? 1000,
|
|
136
|
+
backoffFactor: config.backoffFactor ?? 2
|
|
137
|
+
}
|
|
138
|
+
const _put = config.bindWithRetry
|
|
139
|
+
? withRetry(put.bind(null, config), retryOptions)
|
|
140
|
+
: put.bind(null, config)
|
|
141
|
+
logger.info(`Starting bulk save transaction ${transactionId} for ${docs.length} documents`)
|
|
142
|
+
|
|
143
|
+
// Create transaction document
|
|
144
|
+
const transactionDoc: TransactionDoc = {
|
|
145
|
+
_id: `txn:${transactionId}`,
|
|
146
|
+
_rev: null,
|
|
147
|
+
type: 'transaction',
|
|
148
|
+
status: 'pending',
|
|
149
|
+
changes: docs,
|
|
150
|
+
timestamp: new Date().toISOString()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Save transaction document
|
|
154
|
+
let transactionResponse = await _put(transactionDoc)
|
|
155
|
+
logger.debug('Transaction document created:', transactionDoc, transactionResponse)
|
|
156
|
+
await emitter.emit('transaction-created', {
|
|
157
|
+
transactionResponse,
|
|
158
|
+
txnDoc: transactionDoc
|
|
159
|
+
})
|
|
160
|
+
if (transactionResponse.error) {
|
|
161
|
+
throw new TransactionSetupError('Failed to create transaction document', {
|
|
162
|
+
error: transactionResponse.error,
|
|
163
|
+
response: transactionResponse
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Get current revisions of all documents
|
|
168
|
+
const existingDocs = await bulkGetDictionary(
|
|
169
|
+
config,
|
|
170
|
+
docs.map(d => d._id)
|
|
171
|
+
)
|
|
172
|
+
logger.debug('Fetched current revisions of documents:', existingDocs)
|
|
173
|
+
await emitter.emit('transaction-revs-fetched', existingDocs)
|
|
174
|
+
|
|
175
|
+
/** @type {string[]} */
|
|
176
|
+
const revErrors: string[] = []
|
|
177
|
+
// if any of the existingDocs, and the docs provided do not match on rev, then throw an error
|
|
178
|
+
docs.forEach(d => {
|
|
179
|
+
if (!d._id) return
|
|
180
|
+
if (existingDocs.found[d._id] && existingDocs.found[d._id]._rev !== d._rev)
|
|
181
|
+
revErrors.push(d._id)
|
|
182
|
+
if (existingDocs.notFound[d._id] && d._rev) revErrors.push(d._id)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
if (revErrors.length > 0) {
|
|
186
|
+
throw new TransactionVersionConflictError(revErrors)
|
|
187
|
+
}
|
|
188
|
+
logger.debug('Checked document revisions:', existingDocs)
|
|
189
|
+
await emitter.emit('transaction-revs-checked', existingDocs)
|
|
190
|
+
|
|
191
|
+
const providedDocsById: Record<string, CouchDocInput> = {}
|
|
192
|
+
docs.forEach(d => {
|
|
193
|
+
if (!d._id) return
|
|
194
|
+
providedDocsById[d._id] = d
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const newDocsToRollback: BulkSaveResponse = []
|
|
198
|
+
const potentialExistingDocsToRollback: BulkSaveResponse = []
|
|
199
|
+
const failedDocs: BulkSaveResponse = []
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
logger.info('Transaction started:', transactionDoc)
|
|
203
|
+
await emitter.emit('transaction-started', transactionDoc)
|
|
204
|
+
// Apply updates
|
|
205
|
+
const results = await bulkSave(config, docs)
|
|
206
|
+
logger.info('Transaction updates applied:', results)
|
|
207
|
+
await emitter.emit('transaction-updates-applied', results)
|
|
208
|
+
|
|
209
|
+
// Check for failures
|
|
210
|
+
results.forEach(r => {
|
|
211
|
+
if (!r.id) return // not enough info
|
|
212
|
+
if (!r.error) {
|
|
213
|
+
if (existingDocs.notFound[r.id]) newDocsToRollback.push(r)
|
|
214
|
+
if (existingDocs.found[r.id]) potentialExistingDocsToRollback.push(r)
|
|
215
|
+
} else {
|
|
216
|
+
failedDocs.push(r)
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
if (failedDocs.length > 0) {
|
|
220
|
+
throw new TransactionBulkOperationError(failedDocs)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Update transaction status to completed
|
|
224
|
+
transactionDoc.status = 'completed'
|
|
225
|
+
transactionDoc._rev = transactionResponse.rev
|
|
226
|
+
transactionResponse = await _put(transactionDoc)
|
|
227
|
+
logger.info('Transaction completed:', transactionDoc)
|
|
228
|
+
await emitter.emit('transaction-completed', {
|
|
229
|
+
transactionResponse,
|
|
230
|
+
transactionDoc
|
|
231
|
+
})
|
|
232
|
+
if (transactionResponse.statusCode !== 201) {
|
|
233
|
+
logger.error('Failed to update transaction status to completed')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return results
|
|
237
|
+
} catch (error) {
|
|
238
|
+
logger.error('Transaction failed, attempting rollback:', error)
|
|
239
|
+
|
|
240
|
+
// Rollback changes
|
|
241
|
+
const toRollback: CouchDoc[] = []
|
|
242
|
+
potentialExistingDocsToRollback.forEach(row => {
|
|
243
|
+
if (!row.id || !row.rev) return
|
|
244
|
+
const doc = existingDocs.found[row.id]
|
|
245
|
+
doc._rev = row.rev
|
|
246
|
+
toRollback.push(doc)
|
|
247
|
+
})
|
|
248
|
+
newDocsToRollback.forEach(d => {
|
|
249
|
+
if (!d.id || !d.rev) return
|
|
250
|
+
const before = JSON.parse(JSON.stringify(providedDocsById[d.id]))
|
|
251
|
+
before._rev = d.rev
|
|
252
|
+
before._deleted = true
|
|
253
|
+
toRollback.push(before)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// rollback all the changes
|
|
257
|
+
const bulkRollbackResult = await bulkSave(config, toRollback)
|
|
258
|
+
let status: TransactionStatus = 'rolled_back'
|
|
259
|
+
bulkRollbackResult.forEach(r => {
|
|
260
|
+
if (r.error) status = 'rollback_failed'
|
|
261
|
+
})
|
|
262
|
+
logger.warn('Transaction rolled back:', { bulkRollbackResult, status })
|
|
263
|
+
await emitter.emit('transaction-rolled-back', {
|
|
264
|
+
bulkRollbackResult,
|
|
265
|
+
status
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
// Update transaction status to rolled back
|
|
269
|
+
transactionDoc.status = status
|
|
270
|
+
transactionDoc._rev = transactionResponse.rev || null
|
|
271
|
+
transactionResponse = await _put(transactionDoc)
|
|
272
|
+
logger.warn('Transaction rollback status updated:', transactionDoc)
|
|
273
|
+
await emitter.emit('transaction-rolled-back-status', {
|
|
274
|
+
transactionResponse,
|
|
275
|
+
transactionDoc
|
|
276
|
+
})
|
|
277
|
+
if (transactionResponse.statusCode !== 201) {
|
|
278
|
+
logger.error('Failed to update transaction status to rolled_back')
|
|
279
|
+
}
|
|
280
|
+
throw new TransactionRollbackError(
|
|
281
|
+
'Transaction failed and rollback was unsuccessful',
|
|
282
|
+
error as Error,
|
|
283
|
+
bulkRollbackResult
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
}
|