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,155 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test, { suite } from 'node:test'
|
|
3
|
+
import { EventEmitter } from 'node:events'
|
|
4
|
+
import needle from 'needle'
|
|
5
|
+
import type { CouchConfigInput } from '../../schema/config.mts'
|
|
6
|
+
import { watchDocs } from './watch.mts'
|
|
7
|
+
|
|
8
|
+
class FakeRequest extends EventEmitter {
|
|
9
|
+
destroyed = false
|
|
10
|
+
|
|
11
|
+
destroy() {
|
|
12
|
+
this.destroyed = true
|
|
13
|
+
this.emit('close')
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type NeedleRequest = ReturnType<typeof needle.get>
|
|
18
|
+
|
|
19
|
+
const baseConfig = (): CouchConfigInput => ({
|
|
20
|
+
couch: 'http://localhost:5984/watch-test'
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const waitFor = async (predicate: () => boolean, timeoutMs = 2000, intervalMs = 10) => {
|
|
24
|
+
const startedAt = Date.now()
|
|
25
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
26
|
+
if (predicate()) return
|
|
27
|
+
await new Promise(resolve => {
|
|
28
|
+
setTimeout(resolve, intervalMs)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
throw new Error('waitFor timed out')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
suite('watchDocs', () => {
|
|
35
|
+
test('requires at least one document id', () => {
|
|
36
|
+
assert.throws(() => {
|
|
37
|
+
watchDocs(baseConfig(), [], () => {})
|
|
38
|
+
}, /non-empty array/)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('rejects more than 100 document ids', () => {
|
|
42
|
+
const ids = Array.from({ length: 101 }, (_, index) => `doc-${index}`)
|
|
43
|
+
assert.throws(() => {
|
|
44
|
+
watchDocs(baseConfig(), ids, () => {})
|
|
45
|
+
}, /100 or fewer elements/)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('emits change events for streamed chunks', async t => {
|
|
49
|
+
const requests: FakeRequest[] = []
|
|
50
|
+
const getMock = t.mock.method(needle, 'get', () => {
|
|
51
|
+
const request = new FakeRequest()
|
|
52
|
+
requests.push(request)
|
|
53
|
+
return request as unknown as NeedleRequest
|
|
54
|
+
})
|
|
55
|
+
t.after(() => {
|
|
56
|
+
getMock.mock.restore()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
+
const changes: any[] = []
|
|
61
|
+
const watcher = watchDocs(
|
|
62
|
+
baseConfig(),
|
|
63
|
+
['doc-a', 'doc-b'],
|
|
64
|
+
change => {
|
|
65
|
+
changes.push(change)
|
|
66
|
+
},
|
|
67
|
+
{ include_docs: true }
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
await waitFor(() => requests.length === 1)
|
|
71
|
+
const firstRequest = requests[0]
|
|
72
|
+
|
|
73
|
+
firstRequest.emit('data', Buffer.from('{"id":"doc-a","seq":"1"}\n{"id":"doc-b","seq":"2"}\n'))
|
|
74
|
+
|
|
75
|
+
await waitFor(() => changes.length === 2)
|
|
76
|
+
|
|
77
|
+
assert.deepStrictEqual(
|
|
78
|
+
changes.map(change => change.id),
|
|
79
|
+
['doc-a', 'doc-b']
|
|
80
|
+
)
|
|
81
|
+
const firstArg = getMock.mock.calls[0].arguments[0]
|
|
82
|
+
if (typeof firstArg !== 'string') {
|
|
83
|
+
throw new Error('Expected first argument to be a string')
|
|
84
|
+
}
|
|
85
|
+
assert.match(firstArg, /include_docs=true/)
|
|
86
|
+
assert.match(firstArg, /doc_ids=\["doc-a","doc-b"\]/)
|
|
87
|
+
|
|
88
|
+
watcher.stop()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('reconnects after retryable response status', async t => {
|
|
92
|
+
const requests: FakeRequest[] = []
|
|
93
|
+
const getMock = t.mock.method(needle, 'get', () => {
|
|
94
|
+
const request = new FakeRequest()
|
|
95
|
+
requests.push(request)
|
|
96
|
+
return request as unknown as NeedleRequest
|
|
97
|
+
})
|
|
98
|
+
t.after(() => {
|
|
99
|
+
getMock.mock.restore()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const watcher = watchDocs(baseConfig(), 'doc-retry', () => {}, {
|
|
103
|
+
initialDelay: 1,
|
|
104
|
+
maxDelay: 1,
|
|
105
|
+
maxRetries: 3
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
await waitFor(() => requests.length === 1)
|
|
109
|
+
const firstRequest = requests[0]
|
|
110
|
+
firstRequest.emit('response', { statusCode: 503 })
|
|
111
|
+
|
|
112
|
+
await waitFor(() => requests.length === 2)
|
|
113
|
+
|
|
114
|
+
assert.ok(firstRequest.destroyed)
|
|
115
|
+
assert.strictEqual(getMock.mock.callCount(), 2)
|
|
116
|
+
|
|
117
|
+
watcher.stop()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('emits error after exhausting retries', async t => {
|
|
121
|
+
const requests: FakeRequest[] = []
|
|
122
|
+
const getMock = t.mock.method(needle, 'get', () => {
|
|
123
|
+
const request = new FakeRequest()
|
|
124
|
+
requests.push(request)
|
|
125
|
+
return request as unknown as NeedleRequest
|
|
126
|
+
})
|
|
127
|
+
t.after(() => {
|
|
128
|
+
getMock.mock.restore()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const errors: Error[] = []
|
|
132
|
+
const watcher = watchDocs(baseConfig(), 'doc-max', () => {}, {
|
|
133
|
+
maxRetries: 2,
|
|
134
|
+
initialDelay: 1,
|
|
135
|
+
maxDelay: 1
|
|
136
|
+
})
|
|
137
|
+
watcher.on('error', err => {
|
|
138
|
+
errors.push(err as unknown as Error)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
await waitFor(() => requests.length === 1)
|
|
142
|
+
requests[0].emit('error', { code: 'ECONNRESET' })
|
|
143
|
+
|
|
144
|
+
await waitFor(() => requests.length === 2)
|
|
145
|
+
requests[1].emit('error', { code: 'ECONNRESET' })
|
|
146
|
+
|
|
147
|
+
await waitFor(() => requests.length === 3)
|
|
148
|
+
requests[2].emit('error', { code: 'ECONNRESET' })
|
|
149
|
+
|
|
150
|
+
await waitFor(() => errors.length === 1)
|
|
151
|
+
assert.strictEqual(errors[0].message, 'Max retries reached')
|
|
152
|
+
|
|
153
|
+
watcher.stop()
|
|
154
|
+
})
|
|
155
|
+
})
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a network-level error emitted by Node.js or libraries such as `needle`.
|
|
3
|
+
*
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export interface NetworkError {
|
|
7
|
+
/**
|
|
8
|
+
* Machine-readable error code describing the network failure.
|
|
9
|
+
*/
|
|
10
|
+
code: string
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Optional human-readable message supplied by the underlying library.
|
|
14
|
+
*/
|
|
15
|
+
message?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504])
|
|
19
|
+
|
|
20
|
+
const NETWORK_ERROR_STATUS_MAP = {
|
|
21
|
+
ECONNREFUSED: 503,
|
|
22
|
+
ECONNRESET: 503,
|
|
23
|
+
ETIMEDOUT: 503,
|
|
24
|
+
ENETUNREACH: 503,
|
|
25
|
+
ENOTFOUND: 503,
|
|
26
|
+
EPIPE: 503,
|
|
27
|
+
EHOSTUNREACH: 503,
|
|
28
|
+
ESOCKETTIMEDOUT: 503
|
|
29
|
+
} as const satisfies Record<string, number>
|
|
30
|
+
|
|
31
|
+
type NetworkErrorCode = keyof typeof NETWORK_ERROR_STATUS_MAP
|
|
32
|
+
|
|
33
|
+
const isNetworkError = (value: unknown): value is NetworkError & { code: NetworkErrorCode } => {
|
|
34
|
+
if (typeof value !== 'object' || value === null) return false
|
|
35
|
+
const candidate = value as { code?: unknown }
|
|
36
|
+
return typeof candidate.code === 'string' && candidate.code in NETWORK_ERROR_STATUS_MAP
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Error thrown when a requested CouchDB document cannot be found.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* The `docId` property exposes the identifier that triggered the failure, which is
|
|
44
|
+
* helpful for logging and retry strategies.
|
|
45
|
+
*
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
export class NotFoundError extends Error {
|
|
49
|
+
/**
|
|
50
|
+
* Identifier of the missing document.
|
|
51
|
+
*/
|
|
52
|
+
readonly docId: string
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a new {@link NotFoundError} instance.
|
|
56
|
+
*
|
|
57
|
+
* @param docId - The identifier of the document that was not found.
|
|
58
|
+
* @param message - Optional custom error message.
|
|
59
|
+
*/
|
|
60
|
+
constructor(docId: string, message = 'Document not found') {
|
|
61
|
+
super(message)
|
|
62
|
+
this.name = 'NotFoundError'
|
|
63
|
+
this.docId = docId
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Error signalling that an operation can be retried due to transient conditions.
|
|
69
|
+
*
|
|
70
|
+
* @remarks
|
|
71
|
+
* Use `RetryableError.isRetryableStatusCode` and `RetryableError.handleNetworkError`
|
|
72
|
+
* to detect when a failure should trigger retry logic.
|
|
73
|
+
*
|
|
74
|
+
* @public
|
|
75
|
+
*/
|
|
76
|
+
export class RetryableError extends Error {
|
|
77
|
+
/**
|
|
78
|
+
* HTTP status code associated with the retryable failure, when available.
|
|
79
|
+
*/
|
|
80
|
+
readonly statusCode?: number
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Creates a new {@link RetryableError} instance.
|
|
84
|
+
*
|
|
85
|
+
* @param message - Detailed description of the failure.
|
|
86
|
+
* @param statusCode - Optional HTTP status code corresponding to the failure.
|
|
87
|
+
*/
|
|
88
|
+
constructor(message: string, statusCode?: number) {
|
|
89
|
+
super(message)
|
|
90
|
+
this.name = 'RetryableError'
|
|
91
|
+
this.statusCode = statusCode
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Determines whether the provided status code should be treated as retryable.
|
|
96
|
+
*
|
|
97
|
+
* @param statusCode - HTTP status code returned by CouchDB.
|
|
98
|
+
*
|
|
99
|
+
* @returns `true` if the status code is considered retryable; otherwise `false`.
|
|
100
|
+
*/
|
|
101
|
+
static isRetryableStatusCode(statusCode: number | undefined): statusCode is number {
|
|
102
|
+
if (typeof statusCode !== 'number') return false
|
|
103
|
+
return RETRYABLE_STATUS_CODES.has(statusCode)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Converts low-level network errors into {@link RetryableError} instances when possible.
|
|
108
|
+
*
|
|
109
|
+
* @param err - The error thrown by the underlying HTTP client.
|
|
110
|
+
*
|
|
111
|
+
* @throws {@link RetryableError} When the error maps to a retryable network condition.
|
|
112
|
+
* @throws {*} Re-throws the original error when it cannot be mapped.
|
|
113
|
+
*/
|
|
114
|
+
static handleNetworkError(err: unknown): never {
|
|
115
|
+
if (isNetworkError(err)) {
|
|
116
|
+
const statusCode = NETWORK_ERROR_STATUS_MAP[err.code]
|
|
117
|
+
if (statusCode) {
|
|
118
|
+
throw new RetryableError(`Network error: ${err.code}`, statusCode)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw err
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isConflictError(err: unknown): boolean {
|
|
127
|
+
if (typeof err !== 'object' || err === null) return false
|
|
128
|
+
const candidate = err as { statusCode?: unknown }
|
|
129
|
+
return candidate.statusCode === 409
|
|
130
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test, { suite } from 'node:test'
|
|
3
|
+
import { NotFoundError, RetryableError, isConflictError } from './errors.mts'
|
|
4
|
+
|
|
5
|
+
suite('errors', () => {
|
|
6
|
+
test('NotFoundError exposes docId and message', () => {
|
|
7
|
+
const err = new NotFoundError('doc-123')
|
|
8
|
+
assert.strictEqual(err.name, 'NotFoundError')
|
|
9
|
+
assert.strictEqual(err.message, 'Document not found')
|
|
10
|
+
assert.strictEqual(err.docId, 'doc-123')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('NotFoundError accepts custom message', () => {
|
|
14
|
+
const err = new NotFoundError('doc-456', 'missing doc')
|
|
15
|
+
assert.strictEqual(err.message, 'missing doc')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('RetryableError.isRetryableStatusCode identifies retryable statuses', () => {
|
|
19
|
+
const retryable = [408, 429, 500, 502, 503, 504]
|
|
20
|
+
for (const status of retryable) {
|
|
21
|
+
assert.strictEqual(RetryableError.isRetryableStatusCode(status), true)
|
|
22
|
+
}
|
|
23
|
+
assert.strictEqual(RetryableError.isRetryableStatusCode(404), false)
|
|
24
|
+
assert.strictEqual(RetryableError.isRetryableStatusCode(undefined), false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('handleNetworkError wraps known network failures', () => {
|
|
28
|
+
assert.throws(
|
|
29
|
+
() => RetryableError.handleNetworkError({ code: 'ECONNRESET' }),
|
|
30
|
+
(err: unknown) =>
|
|
31
|
+
err instanceof RetryableError &&
|
|
32
|
+
err.statusCode === 503 &&
|
|
33
|
+
err.message.includes('ECONNRESET')
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('handleNetworkError rethrows unknown errors', () => {
|
|
38
|
+
const original = new Error('boom')
|
|
39
|
+
assert.throws(
|
|
40
|
+
() => RetryableError.handleNetworkError(original),
|
|
41
|
+
(err: unknown) => err === original
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('handleNetworkError rethrows unrecognized network codes', () => {
|
|
46
|
+
const networkErr = { code: 'UNKNOWN' }
|
|
47
|
+
assert.throws(
|
|
48
|
+
() => RetryableError.handleNetworkError(networkErr),
|
|
49
|
+
(err: unknown) => err === networkErr
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('isConflictError detects statusCode 409', () => {
|
|
54
|
+
assert.strictEqual(isConflictError({ statusCode: 409 }), true)
|
|
55
|
+
assert.strictEqual(isConflictError({ statusCode: 412 }), false)
|
|
56
|
+
assert.strictEqual(isConflictError(null), false)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { CouchConfigInput } from '../../schema/config.mts'
|
|
2
|
+
|
|
3
|
+
type LoggerMethod = (...args: unknown[]) => void
|
|
4
|
+
|
|
5
|
+
export type Logger = {
|
|
6
|
+
error: LoggerMethod
|
|
7
|
+
warn: LoggerMethod
|
|
8
|
+
info: LoggerMethod
|
|
9
|
+
debug: LoggerMethod
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type FunctionLogger = (level: keyof Logger, ...args: unknown[]) => void
|
|
13
|
+
|
|
14
|
+
const noop: LoggerMethod = () => {}
|
|
15
|
+
|
|
16
|
+
const createConsoleLogger = (): Logger => ({
|
|
17
|
+
error: (...args) => console.error(...args),
|
|
18
|
+
warn: (...args) => console.warn(...args),
|
|
19
|
+
info: (...args) => console.info(...args),
|
|
20
|
+
debug: (...args) => console.debug(...args)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const createNoopLogger = (): Logger => ({
|
|
24
|
+
error: noop,
|
|
25
|
+
warn: noop,
|
|
26
|
+
info: noop,
|
|
27
|
+
debug: noop
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export function createLogger(config: CouchConfigInput): Logger {
|
|
31
|
+
if (config['~normalizedLogger']) {
|
|
32
|
+
return config['~normalizedLogger']
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!config.logger) {
|
|
36
|
+
const normalized = config.useConsoleLogger ? createConsoleLogger() : createNoopLogger()
|
|
37
|
+
config['~normalizedLogger'] = normalized
|
|
38
|
+
return normalized
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof config.logger === 'function') {
|
|
42
|
+
const loggerFn = config.logger as FunctionLogger
|
|
43
|
+
const normalized: Logger = {
|
|
44
|
+
error: (...args) => loggerFn('error', ...args),
|
|
45
|
+
warn: (...args) => loggerFn('warn', ...args),
|
|
46
|
+
info: (...args) => loggerFn('info', ...args),
|
|
47
|
+
debug: (...args) => loggerFn('debug', ...args)
|
|
48
|
+
}
|
|
49
|
+
config['~normalizedLogger'] = normalized
|
|
50
|
+
return normalized
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const loggerObj = config.logger as Partial<Logger>
|
|
54
|
+
const normalized: Logger = {
|
|
55
|
+
error: loggerObj.error ?? noop,
|
|
56
|
+
warn: loggerObj.warn ?? noop,
|
|
57
|
+
info: loggerObj.info ?? noop,
|
|
58
|
+
debug: loggerObj.debug ?? noop
|
|
59
|
+
}
|
|
60
|
+
config['~normalizedLogger'] = normalized
|
|
61
|
+
return normalized
|
|
62
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test, { suite } from 'node:test'
|
|
3
|
+
import type { CouchConfigInput } from '../../schema/config.mts'
|
|
4
|
+
import { createLogger, type Logger } from './logger.mts'
|
|
5
|
+
|
|
6
|
+
const baseConfig = (): CouchConfigInput => ({
|
|
7
|
+
couch: 'http://localhost:5984'
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
suite('createLogger', () => {
|
|
11
|
+
test('returns cached logger when present', () => {
|
|
12
|
+
const cached: Logger = {
|
|
13
|
+
error: () => {},
|
|
14
|
+
warn: () => {},
|
|
15
|
+
info: () => {},
|
|
16
|
+
debug: () => {}
|
|
17
|
+
}
|
|
18
|
+
const config: CouchConfigInput = {
|
|
19
|
+
...baseConfig(),
|
|
20
|
+
'~normalizedLogger': cached
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const logger = createLogger(config)
|
|
24
|
+
|
|
25
|
+
assert.strictEqual(logger, cached)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('uses console logger when requested', () => {
|
|
29
|
+
const errorCalls: unknown[][] = []
|
|
30
|
+
const warnCalls: unknown[][] = []
|
|
31
|
+
const infoCalls: unknown[][] = []
|
|
32
|
+
const debugCalls: unknown[][] = []
|
|
33
|
+
|
|
34
|
+
const originalError = console.error
|
|
35
|
+
const originalWarn = console.warn
|
|
36
|
+
const originalInfo = console.info
|
|
37
|
+
const originalDebug = console.debug
|
|
38
|
+
|
|
39
|
+
console.error = (...args: unknown[]) => {
|
|
40
|
+
errorCalls.push(args)
|
|
41
|
+
}
|
|
42
|
+
console.warn = (...args: unknown[]) => {
|
|
43
|
+
warnCalls.push(args)
|
|
44
|
+
}
|
|
45
|
+
console.info = (...args: unknown[]) => {
|
|
46
|
+
infoCalls.push(args)
|
|
47
|
+
}
|
|
48
|
+
console.debug = (...args: unknown[]) => {
|
|
49
|
+
debugCalls.push(args)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const config: CouchConfigInput = {
|
|
54
|
+
...baseConfig(),
|
|
55
|
+
useConsoleLogger: true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const logger = createLogger(config)
|
|
59
|
+
|
|
60
|
+
logger.error('boom')
|
|
61
|
+
logger.warn('warn', 123)
|
|
62
|
+
logger.info('info')
|
|
63
|
+
logger.debug('debug')
|
|
64
|
+
|
|
65
|
+
assert.strictEqual(config['~normalizedLogger'], logger)
|
|
66
|
+
assert.deepStrictEqual(errorCalls, [['boom']])
|
|
67
|
+
assert.deepStrictEqual(warnCalls, [['warn', 123]])
|
|
68
|
+
assert.deepStrictEqual(infoCalls, [['info']])
|
|
69
|
+
assert.deepStrictEqual(debugCalls, [['debug']])
|
|
70
|
+
} finally {
|
|
71
|
+
console.error = originalError
|
|
72
|
+
console.warn = originalWarn
|
|
73
|
+
console.info = originalInfo
|
|
74
|
+
console.debug = originalDebug
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('creates no-op logger when none provided', () => {
|
|
79
|
+
const config = baseConfig()
|
|
80
|
+
const logger = createLogger(config)
|
|
81
|
+
|
|
82
|
+
assert.strictEqual(config['~normalizedLogger'], logger)
|
|
83
|
+
assert.doesNotThrow(() => logger.error('noop'))
|
|
84
|
+
assert.doesNotThrow(() => logger.warn('noop'))
|
|
85
|
+
assert.doesNotThrow(() => logger.info('noop'))
|
|
86
|
+
assert.doesNotThrow(() => logger.debug('noop'))
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('wraps function logger', () => {
|
|
90
|
+
const calls: Array<{ level: string; args: unknown[] }> = []
|
|
91
|
+
const fnLogger = (level: string, ...args: unknown[]) => {
|
|
92
|
+
calls.push({ level, args })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const config: CouchConfigInput = {
|
|
96
|
+
...baseConfig(),
|
|
97
|
+
logger: fnLogger
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const logger = createLogger(config)
|
|
101
|
+
logger.info('hello', 42)
|
|
102
|
+
logger.error('problem')
|
|
103
|
+
|
|
104
|
+
assert.deepStrictEqual(calls, [
|
|
105
|
+
{ level: 'info', args: ['hello', 42] },
|
|
106
|
+
{ level: 'error', args: ['problem'] }
|
|
107
|
+
])
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('fills missing methods on object logger', () => {
|
|
111
|
+
let warnCount = 0
|
|
112
|
+
const config: CouchConfigInput = {
|
|
113
|
+
...baseConfig(),
|
|
114
|
+
logger: {
|
|
115
|
+
warn: () => {
|
|
116
|
+
warnCount++
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const logger = createLogger(config)
|
|
122
|
+
logger.warn('watch out')
|
|
123
|
+
|
|
124
|
+
assert.strictEqual(warnCount, 1)
|
|
125
|
+
assert.doesNotThrow(() => logger.error('ignored'))
|
|
126
|
+
assert.doesNotThrow(() => logger.info('ignored'))
|
|
127
|
+
assert.doesNotThrow(() => logger.debug('ignored'))
|
|
128
|
+
})
|
|
129
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MergeNeedleOpts } from '../../schema/util.mts'
|
|
2
|
+
|
|
3
|
+
export const mergeNeedleOpts = MergeNeedleOpts.implement((config, opts) => {
|
|
4
|
+
if (config.needleOpts) {
|
|
5
|
+
return {
|
|
6
|
+
...opts,
|
|
7
|
+
...config.needleOpts,
|
|
8
|
+
headers: {
|
|
9
|
+
...opts.headers,
|
|
10
|
+
...(config.needleOpts.headers ?? {})
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return opts
|
|
16
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { ViewRow } from '../../schema/couch/couch.output.schema.ts'
|
|
3
|
+
import type { StandardSchemaV1 } from '../../types/standard-schema.ts'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
|
|
6
|
+
export type OnInvalidDocAction = 'throw' | 'skip'
|
|
7
|
+
|
|
8
|
+
export async function parseRows<
|
|
9
|
+
DocSchema extends StandardSchemaV1 = StandardSchemaV1<any>,
|
|
10
|
+
KeySchema extends StandardSchemaV1 = StandardSchemaV1<any>,
|
|
11
|
+
ValueSchema extends StandardSchemaV1 = StandardSchemaV1<any>
|
|
12
|
+
>(
|
|
13
|
+
rows: unknown,
|
|
14
|
+
options: {
|
|
15
|
+
onInvalidDoc?: OnInvalidDocAction
|
|
16
|
+
docSchema?: DocSchema
|
|
17
|
+
keySchema?: KeySchema
|
|
18
|
+
valueSchema?: ValueSchema
|
|
19
|
+
}
|
|
20
|
+
) {
|
|
21
|
+
if (!Array.isArray(rows)) {
|
|
22
|
+
throw new Error('invalid rows format')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ParsedRow = {
|
|
26
|
+
id?: string
|
|
27
|
+
key?: StandardSchemaV1.InferOutput<KeySchema>
|
|
28
|
+
value?: StandardSchemaV1.InferOutput<ValueSchema>
|
|
29
|
+
doc?: StandardSchemaV1.InferOutput<DocSchema>
|
|
30
|
+
error?: string
|
|
31
|
+
}
|
|
32
|
+
type RowResult = ParsedRow | 'skip'
|
|
33
|
+
const isFinalRow = (row: RowResult): row is ParsedRow => row !== 'skip'
|
|
34
|
+
|
|
35
|
+
const parsedRows: Array<RowResult> = await Promise.all(
|
|
36
|
+
rows.map(async (row: any) => {
|
|
37
|
+
try {
|
|
38
|
+
/**
|
|
39
|
+
* If no doc is present, parse without doc validation.
|
|
40
|
+
* This allows handling of not-found documents or rows without docs.
|
|
41
|
+
*/
|
|
42
|
+
if (row.doc == null) {
|
|
43
|
+
const parsedRow = z.looseObject(ViewRow.shape).parse(row)
|
|
44
|
+
if (options.keySchema) {
|
|
45
|
+
const parsedKey = await options.keySchema['~standard'].validate(row.key)
|
|
46
|
+
if (parsedKey.issues) {
|
|
47
|
+
throw parsedKey.issues
|
|
48
|
+
}
|
|
49
|
+
parsedRow.key = parsedKey.value
|
|
50
|
+
}
|
|
51
|
+
if (options.valueSchema) {
|
|
52
|
+
const parsedValue = await options.valueSchema['~standard'].validate(row.value)
|
|
53
|
+
if (parsedValue.issues) {
|
|
54
|
+
throw parsedValue.issues
|
|
55
|
+
}
|
|
56
|
+
parsedRow.value = parsedValue.value
|
|
57
|
+
}
|
|
58
|
+
return parsedRow
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let parsedDoc = row.doc
|
|
62
|
+
let parsedKey = row.key
|
|
63
|
+
let parsedValue = row.value
|
|
64
|
+
|
|
65
|
+
if (options.docSchema) {
|
|
66
|
+
const parsedDocRes = await options.docSchema['~standard'].validate(row.doc)
|
|
67
|
+
if (parsedDocRes.issues) {
|
|
68
|
+
if (options.onInvalidDoc === 'skip') {
|
|
69
|
+
// skip invalid doc
|
|
70
|
+
return 'skip'
|
|
71
|
+
} else {
|
|
72
|
+
// throw by default
|
|
73
|
+
throw parsedDocRes.issues
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
parsedDoc = parsedDocRes.value
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (options.keySchema) {
|
|
81
|
+
const parsedKeyRes = await options.keySchema['~standard'].validate(row.key)
|
|
82
|
+
if (parsedKeyRes.issues) {
|
|
83
|
+
throw parsedKeyRes.issues
|
|
84
|
+
} else {
|
|
85
|
+
parsedKey = parsedKeyRes.value
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (options.valueSchema) {
|
|
90
|
+
const parsedValueRes = await options.valueSchema['~standard'].validate(row.value)
|
|
91
|
+
if (parsedValueRes.issues) {
|
|
92
|
+
throw parsedValueRes.issues
|
|
93
|
+
} else {
|
|
94
|
+
parsedValue = parsedValueRes.value
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
...row,
|
|
100
|
+
doc: parsedDoc,
|
|
101
|
+
key: parsedKey,
|
|
102
|
+
value: parsedValue
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {
|
|
105
|
+
if (options.onInvalidDoc === 'skip') {
|
|
106
|
+
// skip invalid doc
|
|
107
|
+
return 'skip'
|
|
108
|
+
} else {
|
|
109
|
+
// throw by default
|
|
110
|
+
throw e
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return parsedRows.filter(isFinalRow)
|
|
117
|
+
}
|