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,280 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test, { suite } from 'node:test'
|
|
3
|
+
import needle from 'needle'
|
|
4
|
+
import { randomUUID } from 'node:crypto'
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
import type { CouchConfigInput } from '../schema/config.mts'
|
|
9
|
+
import { TEST_DB_URL } from '../test/setup-db.mts'
|
|
10
|
+
import { query } from './query.mts'
|
|
11
|
+
import { RetryableError } from './utils/errors.mts'
|
|
12
|
+
|
|
13
|
+
const config: CouchConfigInput = {
|
|
14
|
+
couch: TEST_DB_URL
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function putDoc(doc: Record<string, unknown> & { _id: string }) {
|
|
18
|
+
await needle('put', `${TEST_DB_URL}/${doc._id}`, doc, { json: true })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function putDesignDoc(id: string, viewName: string, mapFn: string) {
|
|
22
|
+
await needle(
|
|
23
|
+
'put',
|
|
24
|
+
`${TEST_DB_URL}/_design/${id}`,
|
|
25
|
+
{
|
|
26
|
+
views: {
|
|
27
|
+
[viewName]: {
|
|
28
|
+
map: mapFn
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{ json: true }
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function eventually<T>(
|
|
37
|
+
fn: () => Promise<T>,
|
|
38
|
+
predicate: (value: T) => boolean,
|
|
39
|
+
attempts = 10,
|
|
40
|
+
waitMs = 100
|
|
41
|
+
): Promise<T> {
|
|
42
|
+
let lastValue: T | undefined
|
|
43
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
44
|
+
lastValue = await fn()
|
|
45
|
+
if (predicate(lastValue)) return lastValue
|
|
46
|
+
await delay(waitMs)
|
|
47
|
+
}
|
|
48
|
+
return lastValue!
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
suite('query', () => {
|
|
52
|
+
test('returns rows with include_docs', async () => {
|
|
53
|
+
const designId = `query-view-${randomUUID()}`
|
|
54
|
+
const viewName = 'byCategory'
|
|
55
|
+
const tag = `query-suite-${randomUUID()}`
|
|
56
|
+
await putDesignDoc(
|
|
57
|
+
designId,
|
|
58
|
+
viewName,
|
|
59
|
+
`function(doc) { if (doc.tag !== '${tag}') return; emit(doc.category, doc.count); }`
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const matchingDoc = {
|
|
63
|
+
_id: `doc-${randomUUID()}`,
|
|
64
|
+
tag,
|
|
65
|
+
category: 'keep',
|
|
66
|
+
count: 42
|
|
67
|
+
}
|
|
68
|
+
const otherDoc = {
|
|
69
|
+
_id: `doc-${randomUUID()}`,
|
|
70
|
+
tag,
|
|
71
|
+
category: 'skip',
|
|
72
|
+
count: 1
|
|
73
|
+
}
|
|
74
|
+
const unrelatedDoc = {
|
|
75
|
+
_id: `doc-${randomUUID()}`,
|
|
76
|
+
tag: 'other',
|
|
77
|
+
category: 'keep',
|
|
78
|
+
count: 100
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await putDoc(matchingDoc)
|
|
82
|
+
await putDoc(otherDoc)
|
|
83
|
+
await putDoc(unrelatedDoc)
|
|
84
|
+
|
|
85
|
+
const response = await eventually(
|
|
86
|
+
() =>
|
|
87
|
+
query(config, `_design/${designId}/_view/${viewName}`, {
|
|
88
|
+
include_docs: true,
|
|
89
|
+
key: matchingDoc.category
|
|
90
|
+
}),
|
|
91
|
+
({ rows }) => rows?.length === 1
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if (!response.rows) {
|
|
95
|
+
throw new Error('Expected rows in response')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
assert.strictEqual(response.rows[0].key, matchingDoc.category)
|
|
99
|
+
assert.strictEqual(response.rows[0].value, matchingDoc.count)
|
|
100
|
+
assert.strictEqual(response.rows[0].doc?._id, matchingDoc._id)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('validates rows when schemas provided', async () => {
|
|
104
|
+
const designId = `query-validate-${randomUUID()}`
|
|
105
|
+
const viewName = 'byPlayer'
|
|
106
|
+
const tag = `query-suite-${randomUUID()}`
|
|
107
|
+
await putDesignDoc(
|
|
108
|
+
designId,
|
|
109
|
+
viewName,
|
|
110
|
+
`function(doc) { if (doc.tag !== '${tag}') return; emit(doc.player, doc.score); }`
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const doc = { _id: `doc-${randomUUID()}`, tag, player: 'alpha', score: 7 }
|
|
114
|
+
await putDoc(doc)
|
|
115
|
+
|
|
116
|
+
const response = await eventually(
|
|
117
|
+
() =>
|
|
118
|
+
query(config, `_design/${designId}/_view/${viewName}`, {
|
|
119
|
+
include_docs: true,
|
|
120
|
+
key: doc.player,
|
|
121
|
+
validate: {
|
|
122
|
+
docSchema: z.looseObject({
|
|
123
|
+
_id: z.string(),
|
|
124
|
+
tag: z.string(),
|
|
125
|
+
player: z.string(),
|
|
126
|
+
score: z.number()
|
|
127
|
+
}),
|
|
128
|
+
keySchema: z.string(),
|
|
129
|
+
valueSchema: z.number()
|
|
130
|
+
}
|
|
131
|
+
}),
|
|
132
|
+
({ rows }) => rows.length === 1
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
assert.strictEqual(response.rows[0]?.value, doc.score)
|
|
136
|
+
assert.strictEqual(response.rows[0]?.doc?.player, doc.player)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('rejects when validation fails', async () => {
|
|
140
|
+
const designId = `query-invalid-${randomUUID()}`
|
|
141
|
+
const viewName = 'byPlayer'
|
|
142
|
+
const tag = `query-suite-${randomUUID()}`
|
|
143
|
+
await putDesignDoc(
|
|
144
|
+
designId,
|
|
145
|
+
viewName,
|
|
146
|
+
`function(doc) { if (doc.tag !== '${tag}') return; emit(doc.player, doc.score); }`
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
const validDoc = {
|
|
150
|
+
_id: `doc-${randomUUID()}`,
|
|
151
|
+
tag,
|
|
152
|
+
player: 'valid',
|
|
153
|
+
score: 3
|
|
154
|
+
}
|
|
155
|
+
const invalidDoc = {
|
|
156
|
+
_id: `doc-${randomUUID()}`,
|
|
157
|
+
tag,
|
|
158
|
+
player: 'invalid',
|
|
159
|
+
score: 'nope'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await putDoc(validDoc)
|
|
163
|
+
await putDoc(invalidDoc)
|
|
164
|
+
|
|
165
|
+
await eventually(
|
|
166
|
+
() =>
|
|
167
|
+
query(config, `_design/${designId}/_view/${viewName}`, {
|
|
168
|
+
key: validDoc.player
|
|
169
|
+
}),
|
|
170
|
+
({ rows }) => rows?.length === 1
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
await assert.rejects(async () => {
|
|
174
|
+
return query(config, `_design/${designId}/_view/${viewName}`, {
|
|
175
|
+
validate: {
|
|
176
|
+
valueSchema: z.number()
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('skips invalid documents when onInvalidDoc=skip', async () => {
|
|
183
|
+
const designId = `query-skip-${randomUUID()}`
|
|
184
|
+
const viewName = 'byPlayer'
|
|
185
|
+
const tag = `query-suite-${randomUUID()}`
|
|
186
|
+
await putDesignDoc(
|
|
187
|
+
designId,
|
|
188
|
+
viewName,
|
|
189
|
+
`function(doc) { if (doc.tag !== '${tag}') return; emit(doc.player, doc.score); }`
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
const validDoc = {
|
|
193
|
+
_id: `doc-${randomUUID()}`,
|
|
194
|
+
tag,
|
|
195
|
+
player: 'valid',
|
|
196
|
+
score: 5
|
|
197
|
+
}
|
|
198
|
+
const invalidDoc = {
|
|
199
|
+
_id: `doc-${randomUUID()}`,
|
|
200
|
+
tag,
|
|
201
|
+
player: 'invalid',
|
|
202
|
+
score: 'nope'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await putDoc(validDoc)
|
|
206
|
+
await putDoc(invalidDoc)
|
|
207
|
+
|
|
208
|
+
const response = await eventually(
|
|
209
|
+
() =>
|
|
210
|
+
query(config, `_design/${designId}/_view/${viewName}`, {
|
|
211
|
+
include_docs: true,
|
|
212
|
+
validate: {
|
|
213
|
+
docSchema: z.looseObject({
|
|
214
|
+
_id: z.string(),
|
|
215
|
+
tag: z.string(),
|
|
216
|
+
player: z.string(),
|
|
217
|
+
score: z.number()
|
|
218
|
+
}),
|
|
219
|
+
keySchema: z.string(),
|
|
220
|
+
valueSchema: z.number(),
|
|
221
|
+
onInvalidDoc: 'skip'
|
|
222
|
+
}
|
|
223
|
+
}),
|
|
224
|
+
({ rows }) => rows.length === 1
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
assert.strictEqual(response.rows[0]?.doc?.player, validDoc.player)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('posts payload when keys exceed URL limit', async () => {
|
|
231
|
+
const designId = `query-post-${randomUUID()}`
|
|
232
|
+
const viewName = 'byPlayer'
|
|
233
|
+
const tag = `query-suite-${randomUUID()}`
|
|
234
|
+
await putDesignDoc(
|
|
235
|
+
designId,
|
|
236
|
+
viewName,
|
|
237
|
+
`function(doc) { if (doc.tag !== '${tag}') return; emit(doc.player, doc.score); }`
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
const targetDoc = {
|
|
241
|
+
_id: `doc-${randomUUID()}`,
|
|
242
|
+
tag,
|
|
243
|
+
player: 'target',
|
|
244
|
+
score: 11
|
|
245
|
+
}
|
|
246
|
+
await putDoc(targetDoc)
|
|
247
|
+
|
|
248
|
+
await eventually(
|
|
249
|
+
() =>
|
|
250
|
+
query(config, `_design/${designId}/_view/${viewName}`, {
|
|
251
|
+
key: targetDoc.player
|
|
252
|
+
}),
|
|
253
|
+
({ rows }) => rows?.length === 1
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
const bulkKeys = Array.from({ length: 400 }, (_, index) => `missing-${index}-${randomUUID()}`)
|
|
257
|
+
bulkKeys.push(targetDoc.player)
|
|
258
|
+
|
|
259
|
+
const response = await query(config, `_design/${designId}/_view/${viewName}`, {
|
|
260
|
+
include_docs: true,
|
|
261
|
+
keys: bulkKeys
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
if (!response.rows) throw new Error('Expected rows in response')
|
|
265
|
+
assert.strictEqual(response.rows.length, 1)
|
|
266
|
+
assert.strictEqual(response.rows[0]?.key, targetDoc.player)
|
|
267
|
+
assert.strictEqual(response.rows[0]?.doc?._id, targetDoc._id)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
test('throws RetryableError on network failure', async () => {
|
|
271
|
+
const offlineConfig: CouchConfigInput = {
|
|
272
|
+
couch: 'http://localhost:6553/offline-db'
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await assert.rejects(
|
|
276
|
+
() => query(offlineConfig, '_all_docs', {}),
|
|
277
|
+
(err: unknown) => err instanceof RetryableError && err.statusCode === 503
|
|
278
|
+
)
|
|
279
|
+
})
|
|
280
|
+
})
|
package/impl/remove.mts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import needle from 'needle'
|
|
2
|
+
import { createLogger } from './utils/logger.mts'
|
|
3
|
+
import { mergeNeedleOpts } from './utils/mergeNeedleOpts.mts'
|
|
4
|
+
import { RetryableError } from './utils/errors.mts'
|
|
5
|
+
import { CouchPutResponse } from '../schema/couch/couch.output.schema.ts'
|
|
6
|
+
import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
|
|
7
|
+
|
|
8
|
+
export const remove = async (configInput: CouchConfigInput, id: string, rev: string) => {
|
|
9
|
+
const config = CouchConfig.parse(configInput)
|
|
10
|
+
const logger = createLogger(config)
|
|
11
|
+
const url = `${config.couch}/${id}?rev=${rev}`
|
|
12
|
+
const opts = {
|
|
13
|
+
json: true,
|
|
14
|
+
headers: {
|
|
15
|
+
'Content-Type': 'application/json'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const mergedOpts = mergeNeedleOpts(config, opts)
|
|
19
|
+
|
|
20
|
+
logger.info(`Deleting document with id: ${id}`)
|
|
21
|
+
let resp
|
|
22
|
+
try {
|
|
23
|
+
resp = await needle('delete', url, null, mergedOpts)
|
|
24
|
+
} catch (err) {
|
|
25
|
+
logger.error('Error during delete operation:', err)
|
|
26
|
+
RetryableError.handleNetworkError(err)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!resp) {
|
|
30
|
+
logger.error('No response received from delete request')
|
|
31
|
+
throw new RetryableError('no response', 503)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let result
|
|
35
|
+
if (typeof resp.body === 'string') {
|
|
36
|
+
try {
|
|
37
|
+
result = JSON.parse(resp.body)
|
|
38
|
+
} catch {
|
|
39
|
+
result = {}
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
result = resp.body || {}
|
|
43
|
+
}
|
|
44
|
+
result.statusCode = resp.statusCode
|
|
45
|
+
|
|
46
|
+
if (resp.statusCode === 404) {
|
|
47
|
+
logger.warn(`Document not found for deletion: ${id}`)
|
|
48
|
+
result.ok = false
|
|
49
|
+
result.error = 'not_found'
|
|
50
|
+
return CouchPutResponse.parse(result)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
54
|
+
logger.warn(`Retryable status code received: ${resp.statusCode}`)
|
|
55
|
+
throw new RetryableError(result.reason || 'retryable error', resp.statusCode)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (resp.statusCode !== 200) {
|
|
59
|
+
logger.error(`Unexpected status code: ${resp.statusCode}`)
|
|
60
|
+
throw new Error(result.reason || 'failed')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
logger.info(`Successfully deleted document: ${id}`)
|
|
64
|
+
return CouchPutResponse.parse(result)
|
|
65
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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 { remove } from './remove.mts'
|
|
6
|
+
import { RetryableError } from './utils/errors.mts'
|
|
7
|
+
import { TEST_DB_URL } from '../test/setup-db.mts'
|
|
8
|
+
|
|
9
|
+
const baseConfig: CouchConfigInput = {
|
|
10
|
+
couch: TEST_DB_URL
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type DocBody = Record<string, unknown>
|
|
14
|
+
|
|
15
|
+
async function saveDoc(id: string, body: DocBody) {
|
|
16
|
+
const response = await needle(
|
|
17
|
+
'put',
|
|
18
|
+
`${TEST_DB_URL}/${id}`,
|
|
19
|
+
{
|
|
20
|
+
_id: id,
|
|
21
|
+
...body
|
|
22
|
+
},
|
|
23
|
+
{ json: true }
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if (response.statusCode !== 201 && response.statusCode !== 200) {
|
|
27
|
+
throw new Error(`Failed to save document ${id}: ${response.statusCode}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return response.body as { rev: string }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function getDoc(id: string) {
|
|
34
|
+
return needle('get', `${TEST_DB_URL}/${id}`, null, { json: true })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
suite('remove', () => {
|
|
38
|
+
test('it should throw if provided config is invalid', async () => {
|
|
39
|
+
await assert.rejects(async () => {
|
|
40
|
+
await remove(
|
|
41
|
+
// @ts-expect-error testing invalid config
|
|
42
|
+
{ couch: DB_URL, useConsoleLogger: true, unexpected: true },
|
|
43
|
+
'doc-invalid-config',
|
|
44
|
+
'1-invalid'
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('integration with pouchdb-server', async t => {
|
|
50
|
+
await t.test('removes an existing document', async () => {
|
|
51
|
+
const remove_doc_id = `remove-doc-1-${Date.now()}`
|
|
52
|
+
const { rev } = await saveDoc(remove_doc_id, { kind: 'test', count: 1 })
|
|
53
|
+
|
|
54
|
+
const result = await remove(baseConfig, remove_doc_id, rev)
|
|
55
|
+
assert.strictEqual(result.ok, true)
|
|
56
|
+
assert.strictEqual(result.id, remove_doc_id)
|
|
57
|
+
assert.strictEqual(result.statusCode, 200)
|
|
58
|
+
|
|
59
|
+
const { statusCode, body } = await getDoc(remove_doc_id)
|
|
60
|
+
assert.strictEqual(statusCode, 404)
|
|
61
|
+
assert.strictEqual(body?.error, 'not_found')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
await t.test('returns not found metadata when document is missing', async () => {
|
|
65
|
+
const result = await remove(baseConfig, 'remove-doc-missing', '1-missing')
|
|
66
|
+
assert.strictEqual(result.ok, false)
|
|
67
|
+
assert.strictEqual(result.error, 'not_found')
|
|
68
|
+
assert.strictEqual(result.statusCode, 404)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
await t.test('propagates retryable network errors', async () => {
|
|
72
|
+
const offlineConfig: CouchConfigInput = {
|
|
73
|
+
couch: 'http://localhost:6553/offline-remove-db'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await assert.rejects(
|
|
77
|
+
() => remove(offlineConfig, 'remove-doc-network', '1-offline'),
|
|
78
|
+
(err: unknown) => err instanceof RetryableError && err.statusCode === 503
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
})
|
package/impl/retry.mts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { setTimeout } from 'node:timers/promises'
|
|
2
|
+
import { RetryableError } from './utils/errors.mts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Settings that control how retry attempts are scheduled.
|
|
6
|
+
*/
|
|
7
|
+
export interface RetryOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Maximum number of retry attempts before rethrowing the original error.
|
|
10
|
+
*/
|
|
11
|
+
maxRetries?: number
|
|
12
|
+
/**
|
|
13
|
+
* Initial wait duration in milliseconds before attempting the first retry.
|
|
14
|
+
*/
|
|
15
|
+
initialDelay?: number
|
|
16
|
+
/**
|
|
17
|
+
* Multiplier applied to the delay after each retry to implement exponential backoff.
|
|
18
|
+
*/
|
|
19
|
+
backoffFactor?: number
|
|
20
|
+
/**
|
|
21
|
+
* Upper bound, in milliseconds, for the delay between retries.
|
|
22
|
+
*/
|
|
23
|
+
maxDelay?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type MaybePromise<T> = PromiseLike<T> | T
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wrap an async-capable function with retry semantics that respect {@link RetryableError}.
|
|
30
|
+
* @typeParam Fn - The function signature to decorate with retry handling.
|
|
31
|
+
* @param fn The function to invoke with retry support.
|
|
32
|
+
* @param options Retry tuning parameters.
|
|
33
|
+
* @returns A function mirroring `fn` that automatically retries on {@link RetryableError}.
|
|
34
|
+
*/
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
export function withRetry<Fn extends (...args: any[]) => MaybePromise<any>>(
|
|
37
|
+
fn: Fn,
|
|
38
|
+
options: RetryOptions = {}
|
|
39
|
+
): (...args: Parameters<Fn>) => Promise<Awaited<ReturnType<Fn>>> {
|
|
40
|
+
const { maxRetries = 3, initialDelay = 1000, backoffFactor = 2, maxDelay = 30000 } = options
|
|
41
|
+
|
|
42
|
+
return async (...args: Parameters<Fn>): Promise<Awaited<ReturnType<Fn>>> => {
|
|
43
|
+
let delay = initialDelay
|
|
44
|
+
|
|
45
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
46
|
+
try {
|
|
47
|
+
const result = await fn(...args)
|
|
48
|
+
return result
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (!(error instanceof RetryableError)) {
|
|
51
|
+
throw error
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (attempt === maxRetries) {
|
|
55
|
+
throw error
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const nextDelay = Math.min(delay, maxDelay)
|
|
59
|
+
await setTimeout(nextDelay)
|
|
60
|
+
delay *= backoffFactor
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw new RetryableError('withRetry exhausted retry attempts without resolving the operation')
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test, { suite } from 'node:test'
|
|
3
|
+
import { withRetry } from './retry.mts'
|
|
4
|
+
import { RetryableError } from './utils/errors.mts'
|
|
5
|
+
|
|
6
|
+
suite('withRetry', () => {
|
|
7
|
+
test('resolves when the wrapped function succeeds without retries', async () => {
|
|
8
|
+
let count = 0
|
|
9
|
+
const fn = async (value: string) => {
|
|
10
|
+
count += 1
|
|
11
|
+
return value
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const wrapped = withRetry(fn)
|
|
15
|
+
const result = await wrapped('ok')
|
|
16
|
+
|
|
17
|
+
assert.strictEqual(result, 'ok')
|
|
18
|
+
assert.strictEqual(count, 1)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('retries on RetryableError until success', async () => {
|
|
22
|
+
let attempts = 0
|
|
23
|
+
const fn = async () => {
|
|
24
|
+
attempts += 1
|
|
25
|
+
if (attempts < 3) {
|
|
26
|
+
throw new RetryableError('temporary', 503)
|
|
27
|
+
}
|
|
28
|
+
return 'done'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const wrapped = withRetry(fn, { initialDelay: 0, maxDelay: 0 })
|
|
32
|
+
const result = await wrapped()
|
|
33
|
+
|
|
34
|
+
assert.strictEqual(result, 'done')
|
|
35
|
+
assert.strictEqual(attempts, 3)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('propagates non retryable errors immediately', async () => {
|
|
39
|
+
let attempts = 0
|
|
40
|
+
const fn = async () => {
|
|
41
|
+
attempts += 1
|
|
42
|
+
throw new Error('fatal')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const wrapped = withRetry(fn, { initialDelay: 0, maxDelay: 0 })
|
|
46
|
+
|
|
47
|
+
await assert.rejects(
|
|
48
|
+
() => wrapped(),
|
|
49
|
+
(err: unknown) => {
|
|
50
|
+
return err instanceof Error && !(err instanceof RetryableError) && err.message === 'fatal'
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
assert.strictEqual(attempts, 1)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('throws after exceeding the maximum retries', async () => {
|
|
57
|
+
let attempts = 0
|
|
58
|
+
const fn = async () => {
|
|
59
|
+
attempts += 1
|
|
60
|
+
throw new RetryableError('still failing', 503)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const wrapped = withRetry(fn, {
|
|
64
|
+
maxRetries: 2,
|
|
65
|
+
initialDelay: 0,
|
|
66
|
+
maxDelay: 0
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
await assert.rejects(
|
|
70
|
+
() => wrapped(),
|
|
71
|
+
(err: unknown) => {
|
|
72
|
+
return err instanceof RetryableError && err.message === 'still failing'
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
assert.strictEqual(attempts, 3)
|
|
76
|
+
})
|
|
77
|
+
})
|