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
package/impl/patch.mts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { put } from './put.mts'
|
|
2
|
+
import { get } from './get.mts'
|
|
3
|
+
import { createLogger } from './utils/logger.mts'
|
|
4
|
+
import { setTimeout } from 'node:timers/promises'
|
|
5
|
+
import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
const PatchProperties = z
|
|
9
|
+
.looseObject({
|
|
10
|
+
_rev: z.string('_rev is required for patch operations')
|
|
11
|
+
})
|
|
12
|
+
.describe('Patch payload with _rev')
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Patch a CouchDB document by merging provided properties.
|
|
16
|
+
* Validates that the _rev matches before applying the patch.
|
|
17
|
+
*
|
|
18
|
+
* @param configInput - CouchDB configuration
|
|
19
|
+
* @param id - Document ID to patch
|
|
20
|
+
* @param _properties - Properties to merge into the document (must include _rev)
|
|
21
|
+
* @returns The result of the put operation
|
|
22
|
+
*
|
|
23
|
+
* @throws Error if the _rev does not match or other errors occur
|
|
24
|
+
*/
|
|
25
|
+
export const patch = async (
|
|
26
|
+
configInput: CouchConfigInput,
|
|
27
|
+
id: string,
|
|
28
|
+
_properties: z.infer<typeof PatchProperties>
|
|
29
|
+
) => {
|
|
30
|
+
const config = CouchConfig.parse(configInput)
|
|
31
|
+
const properties = PatchProperties.parse(_properties)
|
|
32
|
+
const logger = createLogger(configInput)
|
|
33
|
+
|
|
34
|
+
logger.info(`Starting patch operation for document ${id}`)
|
|
35
|
+
logger.debug('Patch properties:', properties)
|
|
36
|
+
const doc = await get(config, id)
|
|
37
|
+
if (doc?._rev !== properties._rev) {
|
|
38
|
+
return {
|
|
39
|
+
statusCode: 409,
|
|
40
|
+
ok: false,
|
|
41
|
+
error: 'conflict'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const updatedDoc = { ...doc, ...properties }
|
|
46
|
+
logger.debug('Merged document:', updatedDoc)
|
|
47
|
+
const result = await put(config, updatedDoc)
|
|
48
|
+
logger.info(`Successfully patched document ${id}, rev: ${result.rev}`)
|
|
49
|
+
return result
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Patch a CouchDB document by merging provided properties.
|
|
54
|
+
* This function will retry on conflicts using an exponential backoff strategy.
|
|
55
|
+
*
|
|
56
|
+
* @remarks patchDangerously can clobber data. It will retry even if a conflict happens. There are some use cases for this, but you have been warned, hence the name.
|
|
57
|
+
*
|
|
58
|
+
* @param configInput - CouchDB configuration
|
|
59
|
+
* @param id - Document ID to patch
|
|
60
|
+
* @param properties - Properties to merge into the document
|
|
61
|
+
* @returns The result of the put operation or an error if max retries are exceeded
|
|
62
|
+
*
|
|
63
|
+
* @throws Error if max retries are exceeded or other errors occur
|
|
64
|
+
*/
|
|
65
|
+
export const patchDangerously = async (
|
|
66
|
+
configInput: CouchConfigInput,
|
|
67
|
+
id: string,
|
|
68
|
+
properties: Record<string, unknown>
|
|
69
|
+
) => {
|
|
70
|
+
const config = CouchConfig.parse(configInput)
|
|
71
|
+
const logger = createLogger(config)
|
|
72
|
+
const maxRetries = config.maxRetries || 5
|
|
73
|
+
let delay = config.initialDelay || 1000
|
|
74
|
+
let attempts = 0
|
|
75
|
+
|
|
76
|
+
logger.info(`Starting patch operation for document ${id}`)
|
|
77
|
+
logger.debug('Patch properties:', properties)
|
|
78
|
+
|
|
79
|
+
while (attempts <= maxRetries) {
|
|
80
|
+
logger.debug(`Attempt ${attempts + 1} of ${maxRetries + 1}`)
|
|
81
|
+
try {
|
|
82
|
+
const doc = await get(config, id)
|
|
83
|
+
if (!doc) {
|
|
84
|
+
logger.warn(`Document ${id} not found`)
|
|
85
|
+
return { ok: false, statusCode: 404, error: 'not_found' }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const updatedDoc = { ...doc, ...properties }
|
|
89
|
+
logger.debug('Merged document:', updatedDoc)
|
|
90
|
+
|
|
91
|
+
const result = await put(config, updatedDoc)
|
|
92
|
+
|
|
93
|
+
// Check if the response indicates a conflict
|
|
94
|
+
if (result.ok) {
|
|
95
|
+
logger.info(`Successfully patched document ${id}, rev: ${result.rev}`)
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// If not ok, treat as conflict and retry
|
|
100
|
+
attempts++
|
|
101
|
+
if (attempts > maxRetries) {
|
|
102
|
+
logger.error(`Failed to patch ${id} after ${maxRetries} attempts`)
|
|
103
|
+
throw new Error(`Failed to patch after ${maxRetries} attempts`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
logger.warn(`Conflict detected for ${id}, retrying (attempt ${attempts})`)
|
|
107
|
+
await setTimeout(delay)
|
|
108
|
+
delay *= config.backoffFactor || 2
|
|
109
|
+
logger.debug(`Next retry delay: ${delay}ms`)
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (
|
|
112
|
+
typeof err === 'object' &&
|
|
113
|
+
err !== null &&
|
|
114
|
+
'message' in err &&
|
|
115
|
+
err.message === 'not_found'
|
|
116
|
+
) {
|
|
117
|
+
logger.warn(`Document ${id} not found during patch operation`)
|
|
118
|
+
return { ok: false, statusCode: 404, error: 'not_found' }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Handle other errors (network, etc)
|
|
122
|
+
attempts++
|
|
123
|
+
if (attempts > maxRetries) {
|
|
124
|
+
const error = `Failed to patch after ${maxRetries} attempts: ${err}`
|
|
125
|
+
logger.error(error)
|
|
126
|
+
return { ok: false, statusCode: 500, error }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
logger.warn(`Error during patch attempt ${attempts}: ${err}`)
|
|
130
|
+
await setTimeout(delay)
|
|
131
|
+
logger.debug(`Retrying after ${delay}ms`)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
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 { get } from './get.mts'
|
|
6
|
+
import { patch, patchDangerously } from './patch.mts'
|
|
7
|
+
import { TEST_DB_URL } from '../test/setup-db.mts'
|
|
8
|
+
|
|
9
|
+
const baseConfig: CouchConfigInput = {
|
|
10
|
+
couch: TEST_DB_URL,
|
|
11
|
+
initialDelay: 10,
|
|
12
|
+
maxRetries: 2,
|
|
13
|
+
backoffFactor: 1.2
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type DocBody = Record<string, unknown>
|
|
17
|
+
|
|
18
|
+
async function saveDoc(id: string, body: DocBody) {
|
|
19
|
+
const response = await needle(
|
|
20
|
+
'put',
|
|
21
|
+
`${TEST_DB_URL}/${id}`,
|
|
22
|
+
{
|
|
23
|
+
_id: id,
|
|
24
|
+
...body
|
|
25
|
+
},
|
|
26
|
+
{ json: true }
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if (response.statusCode !== 201 && response.statusCode !== 200) {
|
|
30
|
+
throw new Error(`Failed to save document ${id}: ${response.statusCode}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return response.body as { rev: string }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
suite('patch', () => {
|
|
37
|
+
test('it will throw if provided config is invalid', async () => {
|
|
38
|
+
await assert.rejects(async () => {
|
|
39
|
+
// @ts-expect-error testing invalid config
|
|
40
|
+
await patch({ notAnOption: true, couch: DB_URL, useConsoleLogger: true }, 'doc1', {
|
|
41
|
+
foo: 'bar'
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
await assert.rejects(async () => {
|
|
46
|
+
await patchDangerously(
|
|
47
|
+
// @ts-expect-error testing invalid config
|
|
48
|
+
{ anotherBadOption: 123, couch: DB_URL, useConsoleLogger: true },
|
|
49
|
+
'doc1',
|
|
50
|
+
{ foo: 'bar' }
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('patch should throw if document revision is not provided', async () => {
|
|
56
|
+
await assert.rejects(
|
|
57
|
+
async () => {
|
|
58
|
+
// @ts-expect-error testing missing _rev
|
|
59
|
+
await patch(baseConfig, 'doc-no-rev', { foo: 'bar' })
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
message: /_rev is required for patch operations/
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('integration with pouchdb-server', async t => {
|
|
68
|
+
const patch_doc_id = `patch-doc-${Date.now()}`
|
|
69
|
+
const initial = await saveDoc(patch_doc_id, { message: 'original' })
|
|
70
|
+
|
|
71
|
+
await t.test('patch updates document when revision matches', async () => {
|
|
72
|
+
const result = await patch(baseConfig, patch_doc_id, {
|
|
73
|
+
_rev: initial.rev,
|
|
74
|
+
message: 'patched',
|
|
75
|
+
updated: true
|
|
76
|
+
})
|
|
77
|
+
assert.ok(result.ok)
|
|
78
|
+
assert.ok(result.rev)
|
|
79
|
+
|
|
80
|
+
const doc = await get(baseConfig, patch_doc_id)
|
|
81
|
+
assert.strictEqual(doc?.message, 'patched')
|
|
82
|
+
assert.strictEqual(doc?.updated, true)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
await t.test('patch returns conflict on stale revision', async () => {
|
|
86
|
+
const current = await get(baseConfig, patch_doc_id)
|
|
87
|
+
const staleRev = initial.rev
|
|
88
|
+
|
|
89
|
+
const conflict = await patch(baseConfig, patch_doc_id, {
|
|
90
|
+
_rev: staleRev,
|
|
91
|
+
message: 'should-fail'
|
|
92
|
+
})
|
|
93
|
+
assert.strictEqual(conflict.ok, false)
|
|
94
|
+
assert.strictEqual(conflict.statusCode, 409)
|
|
95
|
+
assert.strictEqual(conflict.error, 'conflict')
|
|
96
|
+
|
|
97
|
+
const doc = await get(baseConfig, patch_doc_id)
|
|
98
|
+
assert.strictEqual(doc?.message, current?.message)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
await t.test('patchDangerously merges properties without revision', async () => {
|
|
102
|
+
const result = await patchDangerously(baseConfig, patch_doc_id, {
|
|
103
|
+
description: 'dangerously updated'
|
|
104
|
+
})
|
|
105
|
+
assert.ok(result?.ok)
|
|
106
|
+
|
|
107
|
+
const doc = await get(baseConfig, patch_doc_id)
|
|
108
|
+
assert.strictEqual(doc?.description, 'dangerously updated')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
await t.test('patchDangerously returns not_found when document missing', async () => {
|
|
112
|
+
const response = await patchDangerously(baseConfig, 'missing-doc', {
|
|
113
|
+
message: 'noop'
|
|
114
|
+
})
|
|
115
|
+
assert.strictEqual(response?.ok, false)
|
|
116
|
+
assert.strictEqual(response?.statusCode, 404)
|
|
117
|
+
assert.strictEqual(response?.error, 'not_found')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
await t.test('patchDangerously reports failure after exhausting retries', async () => {
|
|
121
|
+
const doc = await get(baseConfig, patch_doc_id)
|
|
122
|
+
const conflictConfig: CouchConfigInput = {
|
|
123
|
+
...baseConfig,
|
|
124
|
+
maxRetries: 1,
|
|
125
|
+
initialDelay: 1,
|
|
126
|
+
backoffFactor: 1
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const response = await patchDangerously(conflictConfig, patch_doc_id, {
|
|
130
|
+
_rev: initial.rev,
|
|
131
|
+
conflicted: true
|
|
132
|
+
})
|
|
133
|
+
assert.strictEqual(response?.ok, false)
|
|
134
|
+
assert.strictEqual(response?.statusCode, 500)
|
|
135
|
+
assert.match(response?.error ?? '', /Failed to patch after 1 attempts/)
|
|
136
|
+
|
|
137
|
+
const current = await get(baseConfig, patch_doc_id)
|
|
138
|
+
assert.strictEqual(current?.conflicted, undefined)
|
|
139
|
+
assert.strictEqual(current?._rev, doc?._rev)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
})
|
package/impl/put.mts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
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 { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
|
|
6
|
+
import { CouchPutResponse, type CouchDoc } from '../schema/couch/couch.output.schema.ts'
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
|
|
9
|
+
export const put = async (
|
|
10
|
+
configInput: CouchConfigInput,
|
|
11
|
+
doc: CouchDoc
|
|
12
|
+
): Promise<z.infer<typeof CouchPutResponse>> => {
|
|
13
|
+
const config = CouchConfig.parse(configInput)
|
|
14
|
+
const logger = createLogger(config)
|
|
15
|
+
const url = `${config.couch}/${doc._id}`
|
|
16
|
+
const body = doc
|
|
17
|
+
const opts = {
|
|
18
|
+
json: true,
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'application/json'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const mergedOpts = mergeNeedleOpts(config, opts)
|
|
24
|
+
|
|
25
|
+
logger.info(`Putting document with id: ${doc._id}`)
|
|
26
|
+
let resp
|
|
27
|
+
try {
|
|
28
|
+
resp = await needle('put', url, body, mergedOpts)
|
|
29
|
+
} catch (err) {
|
|
30
|
+
logger.error('Error during put operation:', err)
|
|
31
|
+
RetryableError.handleNetworkError(err)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!resp) {
|
|
35
|
+
logger.error('No response received from put request')
|
|
36
|
+
throw new RetryableError('no response', 503)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const result = resp?.body || {}
|
|
40
|
+
result.statusCode = resp.statusCode
|
|
41
|
+
|
|
42
|
+
if (resp.statusCode === 409) {
|
|
43
|
+
logger.warn(`Conflict detected for document: ${doc._id}`)
|
|
44
|
+
result.ok = false
|
|
45
|
+
result.error = 'conflict'
|
|
46
|
+
return CouchPutResponse.parse(result)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
50
|
+
logger.warn(`Retryable status code received: ${resp.statusCode}`)
|
|
51
|
+
throw new RetryableError(result.reason || 'retryable error', resp.statusCode)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
logger.info(`Successfully saved document: ${doc._id}`)
|
|
55
|
+
return CouchPutResponse.parse(result)
|
|
56
|
+
}
|
|
@@ -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 type { CouchConfigInput } from '../schema/config.mts'
|
|
5
|
+
import { put } from './put.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 getDoc(id: string) {
|
|
16
|
+
return needle('get', `${TEST_DB_URL}/${id}`, null, { json: true })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function saveDoc(id: string, body: DocBody) {
|
|
20
|
+
const response = await needle('put', `${TEST_DB_URL}/${id}`, { _id: id, ...body }, { json: true })
|
|
21
|
+
if (response.statusCode !== 201 && response.statusCode !== 200) {
|
|
22
|
+
throw new Error(`Failed to save document ${id}: ${response.statusCode}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return response.body as { rev: string }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
suite('put', () => {
|
|
29
|
+
test('rejects invalid config arguments', async () => {
|
|
30
|
+
await assert.rejects(async () => {
|
|
31
|
+
// @ts-expect-error testing invalid config
|
|
32
|
+
await put({ couch: DB_URL, unsupported: true }, { _id: 'bad-config-doc' })
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('propagates retryable network failures', async () => {
|
|
37
|
+
const offlineConfig: CouchConfigInput = {
|
|
38
|
+
couch: 'http://localhost:6555/offline-put-test'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await assert.rejects(
|
|
42
|
+
() => put(offlineConfig, { _id: 'offline-doc', kind: 'offline' }),
|
|
43
|
+
(err: unknown) => err instanceof RetryableError && err.statusCode === 503
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('integration with pouchdb-server', async t => {
|
|
48
|
+
let initialRev: string | undefined
|
|
49
|
+
const put_doc_id = `put-doc-${Date.now()}`
|
|
50
|
+
|
|
51
|
+
await t.test('creates documents via PUT', async () => {
|
|
52
|
+
const result = await put(baseConfig, {
|
|
53
|
+
_id: put_doc_id,
|
|
54
|
+
type: 'integration',
|
|
55
|
+
count: 1
|
|
56
|
+
})
|
|
57
|
+
assert.ok(result)
|
|
58
|
+
assert.strictEqual(result.ok, true)
|
|
59
|
+
assert.strictEqual(result.id, put_doc_id)
|
|
60
|
+
assert.strictEqual(result.statusCode, 201)
|
|
61
|
+
assert.ok(typeof result.rev === 'string')
|
|
62
|
+
initialRev = result.rev
|
|
63
|
+
|
|
64
|
+
const { statusCode, body } = await getDoc(put_doc_id)
|
|
65
|
+
assert.strictEqual(statusCode, 200)
|
|
66
|
+
assert.strictEqual(body?.type, 'integration')
|
|
67
|
+
assert.strictEqual(body?.count, 1)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
await t.test('updates documents when revision supplied', async () => {
|
|
71
|
+
if (!initialRev) throw new Error('Expected initial revision to be captured')
|
|
72
|
+
|
|
73
|
+
const updateResult = await put(baseConfig, {
|
|
74
|
+
_id: put_doc_id,
|
|
75
|
+
_rev: initialRev,
|
|
76
|
+
type: 'integration',
|
|
77
|
+
count: 2
|
|
78
|
+
})
|
|
79
|
+
assert.ok(updateResult)
|
|
80
|
+
assert.strictEqual(updateResult.ok, true)
|
|
81
|
+
assert.strictEqual(updateResult.statusCode, 201)
|
|
82
|
+
assert.ok(typeof updateResult.rev === 'string')
|
|
83
|
+
|
|
84
|
+
const { body } = await getDoc(put_doc_id)
|
|
85
|
+
assert.strictEqual(body?.count, 2)
|
|
86
|
+
initialRev = updateResult.rev
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
await t.test('reports conflicts when revision is stale', async () => {
|
|
90
|
+
if (!initialRev) throw new Error('Expected revision to be captured')
|
|
91
|
+
const staleRev = initialRev
|
|
92
|
+
const latest = await saveDoc(put_doc_id, {
|
|
93
|
+
_rev: staleRev,
|
|
94
|
+
type: 'integration',
|
|
95
|
+
count: 3
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const result = await put(baseConfig, {
|
|
99
|
+
_id: put_doc_id,
|
|
100
|
+
_rev: staleRev,
|
|
101
|
+
type: 'integration',
|
|
102
|
+
count: 4
|
|
103
|
+
})
|
|
104
|
+
assert.ok(result)
|
|
105
|
+
assert.strictEqual(result.ok, false)
|
|
106
|
+
assert.strictEqual(result.error, 'conflict')
|
|
107
|
+
assert.strictEqual(result.statusCode, 409)
|
|
108
|
+
|
|
109
|
+
const { body } = await getDoc(put_doc_id)
|
|
110
|
+
assert.strictEqual(body?.count, 3)
|
|
111
|
+
initialRev = latest.rev
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
})
|
package/impl/query.mts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import needle, { type BodyData, type NeedleHttpVerbs } from 'needle'
|
|
2
|
+
import { createLogger } from './utils/logger.mts'
|
|
3
|
+
|
|
4
|
+
import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
|
|
5
|
+
import { z, ZodAny, ZodNever } from 'zod'
|
|
6
|
+
import { queryString } from './utils/queryString.mts'
|
|
7
|
+
import { mergeNeedleOpts } from './utils/mergeNeedleOpts.mts'
|
|
8
|
+
import { RetryableError } from './utils/errors.mts'
|
|
9
|
+
import { ViewOptions, type ViewString } from '../schema/couch/couch.input.schema.ts'
|
|
10
|
+
import type { CouchDoc, ViewQueryResponseValidated } from '../schema/couch/couch.output.schema.ts'
|
|
11
|
+
import type { StandardSchemaV1 } from '../types/standard-schema.ts'
|
|
12
|
+
import { parseRows, type OnInvalidDocAction } from './utils/parseRows.mts'
|
|
13
|
+
|
|
14
|
+
export async function query<
|
|
15
|
+
DocSchema extends StandardSchemaV1 = typeof CouchDoc,
|
|
16
|
+
KeySchema extends StandardSchemaV1 = ZodAny,
|
|
17
|
+
ValueSchema extends StandardSchemaV1 = ZodAny
|
|
18
|
+
>(
|
|
19
|
+
config: CouchConfigInput,
|
|
20
|
+
view: ViewString,
|
|
21
|
+
options: ViewOptions & {
|
|
22
|
+
include_docs: true
|
|
23
|
+
validate?: {
|
|
24
|
+
onInvalidDoc?: OnInvalidDocAction
|
|
25
|
+
docSchema?: DocSchema
|
|
26
|
+
keySchema?: KeySchema
|
|
27
|
+
valueSchema?: ValueSchema
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
): Promise<ViewQueryResponseValidated<DocSchema, KeySchema, ValueSchema>>
|
|
31
|
+
|
|
32
|
+
export async function query<
|
|
33
|
+
DocSchema extends StandardSchemaV1 = ZodNever,
|
|
34
|
+
KeySchema extends StandardSchemaV1 = ZodAny,
|
|
35
|
+
ValueSchema extends StandardSchemaV1 = ZodAny
|
|
36
|
+
>(
|
|
37
|
+
config: CouchConfigInput,
|
|
38
|
+
view: ViewString,
|
|
39
|
+
options: ViewOptions & {
|
|
40
|
+
include_docs?: false | undefined
|
|
41
|
+
validate?: {
|
|
42
|
+
onInvalidDoc?: OnInvalidDocAction
|
|
43
|
+
docSchema?: DocSchema
|
|
44
|
+
keySchema?: KeySchema
|
|
45
|
+
valueSchema?: ValueSchema
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
): Promise<ViewQueryResponseValidated<ZodNever, KeySchema, ValueSchema>>
|
|
49
|
+
|
|
50
|
+
export async function query(
|
|
51
|
+
config: CouchConfigInput,
|
|
52
|
+
view: ViewString,
|
|
53
|
+
options?: ViewOptions
|
|
54
|
+
): Promise<ViewQueryResponseValidated<ZodNever, ZodAny, ZodAny>>
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Executes a CouchDB view query with optional schema validation and automatic handling
|
|
58
|
+
* of HTTP method selection, query string construction, and retryable errors.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* When using the validation feature, each row in the response will be validated against the provided
|
|
62
|
+
* Types will be inferred from the StandardSchemaV1 supplied in the `options.validate` object.
|
|
63
|
+
*
|
|
64
|
+
* @template DocSchema - StandardSchemaV1 used to validate each returned `doc`, if provided.
|
|
65
|
+
* @template KeySchema - StandardSchemaV1 used to validate each row `key`, if provided.
|
|
66
|
+
* @template ValueSchema - StandardSchemaV1 used to validate each row `value`, if provided.
|
|
67
|
+
*
|
|
68
|
+
* @param _config - CouchDB configuration data that is validated before use.
|
|
69
|
+
* @param view - Fully qualified design document and view identifier (e.g., `_design/foo/_view/bar`).
|
|
70
|
+
* @param options - CouchDB view options, including optional validation schemas.
|
|
71
|
+
*
|
|
72
|
+
* @returns The parsed view response with rows validated against the supplied schemas.
|
|
73
|
+
*
|
|
74
|
+
* @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
|
|
75
|
+
* @throws {Error<Array<StandardSchemaV1.Issue>>} When the configuration or validation schemas fail to parse.
|
|
76
|
+
* @throws {Error} When CouchDB returns a non-retryable error payload.
|
|
77
|
+
*/
|
|
78
|
+
export async function query<
|
|
79
|
+
DocSchema extends StandardSchemaV1,
|
|
80
|
+
KeySchema extends StandardSchemaV1,
|
|
81
|
+
ValueSchema extends StandardSchemaV1
|
|
82
|
+
>(
|
|
83
|
+
_config: CouchConfigInput,
|
|
84
|
+
view: ViewString,
|
|
85
|
+
options: ViewOptions & {
|
|
86
|
+
validate?: {
|
|
87
|
+
onInvalidDoc?: OnInvalidDocAction
|
|
88
|
+
docSchema?: DocSchema
|
|
89
|
+
keySchema?: KeySchema
|
|
90
|
+
valueSchema?: ValueSchema
|
|
91
|
+
}
|
|
92
|
+
} = {}
|
|
93
|
+
): Promise<ViewQueryResponseValidated<DocSchema, KeySchema, ValueSchema>> {
|
|
94
|
+
const configParseResult = CouchConfig.safeParse(_config)
|
|
95
|
+
const logger = createLogger(_config)
|
|
96
|
+
logger.info(`Starting view query: ${view}`)
|
|
97
|
+
logger.debug('Query options:', ViewOptions.parse(options || {}))
|
|
98
|
+
if (!configParseResult.success) {
|
|
99
|
+
logger.error(`Invalid configuration provided: ${z.prettifyError(configParseResult.error)}`)
|
|
100
|
+
throw configParseResult.error
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const config = configParseResult.data
|
|
104
|
+
|
|
105
|
+
let qs = queryString(options)
|
|
106
|
+
let method: NeedleHttpVerbs = 'get'
|
|
107
|
+
let payload: BodyData = null
|
|
108
|
+
|
|
109
|
+
const opts = {
|
|
110
|
+
json: true,
|
|
111
|
+
headers: {
|
|
112
|
+
'Content-Type': 'application/json'
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const mergedOpts = mergeNeedleOpts(config, opts)
|
|
117
|
+
|
|
118
|
+
// If keys are supplied, issue a POST to circumvent GET query string limits
|
|
119
|
+
// see http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options
|
|
120
|
+
if (typeof options.keys !== 'undefined') {
|
|
121
|
+
const MAX_URL_LENGTH = 2000
|
|
122
|
+
// according to http://stackoverflow.com/a/417184/680742,
|
|
123
|
+
// the de facto URL length limit is 2000 characters
|
|
124
|
+
|
|
125
|
+
const _options = structuredClone(options)
|
|
126
|
+
delete _options.keys
|
|
127
|
+
qs = queryString(_options)
|
|
128
|
+
|
|
129
|
+
const keysAsString = `keys=${JSON.stringify(options.keys)}`
|
|
130
|
+
|
|
131
|
+
if (keysAsString.length + qs.length + 1 <= MAX_URL_LENGTH) {
|
|
132
|
+
// If the keys are short enough, do a GET. we do this to work around
|
|
133
|
+
// Safari not understanding 304s on POSTs (see pouchdb/pouchdb#1239)
|
|
134
|
+
method = 'get'
|
|
135
|
+
if (qs.length > 0) qs += '&'
|
|
136
|
+
else qs = ''
|
|
137
|
+
qs += keysAsString
|
|
138
|
+
} else {
|
|
139
|
+
method = 'post'
|
|
140
|
+
payload = { keys: options.keys }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
logger.debug('Generated query string:', qs)
|
|
145
|
+
const url = `${config.couch}/${view}?${qs}`
|
|
146
|
+
let results
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
logger.debug(`Sending ${method} request to: ${url}`)
|
|
150
|
+
results =
|
|
151
|
+
method === 'get'
|
|
152
|
+
? await needle('get', url, mergedOpts)
|
|
153
|
+
: await needle('post', url, payload, mergedOpts)
|
|
154
|
+
} catch (err) {
|
|
155
|
+
logger.error('Network error during query:', err)
|
|
156
|
+
RetryableError.handleNetworkError(err)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!results) {
|
|
160
|
+
logger.error('No response received from query request')
|
|
161
|
+
throw new RetryableError('no response', 503)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const body = results.body
|
|
165
|
+
|
|
166
|
+
if (RetryableError.isRetryableStatusCode(results.statusCode)) {
|
|
167
|
+
logger.warn(`Retryable status code received: ${results.statusCode}`)
|
|
168
|
+
throw new RetryableError(body.error || 'retryable error during query', results.statusCode)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (body.error) {
|
|
172
|
+
logger.error(`Query error: ${JSON.stringify(body)}`)
|
|
173
|
+
throw new Error(`CouchDB query error: ${body.error} - ${body.reason || ''}`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// If validation schemas are provided, validate each row accordingly
|
|
177
|
+
if (options.validate && body.rows) {
|
|
178
|
+
body.rows = await parseRows<DocSchema, KeySchema, ValueSchema>(body.rows, options.validate)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
logger.info(`Successfully executed view query: ${view}`)
|
|
182
|
+
logger.debug('Query response:', body)
|
|
183
|
+
|
|
184
|
+
return body
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export type QueryBound = {
|
|
188
|
+
<
|
|
189
|
+
DocSchema extends StandardSchemaV1 = typeof CouchDoc,
|
|
190
|
+
KeySchema extends StandardSchemaV1 = ZodAny,
|
|
191
|
+
ValueSchema extends StandardSchemaV1 = ZodAny
|
|
192
|
+
>(
|
|
193
|
+
view: ViewString,
|
|
194
|
+
options: ViewOptions & {
|
|
195
|
+
include_docs: true
|
|
196
|
+
validate?: {
|
|
197
|
+
onInvalidDoc?: OnInvalidDocAction
|
|
198
|
+
docSchema?: DocSchema
|
|
199
|
+
keySchema?: KeySchema
|
|
200
|
+
valueSchema?: ValueSchema
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
): Promise<ViewQueryResponseValidated<DocSchema, KeySchema, ValueSchema>>
|
|
204
|
+
<
|
|
205
|
+
DocSchema extends StandardSchemaV1 = ZodNever,
|
|
206
|
+
KeySchema extends StandardSchemaV1 = ZodAny,
|
|
207
|
+
ValueSchema extends StandardSchemaV1 = ZodAny
|
|
208
|
+
>(
|
|
209
|
+
view: ViewString,
|
|
210
|
+
options: ViewOptions & {
|
|
211
|
+
include_docs?: false | undefined
|
|
212
|
+
validate?: {
|
|
213
|
+
onInvalidDoc?: OnInvalidDocAction
|
|
214
|
+
docSchema?: DocSchema
|
|
215
|
+
keySchema?: KeySchema
|
|
216
|
+
valueSchema?: ValueSchema
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
): Promise<ViewQueryResponseValidated<ZodNever, KeySchema, ValueSchema>>
|
|
220
|
+
(
|
|
221
|
+
view: ViewString,
|
|
222
|
+
options?: ViewOptions
|
|
223
|
+
): Promise<ViewQueryResponseValidated<ZodNever, ZodAny, ZodAny>>
|
|
224
|
+
}
|