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,319 @@
|
|
|
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 { bulkSave, bulkSaveTransaction } from './bulkSave.mts'
|
|
6
|
+
import { RetryableError } from './utils/errors.mts'
|
|
7
|
+
import {
|
|
8
|
+
TransactionRollbackError,
|
|
9
|
+
TransactionVersionConflictError
|
|
10
|
+
} from './utils/transactionErrors.mts'
|
|
11
|
+
import { TEST_DB_URL } from '../test/setup-db.mts'
|
|
12
|
+
|
|
13
|
+
const baseConfig: CouchConfigInput = {
|
|
14
|
+
couch: TEST_DB_URL,
|
|
15
|
+
useConsoleLogger: true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const transactionBaseConfig: CouchConfigInput = {
|
|
19
|
+
couch: TEST_DB_URL,
|
|
20
|
+
bindWithRetry: false,
|
|
21
|
+
useConsoleLogger: true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type EventRecord = { event: string; payload: unknown }
|
|
25
|
+
|
|
26
|
+
function createTestEmitter() {
|
|
27
|
+
const events: EventRecord[] = []
|
|
28
|
+
const handlers = new Map<string, Array<(payload: unknown) => Promise<void> | void>>()
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
events,
|
|
32
|
+
on(event: string, handler: (payload: unknown) => Promise<void> | void) {
|
|
33
|
+
const list = handlers.get(event) ?? []
|
|
34
|
+
list.push(handler)
|
|
35
|
+
handlers.set(event, list)
|
|
36
|
+
},
|
|
37
|
+
async emit(event: string, payload: unknown) {
|
|
38
|
+
events.push({ event, payload })
|
|
39
|
+
const list = handlers.get(event)
|
|
40
|
+
if (!list) return
|
|
41
|
+
for (const handler of list) {
|
|
42
|
+
await handler(payload)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function saveDoc(dbUrl: string, id: string, body: Record<string, unknown>) {
|
|
49
|
+
const response = await needle('put', `${dbUrl}/${id}`, { _id: id, ...body }, { json: true })
|
|
50
|
+
|
|
51
|
+
if (response.statusCode !== 201 && response.statusCode !== 200) {
|
|
52
|
+
throw new Error(`Failed to save document ${id}: ${response.statusCode}`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return response.body as { rev: string }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function getDocFrom(dbUrl: string, id: string) {
|
|
59
|
+
return needle('get', `${dbUrl}/${id}`, null, { json: true })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function getDoc(id: string) {
|
|
63
|
+
return getDocFrom(TEST_DB_URL, id)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
suite('bulkSave', () => {
|
|
67
|
+
test('rejects invalid config arguments', async () => {
|
|
68
|
+
await assert.rejects(async () => {
|
|
69
|
+
// @ts-expect-error intentionally passing unsupported option
|
|
70
|
+
await bulkSave({ couch: TEST_DB_URL, unsupported: true }, [
|
|
71
|
+
{ _id: 'bad-config-doc', count: 1 }
|
|
72
|
+
])
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('throws error if called with no docs', async () => {
|
|
77
|
+
await assert.rejects(async () => {
|
|
78
|
+
// @ts-expect-error testing no docs
|
|
79
|
+
await bulkSave(baseConfig, null)
|
|
80
|
+
})
|
|
81
|
+
await assert.rejects(async () => {
|
|
82
|
+
await bulkSave(baseConfig, [])
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('propagates retryable network failures', async () => {
|
|
87
|
+
const offlineConfig: CouchConfigInput = {
|
|
88
|
+
couch: 'http://localhost:6554/offline-bulk-save'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await assert.rejects(
|
|
92
|
+
() => bulkSave(offlineConfig, [{ _id: 'offline-doc', count: 1 }]),
|
|
93
|
+
(err: unknown) => err instanceof RetryableError && err.statusCode === 503
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('integration with pouchdb-server', async t => {
|
|
98
|
+
let docTwoInitialRev: string | undefined
|
|
99
|
+
const docs = [
|
|
100
|
+
{ _id: `bulk-save-doc-1-${Date.now()}`, type: 'integration', count: 1 },
|
|
101
|
+
{ _id: `bulk-save-doc-2-${Date.now()}`, type: 'integration', count: 2 }
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
await t.test('creates documents via _bulk_docs', async () => {
|
|
105
|
+
const results = await bulkSave(baseConfig, docs)
|
|
106
|
+
assert.strictEqual(results.length, 2)
|
|
107
|
+
const [first, second] = results
|
|
108
|
+
assert.ok(first)
|
|
109
|
+
assert.strictEqual(first.id, docs[0]._id)
|
|
110
|
+
assert.strictEqual(first.ok, true)
|
|
111
|
+
assert.ok(second)
|
|
112
|
+
assert.strictEqual(second.id, docs[1]._id)
|
|
113
|
+
assert.strictEqual(second.ok, true)
|
|
114
|
+
|
|
115
|
+
docTwoInitialRev = second.rev ?? undefined
|
|
116
|
+
assert.ok(typeof docTwoInitialRev === 'string')
|
|
117
|
+
|
|
118
|
+
const { statusCode, body } = await getDoc(docs[0]._id)
|
|
119
|
+
assert.strictEqual(statusCode, 200)
|
|
120
|
+
assert.strictEqual(body?.type, 'integration')
|
|
121
|
+
assert.strictEqual(body?.count, 1)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
await t.test('updates documents when revision supplied', async () => {
|
|
125
|
+
const current = await getDoc(docs[1]._id)
|
|
126
|
+
assert.strictEqual(current.statusCode, 200)
|
|
127
|
+
const updateResults = await bulkSave(baseConfig, [
|
|
128
|
+
{
|
|
129
|
+
_id: docs[1]._id,
|
|
130
|
+
_rev: current.body?._rev,
|
|
131
|
+
type: 'integration',
|
|
132
|
+
count: 3
|
|
133
|
+
}
|
|
134
|
+
])
|
|
135
|
+
|
|
136
|
+
assert.strictEqual(updateResults.length, 1)
|
|
137
|
+
const [updated] = updateResults
|
|
138
|
+
assert.ok(updated)
|
|
139
|
+
assert.strictEqual(updated.ok, true)
|
|
140
|
+
assert.ok(updated.rev)
|
|
141
|
+
|
|
142
|
+
const { body } = await getDoc(docs[1]._id)
|
|
143
|
+
assert.strictEqual(body?.count, 3)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
await t.test('reports conflicts when revision is stale', async () => {
|
|
147
|
+
if (!docTwoInitialRev) throw new Error('Expected initial revision to be captured')
|
|
148
|
+
|
|
149
|
+
const conflictResults = await bulkSave(baseConfig, [
|
|
150
|
+
{
|
|
151
|
+
_id: docs[1]._id,
|
|
152
|
+
_rev: docTwoInitialRev,
|
|
153
|
+
type: 'integration',
|
|
154
|
+
count: 99
|
|
155
|
+
}
|
|
156
|
+
])
|
|
157
|
+
|
|
158
|
+
assert.strictEqual(conflictResults.length, 1)
|
|
159
|
+
const [conflict] = conflictResults
|
|
160
|
+
assert.ok(conflict)
|
|
161
|
+
assert.strictEqual(conflict.id, docs[1]._id)
|
|
162
|
+
assert.strictEqual(conflict.error, 'conflict')
|
|
163
|
+
assert.ok(conflict.reason)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
suite('bulkSaveTransaction', () => {
|
|
169
|
+
test('integration with pouchdb-server', async t => {
|
|
170
|
+
await t.test('completes transaction for new and existing docs', async () => {
|
|
171
|
+
const emitter = createTestEmitter()
|
|
172
|
+
const config: CouchConfigInput = {
|
|
173
|
+
...transactionBaseConfig,
|
|
174
|
+
'~emitter': emitter
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const existingId = `txn-existing-success-${Date.now()}`
|
|
178
|
+
const newId = `txn-new-success-${Date.now()}`
|
|
179
|
+
const transactionId = `bulk-transaction-success-${Date.now()}`
|
|
180
|
+
|
|
181
|
+
const existing = await saveDoc(TEST_DB_URL, existingId, {
|
|
182
|
+
type: 'transaction',
|
|
183
|
+
count: 1
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const docs = [
|
|
187
|
+
{
|
|
188
|
+
_id: existingId,
|
|
189
|
+
_rev: existing.rev,
|
|
190
|
+
type: 'transaction',
|
|
191
|
+
count: 2
|
|
192
|
+
},
|
|
193
|
+
{ _id: newId, type: 'transaction', count: 1 }
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
const results = await bulkSaveTransaction(config, transactionId, docs)
|
|
197
|
+
assert.strictEqual(results.length, 2)
|
|
198
|
+
assert.ok(results[0]?.ok)
|
|
199
|
+
assert.strictEqual(results[0]?.id, existingId)
|
|
200
|
+
assert.ok(results[1]?.ok)
|
|
201
|
+
assert.strictEqual(results[1]?.id, newId)
|
|
202
|
+
|
|
203
|
+
const updatedExisting = await getDocFrom(TEST_DB_URL, existingId)
|
|
204
|
+
assert.strictEqual(updatedExisting.statusCode, 200)
|
|
205
|
+
assert.strictEqual(updatedExisting.body?.count, 2)
|
|
206
|
+
|
|
207
|
+
const createdDoc = await getDocFrom(TEST_DB_URL, newId)
|
|
208
|
+
assert.strictEqual(createdDoc.statusCode, 200)
|
|
209
|
+
assert.strictEqual(createdDoc.body?.count, 1)
|
|
210
|
+
|
|
211
|
+
const transactionDoc = await getDocFrom(TEST_DB_URL, `txn:${transactionId}`)
|
|
212
|
+
assert.strictEqual(transactionDoc.statusCode, 200)
|
|
213
|
+
assert.strictEqual(transactionDoc.body?.status, 'completed')
|
|
214
|
+
|
|
215
|
+
assert.ok(emitter.events.some(({ event }) => event === 'transaction-created'))
|
|
216
|
+
assert.ok(emitter.events.some(({ event }) => event === 'transaction-completed'))
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
await t.test('throws TransactionVersionConflictError when revisions mismatch', async () => {
|
|
220
|
+
const emitter = createTestEmitter()
|
|
221
|
+
const config: CouchConfigInput = {
|
|
222
|
+
...transactionBaseConfig,
|
|
223
|
+
'~emitter': emitter
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const docId = `txn-conflict-doc-${Date.now()}`
|
|
227
|
+
const transactionId = `bulk-transaction-conflict-${Date.now()}`
|
|
228
|
+
|
|
229
|
+
const first = await saveDoc(TEST_DB_URL, docId, {
|
|
230
|
+
type: 'conflict',
|
|
231
|
+
count: 1
|
|
232
|
+
})
|
|
233
|
+
await saveDoc(TEST_DB_URL, docId, {
|
|
234
|
+
_rev: first.rev,
|
|
235
|
+
type: 'conflict',
|
|
236
|
+
count: 2
|
|
237
|
+
})
|
|
238
|
+
await assert.rejects(
|
|
239
|
+
() =>
|
|
240
|
+
bulkSaveTransaction(config, transactionId, [
|
|
241
|
+
{
|
|
242
|
+
_id: docId,
|
|
243
|
+
_rev: first.rev,
|
|
244
|
+
type: 'conflict',
|
|
245
|
+
count: 3
|
|
246
|
+
}
|
|
247
|
+
]),
|
|
248
|
+
(err: unknown) =>
|
|
249
|
+
err instanceof TransactionVersionConflictError && err.conflictingIds.includes(docId)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
assert.ok(emitter.events.some(({ event }) => event === 'transaction-created'))
|
|
253
|
+
assert.ok(emitter.events.some(({ event }) => event === 'transaction-revs-fetched'))
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
await t.test('rolls back changes when bulk save fails', async () => {
|
|
257
|
+
const emitter = createTestEmitter()
|
|
258
|
+
const config: CouchConfigInput = {
|
|
259
|
+
...transactionBaseConfig,
|
|
260
|
+
'~emitter': emitter
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const successId = `txn-rollback-existing-${Date.now()}`
|
|
264
|
+
const conflictId = `txn-rollback-conflict-${Date.now()}`
|
|
265
|
+
const transactionId = `bulk-transaction-rollback-${Date.now()}`
|
|
266
|
+
|
|
267
|
+
const existing = await saveDoc(TEST_DB_URL, successId, {
|
|
268
|
+
type: 'rollback',
|
|
269
|
+
count: 1
|
|
270
|
+
})
|
|
271
|
+
const conflicting = await saveDoc(TEST_DB_URL, conflictId, {
|
|
272
|
+
type: 'rollback',
|
|
273
|
+
count: 1
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
emitter.on('transaction-revs-checked', async () => {
|
|
277
|
+
await needle(
|
|
278
|
+
'put',
|
|
279
|
+
`${TEST_DB_URL}/${conflictId}`,
|
|
280
|
+
{
|
|
281
|
+
_id: conflictId,
|
|
282
|
+
_rev: conflicting.rev,
|
|
283
|
+
type: 'rollback',
|
|
284
|
+
count: 99
|
|
285
|
+
},
|
|
286
|
+
{ json: true }
|
|
287
|
+
)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
await assert.rejects(
|
|
291
|
+
() =>
|
|
292
|
+
bulkSaveTransaction(config, transactionId, [
|
|
293
|
+
{ _id: successId, _rev: existing.rev, type: 'rollback', count: 2 },
|
|
294
|
+
{
|
|
295
|
+
_id: conflictId,
|
|
296
|
+
_rev: conflicting.rev,
|
|
297
|
+
type: 'rollback',
|
|
298
|
+
count: 2
|
|
299
|
+
}
|
|
300
|
+
]),
|
|
301
|
+
(err: unknown) => err instanceof TransactionRollbackError
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
const rolledBack = await getDocFrom(TEST_DB_URL, successId)
|
|
305
|
+
assert.strictEqual(rolledBack.statusCode, 200)
|
|
306
|
+
assert.strictEqual(rolledBack.body?.count, 1)
|
|
307
|
+
|
|
308
|
+
const conflicted = await getDocFrom(TEST_DB_URL, conflictId)
|
|
309
|
+
assert.strictEqual(conflicted.statusCode, 200)
|
|
310
|
+
assert.strictEqual(conflicted.body?.count, 99)
|
|
311
|
+
|
|
312
|
+
const transactionDoc = await getDocFrom(TEST_DB_URL, `txn:${transactionId}`)
|
|
313
|
+
assert.strictEqual(transactionDoc.statusCode, 200)
|
|
314
|
+
assert.strictEqual(transactionDoc.body?.status, 'rolled_back')
|
|
315
|
+
|
|
316
|
+
assert.ok(emitter.events.some(({ event }) => event === 'transaction-rolled-back'))
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
})
|
package/impl/get.mts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import needle from 'needle'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { CouchConfigInput } from '../schema/config.mts'
|
|
4
|
+
import { createLogger } from './utils/logger.mts'
|
|
5
|
+
import { mergeNeedleOpts } from './utils/mergeNeedleOpts.mts'
|
|
6
|
+
import { RetryableError, NotFoundError } from './utils/errors.mts'
|
|
7
|
+
import type { StandardSchemaV1 } from '../types/standard-schema.ts'
|
|
8
|
+
import { CouchDoc } from '../schema/couch/couch.output.schema.ts'
|
|
9
|
+
|
|
10
|
+
export type GetOptions<DocSchema extends StandardSchemaV1> = {
|
|
11
|
+
validate?: {
|
|
12
|
+
docSchema?: DocSchema
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type InternalGetOptions<DocSchema extends StandardSchemaV1> = GetOptions<DocSchema> & {
|
|
17
|
+
rev?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ValidSchema = z.custom(
|
|
21
|
+
value => {
|
|
22
|
+
return value !== null && typeof value === 'object' && '~standard' in value
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
message: 'docSchema must be a valid StandardSchemaV1 schema'
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
export const CouchGetOptions = z.object({
|
|
30
|
+
rev: z.string().optional().describe('the couch doc revision'),
|
|
31
|
+
validate: z
|
|
32
|
+
.object({
|
|
33
|
+
docSchema: ValidSchema.optional()
|
|
34
|
+
})
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('optional document validation rules')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
async function _getWithOptions<DocSchema extends StandardSchemaV1>(
|
|
40
|
+
config: CouchConfigInput,
|
|
41
|
+
id: string,
|
|
42
|
+
options: InternalGetOptions<DocSchema>
|
|
43
|
+
): Promise<StandardSchemaV1.InferOutput<DocSchema> | null> {
|
|
44
|
+
const parsedOptions = CouchGetOptions.parse({
|
|
45
|
+
rev: options.rev,
|
|
46
|
+
validate: options.validate
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const logger = createLogger(config)
|
|
50
|
+
const rev = parsedOptions.rev
|
|
51
|
+
const path = rev ? `${id}?rev=${rev}` : id
|
|
52
|
+
const url = `${config.couch}/${path}`
|
|
53
|
+
|
|
54
|
+
const httpOptions = {
|
|
55
|
+
json: true,
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': 'application/json'
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const requestOptions = mergeNeedleOpts(config, httpOptions)
|
|
62
|
+
logger.info(`Getting document with id: ${id}, rev ${rev ?? 'latest'}`)
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const resp = await needle('get', url, null, requestOptions)
|
|
66
|
+
if (!resp) {
|
|
67
|
+
logger.error('No response received from get request')
|
|
68
|
+
throw new RetryableError('no response', 503)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const body = resp.body ?? null
|
|
72
|
+
|
|
73
|
+
if (resp.statusCode === 404) {
|
|
74
|
+
if (config.throwOnGetNotFound) {
|
|
75
|
+
const reason = typeof body?.reason === 'string' ? body.reason : 'not_found'
|
|
76
|
+
logger.warn(`Document not found (throwing error): ${id}, rev ${rev ?? 'latest'}`)
|
|
77
|
+
throw new NotFoundError(id, reason)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
logger.debug(`Document not found (returning undefined): ${id}, rev ${rev ?? 'latest'}`)
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
85
|
+
const reason = typeof body?.reason === 'string' ? body.reason : 'retryable error'
|
|
86
|
+
logger.warn(`Retryable status code received: ${resp.statusCode}`)
|
|
87
|
+
throw new RetryableError(reason, resp.statusCode)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (resp.statusCode !== 200) {
|
|
91
|
+
const reason = typeof body?.reason === 'string' ? body.reason : 'failed'
|
|
92
|
+
logger.error(`Unexpected status code: ${resp.statusCode}`)
|
|
93
|
+
throw new Error(reason)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const docSchema = (parsedOptions.validate?.docSchema ?? CouchDoc) as DocSchema
|
|
97
|
+
const typedDoc = await docSchema['~standard'].validate(body)
|
|
98
|
+
|
|
99
|
+
if (typedDoc.issues) {
|
|
100
|
+
throw typedDoc.issues
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
logger.info(`Successfully retrieved document: ${id}, rev ${rev ?? 'latest'}`)
|
|
104
|
+
return typedDoc.value
|
|
105
|
+
} catch (err) {
|
|
106
|
+
logger.error('Error during get operation:', err)
|
|
107
|
+
RetryableError.handleNetworkError(err)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function get<DocSchema extends StandardSchemaV1 = typeof CouchDoc>(
|
|
112
|
+
config: CouchConfigInput,
|
|
113
|
+
id: string,
|
|
114
|
+
options?: GetOptions<DocSchema>
|
|
115
|
+
): Promise<StandardSchemaV1.InferOutput<DocSchema> | null> {
|
|
116
|
+
return _getWithOptions<DocSchema>(config, id, options ?? {})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type GetBound = <DocSchema extends StandardSchemaV1 = typeof CouchDoc>(
|
|
120
|
+
id: string,
|
|
121
|
+
options?: GetOptions<DocSchema>
|
|
122
|
+
) => Promise<StandardSchemaV1.InferOutput<DocSchema> | null>
|
|
123
|
+
|
|
124
|
+
export async function getAtRev<DocSchema extends StandardSchemaV1>(
|
|
125
|
+
config: CouchConfigInput,
|
|
126
|
+
id: string,
|
|
127
|
+
rev: string,
|
|
128
|
+
options?: GetOptions<DocSchema>
|
|
129
|
+
): Promise<StandardSchemaV1.InferOutput<DocSchema> | null> {
|
|
130
|
+
return _getWithOptions<DocSchema>(config, id, { ...options, rev })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type GetAtRevBound = <DocSchema extends StandardSchemaV1 = typeof CouchDoc>(
|
|
134
|
+
id: string,
|
|
135
|
+
rev: string,
|
|
136
|
+
options?: GetOptions<DocSchema> | undefined
|
|
137
|
+
) => Promise<StandardSchemaV1.InferOutput<DocSchema> | null>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test, { suite } from 'node:test'
|
|
3
|
+
import needle from 'needle'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import type { CouchConfigInput } from '../schema/config.mts'
|
|
6
|
+
import { get, getAtRev } from './get.mts'
|
|
7
|
+
import { NotFoundError, RetryableError } from './utils/errors.mts'
|
|
8
|
+
import { TEST_DB_URL } from '../test/setup-db.mts'
|
|
9
|
+
|
|
10
|
+
const baseConfig: CouchConfigInput = {
|
|
11
|
+
couch: TEST_DB_URL
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type DocBody = Record<string, unknown>
|
|
15
|
+
|
|
16
|
+
async function saveDoc(id: string, body: DocBody) {
|
|
17
|
+
const response = await needle(
|
|
18
|
+
'put',
|
|
19
|
+
`${TEST_DB_URL}/${id}`,
|
|
20
|
+
{
|
|
21
|
+
_id: id,
|
|
22
|
+
...body
|
|
23
|
+
},
|
|
24
|
+
{ json: true }
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if (response.statusCode !== 201 && response.statusCode !== 200) {
|
|
28
|
+
throw new Error(`Failed to save document ${id}: ${response.statusCode}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return response.body as { rev: string }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
suite('get', () => {
|
|
35
|
+
test('integration with pouchdb-server', async t => {
|
|
36
|
+
const doc_valid_id = `doc-valid-${Date.now()}`
|
|
37
|
+
const doc_invalid_id = `doc-invalid-${Date.now()}`
|
|
38
|
+
const doc_rev_id = `doc-rev-${Date.now()}`
|
|
39
|
+
await saveDoc(doc_valid_id, { kind: 'example', count: 7 })
|
|
40
|
+
await saveDoc(doc_invalid_id, { kind: 'example', count: 'oops' })
|
|
41
|
+
const firstRev = await saveDoc(doc_rev_id, { version: 1 })
|
|
42
|
+
await saveDoc(doc_rev_id, { _rev: firstRev.rev, version: 2 })
|
|
43
|
+
|
|
44
|
+
await t.test('returns documents and validates schema', async () => {
|
|
45
|
+
const schema = z.looseObject({
|
|
46
|
+
_id: z.string(),
|
|
47
|
+
kind: z.literal('example'),
|
|
48
|
+
count: z.number()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const doc = await get(baseConfig, doc_valid_id, {
|
|
52
|
+
validate: { docSchema: schema }
|
|
53
|
+
})
|
|
54
|
+
assert.ok(doc)
|
|
55
|
+
assert.strictEqual(doc?.kind, 'example')
|
|
56
|
+
assert.strictEqual(doc?.count, 7)
|
|
57
|
+
|
|
58
|
+
await assert.rejects(
|
|
59
|
+
() => get(baseConfig, doc_invalid_id, { validate: { docSchema: schema } }),
|
|
60
|
+
(err: unknown) => {
|
|
61
|
+
return (
|
|
62
|
+
Array.isArray(err) &&
|
|
63
|
+
err[0].message === 'Invalid input: expected number, received string'
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
await t.test('returns null when not found by default', async () => {
|
|
70
|
+
const missing = await get(baseConfig, 'doc-missing')
|
|
71
|
+
assert.strictEqual(missing, null)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
await t.test('throws NotFoundError when configured', async () => {
|
|
75
|
+
const strictConfig: CouchConfigInput = {
|
|
76
|
+
...baseConfig,
|
|
77
|
+
throwOnGetNotFound: true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await assert.rejects(
|
|
81
|
+
() => get(strictConfig, 'doc-missing'),
|
|
82
|
+
(err: unknown) => err instanceof NotFoundError && err.docId === 'doc-missing'
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
await t.test('getAtRev returns specific revision', async () => {
|
|
87
|
+
const versionedSchema = z.looseObject({
|
|
88
|
+
_id: z.string(),
|
|
89
|
+
version: z.number()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const latest = await get(baseConfig, doc_rev_id, {
|
|
93
|
+
validate: { docSchema: versionedSchema }
|
|
94
|
+
})
|
|
95
|
+
assert.strictEqual(latest?.version, 2)
|
|
96
|
+
|
|
97
|
+
const early = await getAtRev(baseConfig, doc_rev_id, firstRev.rev, {
|
|
98
|
+
validate: { docSchema: versionedSchema }
|
|
99
|
+
})
|
|
100
|
+
assert.strictEqual(early?.version, 1)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
await t.test('propagates retryable network errors', async () => {
|
|
104
|
+
const offlineConfig: CouchConfigInput = {
|
|
105
|
+
couch: 'http://localhost:6553/offline-db'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await assert.rejects(
|
|
109
|
+
() => get(offlineConfig, 'doc-valid'),
|
|
110
|
+
(err: unknown) => err instanceof RetryableError && err.statusCode === 503
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import needle, { type NeedleResponse } from 'needle'
|
|
2
|
+
import { RetryableError } from './utils/errors.mts'
|
|
3
|
+
import { createLogger } from './utils/logger.mts'
|
|
4
|
+
import { mergeNeedleOpts } from './utils/mergeNeedleOpts.mts'
|
|
5
|
+
import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
|
|
6
|
+
import { CouchDBInfo } from '../schema/couch/couch.output.schema.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fetches and returns CouchDB database information.
|
|
10
|
+
*
|
|
11
|
+
* @see {@link https://docs.couchdb.org/en/stable/api/database/common.html#get--db | CouchDB API Documentation}
|
|
12
|
+
*
|
|
13
|
+
* @param configInput - The CouchDB configuration input.
|
|
14
|
+
* @returns A promise that resolves to the CouchDB database information.
|
|
15
|
+
* @throws {RetryableError} `RetryableError` If a retryable error occurs during the request.
|
|
16
|
+
* @throws {Error} `Error` For other non-retryable errors.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { getDBInfo } from './impl/getDBInfo.mts';
|
|
21
|
+
*
|
|
22
|
+
* const config = { couch: 'http://localhost:5984/my-database' };
|
|
23
|
+
*
|
|
24
|
+
* getDBInfo(config)
|
|
25
|
+
* .then(info => {
|
|
26
|
+
* console.log('Database Info:', info);
|
|
27
|
+
* })
|
|
28
|
+
* .catch(err => {
|
|
29
|
+
* console.error('Error fetching database info:', err);
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const getDBInfo = async (configInput: CouchConfigInput) => {
|
|
34
|
+
const config = CouchConfig.parse(configInput)
|
|
35
|
+
const logger = createLogger(config)
|
|
36
|
+
const url = `${config.couch}`
|
|
37
|
+
|
|
38
|
+
let resp: NeedleResponse | undefined
|
|
39
|
+
try {
|
|
40
|
+
resp = await needle(
|
|
41
|
+
'get',
|
|
42
|
+
url,
|
|
43
|
+
mergeNeedleOpts(config, {
|
|
44
|
+
json: true,
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': 'application/json'
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
)
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logger.error('Error during get operation:', err)
|
|
52
|
+
RetryableError.handleNetworkError(err)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!resp) {
|
|
56
|
+
logger.error('No response received from get request')
|
|
57
|
+
throw new RetryableError('no response', 503)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = resp.body
|
|
61
|
+
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
62
|
+
logger.warn(`Retryable status code received: ${resp.statusCode}`)
|
|
63
|
+
throw new RetryableError(result.reason ?? 'retryable error', resp.statusCode)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return CouchDBInfo.parse(result)
|
|
67
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { createServer } from 'node:http'
|
|
3
|
+
import test, { suite } from 'node:test'
|
|
4
|
+
import type { CouchConfigInput } from '../schema/config.mts'
|
|
5
|
+
import { getDBInfo } from './getDBInfo.mts'
|
|
6
|
+
import { RetryableError } from './utils/errors.mts'
|
|
7
|
+
import { TEST_DB_URL } from '../test/setup-db.mts'
|
|
8
|
+
|
|
9
|
+
suite('getDBInfo', () => {
|
|
10
|
+
test('it should throw if provided config is invalid', async () => {
|
|
11
|
+
await assert.rejects(async () => {
|
|
12
|
+
await getDBInfo({
|
|
13
|
+
// @ts-expect-error testing invalid config
|
|
14
|
+
notAnOption: true,
|
|
15
|
+
// @ts-expect-error testing invalid config
|
|
16
|
+
couch: DB_URL,
|
|
17
|
+
useConsoleLogger: true
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
test('integration with pouchdb-server', async t => {
|
|
22
|
+
await t.test('returns database metadata', async () => {
|
|
23
|
+
const config: CouchConfigInput = { couch: TEST_DB_URL }
|
|
24
|
+
const info = await getDBInfo(config)
|
|
25
|
+
assert.strictEqual(info.db_name, 'hide-a-bed-test-db')
|
|
26
|
+
assert.ok(typeof info.doc_count === 'number')
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('throws RetryableError when server marks response retryable', async t => {
|
|
31
|
+
const port = 8993
|
|
32
|
+
const server = createServer((_req, res) => {
|
|
33
|
+
res.statusCode = 503
|
|
34
|
+
res.setHeader('Content-Type', 'application/json')
|
|
35
|
+
res.end(JSON.stringify({ reason: 'maintenance' }))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
await new Promise<void>(resolve => {
|
|
39
|
+
server.listen(port, resolve)
|
|
40
|
+
})
|
|
41
|
+
t.after(() => {
|
|
42
|
+
server.close()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
await assert.rejects(
|
|
46
|
+
() => getDBInfo({ couch: `http://localhost:${port}/retryable` }),
|
|
47
|
+
(err: unknown) => {
|
|
48
|
+
assert.ok(err instanceof RetryableError)
|
|
49
|
+
assert.strictEqual(err.statusCode, 503)
|
|
50
|
+
assert.strictEqual(err.message, 'maintenance')
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('converts network failures into RetryableError', async () => {
|
|
57
|
+
await assert.rejects(
|
|
58
|
+
() => getDBInfo({ couch: 'http://localhost:6555/offline-db' }),
|
|
59
|
+
(err: unknown) => err instanceof RetryableError && err.statusCode === 503
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
})
|