hide-a-bed 6.0.0 → 7.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/README.md +89 -28
- package/dist/cjs/index.cjs +888 -443
- package/dist/esm/index.mjs +883 -443
- package/eslint.config.js +6 -1
- package/impl/bindConfig.mts +30 -3
- package/impl/bulkGet.mts +50 -27
- package/impl/bulkRemove.mts +4 -2
- package/impl/bulkSave.mts +50 -28
- package/impl/get.mts +49 -40
- package/impl/getDBInfo.mts +26 -24
- package/impl/patch.mts +46 -42
- package/impl/put.mts +39 -21
- package/impl/query.mts +101 -81
- package/impl/remove.mts +33 -33
- package/impl/stream.mts +163 -102
- package/impl/sugar/watch.mts +165 -97
- package/impl/utils/errors.mts +261 -35
- package/impl/utils/fetch.mts +201 -0
- package/impl/utils/parseRows.mts +47 -6
- package/impl/utils/request.mts +22 -0
- package/impl/utils/response.mts +50 -0
- package/impl/utils/transactionErrors.mts +14 -8
- package/impl/utils/url.mts +21 -0
- package/index.mts +19 -2
- package/migration_guides/v7.md +353 -0
- package/package.json +4 -4
- package/schema/config.mts +17 -34
- package/schema/request.mts +36 -0
- package/schema/sugar/watch.mts +1 -1
- package/tsconfig.json +9 -1
- package/types/output/impl/bindConfig.d.mts +31 -149
- package/types/output/impl/bindConfig.d.mts.map +1 -1
- package/types/output/impl/bindConfig.test.d.mts +2 -0
- package/types/output/impl/bindConfig.test.d.mts.map +1 -0
- package/types/output/impl/bulkGet.d.mts +5 -5
- package/types/output/impl/bulkGet.d.mts.map +1 -1
- package/types/output/impl/bulkRemove.d.mts +4 -2
- package/types/output/impl/bulkRemove.d.mts.map +1 -1
- package/types/output/impl/bulkSave.d.mts +2 -2
- package/types/output/impl/bulkSave.d.mts.map +1 -1
- package/types/output/impl/get.d.mts +2 -2
- package/types/output/impl/get.d.mts.map +1 -1
- package/types/output/impl/getDBInfo.d.mts +1 -1
- package/types/output/impl/getDBInfo.d.mts.map +1 -1
- package/types/output/impl/patch.d.mts +8 -3
- package/types/output/impl/patch.d.mts.map +1 -1
- package/types/output/impl/put.d.mts.map +1 -1
- package/types/output/impl/query.d.mts +8 -23
- package/types/output/impl/query.d.mts.map +1 -1
- package/types/output/impl/remove.d.mts.map +1 -1
- package/types/output/impl/request-controls.test.d.mts +2 -0
- package/types/output/impl/request-controls.test.d.mts.map +1 -0
- package/types/output/impl/stream.d.mts +1 -1
- package/types/output/impl/stream.d.mts.map +1 -1
- package/types/output/impl/sugar/watch.d.mts +7 -5
- package/types/output/impl/sugar/watch.d.mts.map +1 -1
- package/types/output/impl/utils/errors.d.mts +84 -26
- package/types/output/impl/utils/errors.d.mts.map +1 -1
- package/types/output/impl/utils/fetch.d.mts +27 -0
- package/types/output/impl/utils/fetch.d.mts.map +1 -0
- package/types/output/impl/utils/fetch.test.d.mts +2 -0
- package/types/output/impl/utils/fetch.test.d.mts.map +1 -0
- package/types/output/impl/utils/parseRows.d.mts +3 -0
- package/types/output/impl/utils/parseRows.d.mts.map +1 -1
- package/types/output/impl/utils/request.d.mts +6 -0
- package/types/output/impl/utils/request.d.mts.map +1 -0
- package/types/output/impl/utils/response.d.mts +7 -0
- package/types/output/impl/utils/response.d.mts.map +1 -0
- package/types/output/impl/utils/response.test.d.mts +2 -0
- package/types/output/impl/utils/response.test.d.mts.map +1 -0
- package/types/output/impl/utils/trackedEmitter.test.d.mts +2 -0
- package/types/output/impl/utils/trackedEmitter.test.d.mts.map +1 -0
- package/types/output/impl/utils/transactionErrors.d.mts +5 -4
- package/types/output/impl/utils/transactionErrors.d.mts.map +1 -1
- package/types/output/impl/utils/transactionErrors.test.d.mts +2 -0
- package/types/output/impl/utils/transactionErrors.test.d.mts.map +1 -0
- package/types/output/impl/utils/url.d.mts +4 -0
- package/types/output/impl/utils/url.d.mts.map +1 -0
- package/types/output/impl/utils/url.test.d.mts +2 -0
- package/types/output/impl/utils/url.test.d.mts.map +1 -0
- package/types/output/index.d.mts +5 -2
- package/types/output/index.d.mts.map +1 -1
- package/types/output/schema/config.d.mts +13 -69
- package/types/output/schema/config.d.mts.map +1 -1
- package/types/output/schema/config.test.d.mts +2 -0
- package/types/output/schema/config.test.d.mts.map +1 -0
- package/types/output/schema/request.d.mts +10 -0
- package/types/output/schema/request.d.mts.map +1 -0
- package/types/output/schema/sugar/lock.test.d.mts +2 -0
- package/types/output/schema/sugar/lock.test.d.mts.map +1 -0
- package/types/output/schema/sugar/watch.d.mts +1 -1
- package/types/output/schema/sugar/watch.d.mts.map +1 -1
- package/types/output/schema/sugar/watch.test.d.mts +2 -0
- package/types/output/schema/sugar/watch.test.d.mts.map +1 -0
- package/impl/utils/mergeNeedleOpts.mts +0 -16
- package/schema/util.mts +0 -8
- package/types/output/impl/utils/mergeNeedleOpts.d.mts +0 -53
- package/types/output/impl/utils/mergeNeedleOpts.d.mts.map +0 -1
- package/types/output/schema/util.d.mts +0 -85
- package/types/output/schema/util.d.mts.map +0 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const isRecord = (value: unknown): value is Record<string, unknown> => {
|
|
2
|
+
return value !== null && typeof value === 'object'
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export type CouchSuccessProfile =
|
|
6
|
+
| 'bulkGet'
|
|
7
|
+
| 'bulkSave'
|
|
8
|
+
| 'changesFeed'
|
|
9
|
+
| 'database'
|
|
10
|
+
| 'documentDelete'
|
|
11
|
+
| 'documentRead'
|
|
12
|
+
| 'documentWrite'
|
|
13
|
+
| 'viewQuery'
|
|
14
|
+
| 'viewStream'
|
|
15
|
+
|
|
16
|
+
const SUCCESS_STATUS_CODES: Record<CouchSuccessProfile, readonly number[]> = {
|
|
17
|
+
bulkGet: [200],
|
|
18
|
+
bulkSave: [201, 202],
|
|
19
|
+
changesFeed: [200],
|
|
20
|
+
database: [200],
|
|
21
|
+
documentDelete: [200, 202],
|
|
22
|
+
documentRead: [200],
|
|
23
|
+
documentWrite: [200, 201, 202],
|
|
24
|
+
viewQuery: [200],
|
|
25
|
+
viewStream: [200]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const getReason = (value: unknown, fallback: string) => {
|
|
29
|
+
if (!isRecord(value) || typeof value.reason !== 'string') {
|
|
30
|
+
return fallback
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return value.reason
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const getCouchError = (value: unknown) => {
|
|
37
|
+
if (!isRecord(value) || typeof value.error !== 'string') {
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return value.error
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const getSuccessStatusCodes = (profile: CouchSuccessProfile) => {
|
|
45
|
+
return SUCCESS_STATUS_CODES[profile]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const isSuccessStatusCode = (profile: CouchSuccessProfile, statusCode: number) => {
|
|
49
|
+
return SUCCESS_STATUS_CODES[profile].includes(statusCode)
|
|
50
|
+
}
|
|
@@ -1,24 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
import { OperationError } from './errors.mts'
|
|
2
|
+
|
|
3
|
+
export class TransactionSetupError extends OperationError {
|
|
2
4
|
details: Record<string, unknown>
|
|
3
5
|
|
|
4
6
|
constructor(message: string, details: Record<string, unknown> = {}) {
|
|
5
|
-
super(message)
|
|
7
|
+
super(message, { category: 'transaction' })
|
|
6
8
|
this.name = 'TransactionSetupError'
|
|
7
9
|
this.details = details
|
|
8
10
|
}
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
export class TransactionVersionConflictError extends
|
|
13
|
+
export class TransactionVersionConflictError extends OperationError {
|
|
12
14
|
conflictingIds: string[]
|
|
13
15
|
|
|
14
16
|
constructor(conflictingIds: string[]) {
|
|
15
|
-
super(`Revision mismatch for documents: ${conflictingIds.join(', ')}
|
|
17
|
+
super(`Revision mismatch for documents: ${conflictingIds.join(', ')}`, {
|
|
18
|
+
category: 'transaction'
|
|
19
|
+
})
|
|
16
20
|
this.name = 'TransactionVersionConflictError'
|
|
17
21
|
this.conflictingIds = conflictingIds
|
|
18
22
|
}
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
export class TransactionBulkOperationError extends
|
|
25
|
+
export class TransactionBulkOperationError extends OperationError {
|
|
22
26
|
failedDocs: {
|
|
23
27
|
ok?: boolean | null
|
|
24
28
|
id?: string | null
|
|
@@ -36,13 +40,15 @@ export class TransactionBulkOperationError extends Error {
|
|
|
36
40
|
reason?: string | null
|
|
37
41
|
}>
|
|
38
42
|
) {
|
|
39
|
-
super(`Failed to save documents: ${failedDocs.map(d => d.id).join(', ')}
|
|
43
|
+
super(`Failed to save documents: ${failedDocs.map(d => d.id).join(', ')}`, {
|
|
44
|
+
category: 'transaction'
|
|
45
|
+
})
|
|
40
46
|
this.name = 'TransactionBulkOperationError'
|
|
41
47
|
this.failedDocs = failedDocs
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
export class TransactionRollbackError extends
|
|
51
|
+
export class TransactionRollbackError extends OperationError {
|
|
46
52
|
originalError: Error
|
|
47
53
|
rollbackResults: {
|
|
48
54
|
ok?: boolean | null
|
|
@@ -63,7 +69,7 @@ export class TransactionRollbackError extends Error {
|
|
|
63
69
|
reason?: string | null
|
|
64
70
|
}>
|
|
65
71
|
) {
|
|
66
|
-
super(message)
|
|
72
|
+
super(message, { category: 'transaction' })
|
|
67
73
|
this.name = 'TransactionRollbackError'
|
|
68
74
|
this.originalError = originalError
|
|
69
75
|
this.rollbackResults = rollbackResults
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const ensureDirectoryUrl = (value: string | URL) => {
|
|
2
|
+
const url = new URL(value)
|
|
3
|
+
|
|
4
|
+
if (!url.pathname.endsWith('/')) {
|
|
5
|
+
url.pathname = `${url.pathname}/`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return url
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const createCouchDbUrl = (value: string | URL) => {
|
|
12
|
+
return new URL(value)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const createCouchPathUrl = (path: string, base: string | URL) => {
|
|
16
|
+
return new URL(path, ensureDirectoryUrl(base))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const createCouchDocUrl = (docId: string, base: string | URL) => {
|
|
20
|
+
return new URL(encodeURIComponent(docId), ensureDirectoryUrl(base))
|
|
21
|
+
}
|
package/index.mts
CHANGED
|
@@ -46,6 +46,15 @@ export {
|
|
|
46
46
|
removeLock
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export {
|
|
50
|
+
ConflictError,
|
|
51
|
+
HideABedError,
|
|
52
|
+
NotFoundError,
|
|
53
|
+
OperationError,
|
|
54
|
+
RetryableError,
|
|
55
|
+
ValidationError
|
|
56
|
+
} from './impl/utils/errors.mts'
|
|
57
|
+
|
|
49
58
|
export type {
|
|
50
59
|
BulkGetBound,
|
|
51
60
|
BulkGetDictionaryBound,
|
|
@@ -70,10 +79,18 @@ export type {
|
|
|
70
79
|
ViewRowValidated
|
|
71
80
|
} from './schema/couch/couch.output.schema.ts'
|
|
72
81
|
export type { RetryOptions } from './impl/retry.mts'
|
|
73
|
-
export type {
|
|
82
|
+
export type {
|
|
83
|
+
ErrorCategory,
|
|
84
|
+
ErrorOperation,
|
|
85
|
+
HideABedErrorOptions,
|
|
86
|
+
NetworkError,
|
|
87
|
+
ValidationErrorOptions
|
|
88
|
+
} from './impl/utils/errors.mts'
|
|
74
89
|
export type { OnRow } from './impl/stream.mts'
|
|
75
|
-
export type { CouchConfig, CouchConfigInput } from './schema/config.mts'
|
|
90
|
+
export type { CouchAuth, CouchAuthInput, CouchConfig, CouchConfigInput } from './schema/config.mts'
|
|
91
|
+
export type { Dispatcher, RequestOptions, RequestOptionsInput } from './schema/request.mts'
|
|
76
92
|
export type { LockOptions, LockOptionsInput, LockDoc } from './schema/sugar/lock.mts'
|
|
93
|
+
export type { WatchHandle, WatchListener } from './impl/sugar/watch.mts'
|
|
77
94
|
export type {
|
|
78
95
|
WatchOptions as WatchOptionsSchema,
|
|
79
96
|
WatchOptionsInput
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# hide-a-bed v7 Migration Guide
|
|
2
|
+
|
|
3
|
+
## When to read this
|
|
4
|
+
|
|
5
|
+
- Teams upgrading from any `hide-a-bed` v6 release to the current v7 beta line.
|
|
6
|
+
- Consumers who passed transport-specific `needle` options into config.
|
|
7
|
+
- Consumers who used return payloads like `{ ok: false, error: 'conflict' }` for control flow.
|
|
8
|
+
- Teams that validate docs or rows and need to update error handling.
|
|
9
|
+
|
|
10
|
+
## Scope
|
|
11
|
+
|
|
12
|
+
This guide covers the behavior changes between the `6.0.0` release and the current v7 work in this branch. The biggest shift is the move from `needle` to native `fetch`, but the upgrade also changes config validation, request controls, and several failure paths.
|
|
13
|
+
|
|
14
|
+
## Upgrade checklist
|
|
15
|
+
|
|
16
|
+
1. Update `hide-a-bed` to v7 and run your tests on a Node runtime that matches the package manifest for `client/`.
|
|
17
|
+
2. Remove every use of `config.needleOpts`.
|
|
18
|
+
3. Move CouchDB credentials out of the Couch URL and into `config.auth`.
|
|
19
|
+
4. Move timeout, abort, and dispatcher settings into `config.request`.
|
|
20
|
+
5. Update any code that inspects `{ ok: false, error: ... }` from single-document helpers to catch typed errors instead.
|
|
21
|
+
6. Update any validation error handling that expected raw schema issues instead of a `ValidationError`.
|
|
22
|
+
7. Re-test streaming and watch flows with abort/cancellation, especially if you previously relied on request-library behavior.
|
|
23
|
+
|
|
24
|
+
## Breaking changes
|
|
25
|
+
|
|
26
|
+
### 1. `needleOpts` is gone
|
|
27
|
+
|
|
28
|
+
v6 accepted transport-specific options through `config.needleOpts`. v7 validates config strictly and rejects `needleOpts`.
|
|
29
|
+
|
|
30
|
+
Before:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
const config = {
|
|
34
|
+
couch: 'http://localhost:5984/mydb',
|
|
35
|
+
needleOpts: {
|
|
36
|
+
timeout: 5000,
|
|
37
|
+
username: process.env.COUCHDB_USER,
|
|
38
|
+
password: process.env.COUCHDB_PASSWORD
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
After:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const config = {
|
|
47
|
+
couch: 'http://localhost:5984/mydb',
|
|
48
|
+
auth: {
|
|
49
|
+
username: process.env.COUCHDB_USER!,
|
|
50
|
+
password: process.env.COUCHDB_PASSWORD!
|
|
51
|
+
},
|
|
52
|
+
request: {
|
|
53
|
+
timeout: 5000
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
What changed:
|
|
59
|
+
|
|
60
|
+
- `config.auth` is the supported way to send basic auth credentials.
|
|
61
|
+
- `config.request` is the supported way to pass native fetch controls.
|
|
62
|
+
- Only these request fields are supported: `signal`, `timeout`, and `dispatcher`.
|
|
63
|
+
- Unknown keys under `config.request` now fail validation.
|
|
64
|
+
|
|
65
|
+
### 2. Couch URLs with embedded credentials are rejected
|
|
66
|
+
|
|
67
|
+
v6 accepted a `couch` string like `http://alice:secret@localhost:5984/mydb`. v7 rejects config values with embedded credentials.
|
|
68
|
+
|
|
69
|
+
Before:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const config = {
|
|
73
|
+
couch: 'http://alice:secret@localhost:5984/mydb'
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
After:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const config = {
|
|
81
|
+
couch: 'http://localhost:5984/mydb',
|
|
82
|
+
auth: {
|
|
83
|
+
username: 'alice',
|
|
84
|
+
password: 'secret'
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 3. Request controls moved to config-level `request`
|
|
90
|
+
|
|
91
|
+
v7 uses native fetch internally, so request customization is intentionally much smaller than the old `needle` surface.
|
|
92
|
+
|
|
93
|
+
Supported:
|
|
94
|
+
|
|
95
|
+
- `config.request.signal`
|
|
96
|
+
- `config.request.timeout`
|
|
97
|
+
- `config.request.dispatcher`
|
|
98
|
+
|
|
99
|
+
Not supported:
|
|
100
|
+
|
|
101
|
+
- Per-call `request` overrides inside `get()` options, `query()` options, or `watchDocs()` options.
|
|
102
|
+
- Any other transport-specific fields from `needle`.
|
|
103
|
+
|
|
104
|
+
Use config overrides instead:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const db = bindConfig({
|
|
108
|
+
couch: 'http://localhost:5984/mydb',
|
|
109
|
+
request: { timeout: 5000 }
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
await db
|
|
113
|
+
.options({
|
|
114
|
+
request: {
|
|
115
|
+
timeout: 1000,
|
|
116
|
+
signal: abortController.signal
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
.get('doc-123')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 4. Single-document write/delete helpers now throw typed errors
|
|
123
|
+
|
|
124
|
+
Several v6 code paths returned parsed CouchDB payloads like `{ ok: false, error: 'conflict' }`. In v7 those same cases now throw.
|
|
125
|
+
|
|
126
|
+
#### `put()`
|
|
127
|
+
|
|
128
|
+
Before:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const result = await put(config, {
|
|
132
|
+
_id: 'doc-123',
|
|
133
|
+
_rev: 'stale-rev'
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
if (result.error === 'conflict') {
|
|
137
|
+
// handle stale rev
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
After:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { ConflictError } from 'hide-a-bed'
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await put(config, {
|
|
148
|
+
_id: 'doc-123',
|
|
149
|
+
_rev: 'stale-rev'
|
|
150
|
+
})
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error instanceof ConflictError) {
|
|
153
|
+
console.log(error.docId)
|
|
154
|
+
console.log(error.statusCode)
|
|
155
|
+
} else {
|
|
156
|
+
throw error
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### `remove()`
|
|
162
|
+
|
|
163
|
+
Before:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const result = await remove(config, 'missing-doc', '1-deadbeef')
|
|
167
|
+
|
|
168
|
+
if (result.error === 'not_found') {
|
|
169
|
+
// handle missing document
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
After:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { NotFoundError } from 'hide-a-bed'
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await remove(config, 'missing-doc', '1-deadbeef')
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (error instanceof NotFoundError) {
|
|
182
|
+
console.log(error.docId)
|
|
183
|
+
} else {
|
|
184
|
+
throw error
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### `patch()` and `patchDangerously()`
|
|
190
|
+
|
|
191
|
+
Before:
|
|
192
|
+
|
|
193
|
+
- `patch()` returned a conflict-shaped response when `_rev` did not match.
|
|
194
|
+
- `patchDangerously()` could return `{ ok: false, error: 'not_found' }` or a generic failure payload after retry exhaustion.
|
|
195
|
+
|
|
196
|
+
After:
|
|
197
|
+
|
|
198
|
+
- `patch()` throws `ConflictError` for stale revisions.
|
|
199
|
+
- `patch()` throws `NotFoundError` when the document is missing.
|
|
200
|
+
- `patchDangerously()` throws `NotFoundError` when the document is missing.
|
|
201
|
+
- `patchDangerously()` throws `OperationError` when retries are exhausted.
|
|
202
|
+
|
|
203
|
+
This means upgrade code should stop pattern-matching on `result.ok` for unhappy paths and switch to `try`/`catch`.
|
|
204
|
+
|
|
205
|
+
### 5. Validation failures now throw `ValidationError`
|
|
206
|
+
|
|
207
|
+
In v6, validation failures could bubble out as raw schema issues. In v7 they are wrapped in a first-class `ValidationError`.
|
|
208
|
+
|
|
209
|
+
Affected helpers include:
|
|
210
|
+
|
|
211
|
+
- `get()`
|
|
212
|
+
- `getAtRev()`
|
|
213
|
+
- `bulkGet()`
|
|
214
|
+
- `bulkGetDictionary()`
|
|
215
|
+
- `query()`
|
|
216
|
+
|
|
217
|
+
Before:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
try {
|
|
221
|
+
await get(config, 'doc-123', {
|
|
222
|
+
validate: { docSchema }
|
|
223
|
+
})
|
|
224
|
+
} catch (issues) {
|
|
225
|
+
// issues might be the raw schema issue array
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
After:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { ValidationError } from 'hide-a-bed'
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await get(config, 'doc-123', {
|
|
236
|
+
validate: { docSchema }
|
|
237
|
+
})
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (error instanceof ValidationError) {
|
|
240
|
+
console.log(error.docId)
|
|
241
|
+
console.log(error.operation)
|
|
242
|
+
console.log(error.issues)
|
|
243
|
+
} else {
|
|
244
|
+
throw error
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 6. Request-level failures now use structured operational errors
|
|
250
|
+
|
|
251
|
+
v7 adds exported error classes for request and CouchDB failures:
|
|
252
|
+
|
|
253
|
+
- `HideABedError`
|
|
254
|
+
- `ConflictError`
|
|
255
|
+
- `NotFoundError`
|
|
256
|
+
- `OperationError`
|
|
257
|
+
- `RetryableError`
|
|
258
|
+
- `ValidationError`
|
|
259
|
+
|
|
260
|
+
These errors carry machine-readable fields such as:
|
|
261
|
+
|
|
262
|
+
- `statusCode`
|
|
263
|
+
- `docId`
|
|
264
|
+
- `operation`
|
|
265
|
+
- `couchError`
|
|
266
|
+
- `retryable`
|
|
267
|
+
|
|
268
|
+
This especially affects:
|
|
269
|
+
|
|
270
|
+
- `query()`
|
|
271
|
+
- `bulkGet()`
|
|
272
|
+
- `bulkSave()`
|
|
273
|
+
- `getDBInfo()`
|
|
274
|
+
- `queryStream()`
|
|
275
|
+
- `watchDocs()`
|
|
276
|
+
|
|
277
|
+
Instead of parsing `error.message`, prefer branching on the class and metadata.
|
|
278
|
+
|
|
279
|
+
### 7. `bulkSave([])` is now a hard error
|
|
280
|
+
|
|
281
|
+
v6 threw a generic `Error('no docs provided')`. v7 throws an `OperationError` with message `Bulk save requires at least one document`.
|
|
282
|
+
|
|
283
|
+
If you intentionally call `bulkSave()` with a maybe-empty array, guard it first:
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
if (docs.length > 0) {
|
|
287
|
+
await bulkSave(config, docs)
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 8. Streaming and watch flows now respect `config.request`
|
|
292
|
+
|
|
293
|
+
`queryStream()` and `watchDocs()` now run through the same native fetch layer as the rest of the client.
|
|
294
|
+
|
|
295
|
+
Practical migration notes:
|
|
296
|
+
|
|
297
|
+
- Aborting `config.request.signal` aborts the active stream request.
|
|
298
|
+
- `watchDocs().stop()` aborts the active request instead of only destroying the old request object.
|
|
299
|
+
- `watchDocs()` emits a single `end` when stopped or externally aborted.
|
|
300
|
+
- Invalid `watchDocs()` input now throws `OperationError` instead of a generic `Error`.
|
|
301
|
+
|
|
302
|
+
Example:
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
const controller = new AbortController()
|
|
306
|
+
|
|
307
|
+
const watcher = watchDocs(
|
|
308
|
+
{
|
|
309
|
+
couch: 'http://localhost:5984/mydb',
|
|
310
|
+
request: { signal: controller.signal }
|
|
311
|
+
},
|
|
312
|
+
['doc-1', 'doc-2'],
|
|
313
|
+
change => {
|
|
314
|
+
console.log(change.id)
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
controller.abort()
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## What stays the same
|
|
322
|
+
|
|
323
|
+
- The top-level CRUD, bulk, query, and stream helper names are unchanged.
|
|
324
|
+
- `get()` still returns `null` on 404 by default, and still throws when `throwOnGetNotFound: true`.
|
|
325
|
+
- Bulk APIs still report item-level failures inside their result payloads where that was already the contract.
|
|
326
|
+
- `bindConfig()` and `withRetry()` remain the main composition helpers.
|
|
327
|
+
- `watchDocs()` and `queryStream()` remain in the main package.
|
|
328
|
+
|
|
329
|
+
## New exports worth using
|
|
330
|
+
|
|
331
|
+
v7 newly exports runtime classes and types that make upgrade code cleaner:
|
|
332
|
+
|
|
333
|
+
- Error classes: `ConflictError`, `HideABedError`, `NotFoundError`, `OperationError`, `RetryableError`, `ValidationError`
|
|
334
|
+
- Config types: `CouchAuth`, `CouchAuthInput`, `RequestOptions`, `RequestOptionsInput`, `Dispatcher`
|
|
335
|
+
- Watch types: `WatchHandle`, `WatchListener`
|
|
336
|
+
|
|
337
|
+
## Suggested migration order
|
|
338
|
+
|
|
339
|
+
1. Replace config construction first.
|
|
340
|
+
2. Update single-document error handling next.
|
|
341
|
+
3. Update validation error handling.
|
|
342
|
+
4. Re-test watchers, streams, and timeout/abort behavior.
|
|
343
|
+
5. Re-run integration tests against CouchDB or the stub package.
|
|
344
|
+
|
|
345
|
+
## Verification checklist
|
|
346
|
+
|
|
347
|
+
- No code still references `needleOpts`.
|
|
348
|
+
- No Couch URLs still embed credentials.
|
|
349
|
+
- All auth now flows through `config.auth`.
|
|
350
|
+
- Timeouts and abort signals now flow through `config.request`.
|
|
351
|
+
- `put()`, `remove()`, `patch()`, and `patchDangerously()` callers use `try`/`catch` for unhappy paths.
|
|
352
|
+
- Validation callers catch `ValidationError` instead of assuming raw issues.
|
|
353
|
+
- Streaming and watcher tests still pass with cancellation enabled.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hide-a-bed",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0-beta.0",
|
|
4
4
|
"description": "An abstraction over couchdb calls that includes easy mock/stubs with pouchdb",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.cjs",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
33
|
"typecheck:watch": "tsc --noEmit --watch",
|
|
34
34
|
"prepublishOnly": "npm run build",
|
|
35
|
+
"validate": "npm run format && npm run lint && npm run typecheck",
|
|
35
36
|
"full": "npm run lint:fix && npm run build && npm run clean"
|
|
36
37
|
},
|
|
37
38
|
"repository": {
|
|
@@ -49,15 +50,14 @@
|
|
|
49
50
|
},
|
|
50
51
|
"homepage": "https://github.com/ryanramage/hide-a-bed#readme",
|
|
51
52
|
"dependencies": {
|
|
52
|
-
"needle": "3.3.1",
|
|
53
53
|
"stream-chain": "3.4.0",
|
|
54
54
|
"stream-json": "1.9.1",
|
|
55
|
+
"undici-types": "7.24.3",
|
|
55
56
|
"zod": "4.2.1"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@eslint/js": "9.39.2",
|
|
59
|
-
"@types/
|
|
60
|
-
"@types/node": "22.19.3",
|
|
60
|
+
"@types/node": "24.12.0",
|
|
61
61
|
"@types/stream-json": "1.7.8",
|
|
62
62
|
"eslint": "9.39.2",
|
|
63
63
|
"globals": "16.5.0",
|
package/schema/config.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import type { StandardSchemaV1 } from '../types/standard-schema.ts'
|
|
3
|
+
import { RequestOptions } from './request.mts'
|
|
3
4
|
|
|
4
5
|
const anyArgs = z.array(z.any())
|
|
5
6
|
|
|
@@ -12,46 +13,26 @@ const LoggerSchema = z
|
|
|
12
13
|
})
|
|
13
14
|
.or(z.function({ input: anyArgs, output: z.void() }))
|
|
14
15
|
|
|
15
|
-
export const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
parse_response: z.boolean().optional()
|
|
16
|
+
export const CouchAuth = z.strictObject({
|
|
17
|
+
username: z.string().describe('basic auth username for CouchDB requests'),
|
|
18
|
+
password: z.string().describe('basic auth password for CouchDB requests')
|
|
19
19
|
})
|
|
20
|
-
export type NeedleBaseOptionsSchema = z.infer<typeof NeedleBaseOptions>
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
timeout: z.number().optional(),
|
|
30
|
-
read_timeout: z.number().optional(),
|
|
31
|
-
parse_response: z.boolean().optional(),
|
|
32
|
-
decode: z.boolean().optional(),
|
|
33
|
-
parse_cookies: z.boolean().optional(),
|
|
34
|
-
cookies: z.record(z.string(), z.string()).optional(),
|
|
35
|
-
headers: z.record(z.string(), z.string()).optional(),
|
|
36
|
-
auth: z.enum(['auto', 'digest', 'basic']).optional(),
|
|
37
|
-
username: z.string().optional(),
|
|
38
|
-
password: z.string().optional(),
|
|
39
|
-
proxy: z.string().optional(),
|
|
40
|
-
agent: z.any().optional(),
|
|
41
|
-
rejectUnauthorized: z.boolean().optional(),
|
|
42
|
-
output: z.string().optional(),
|
|
43
|
-
parse: z.boolean().optional(),
|
|
44
|
-
multipart: z.boolean().optional(),
|
|
45
|
-
open_timeout: z.number().optional(),
|
|
46
|
-
response_timeout: z.number().optional(),
|
|
47
|
-
keepAlive: z.boolean().optional()
|
|
21
|
+
const CouchUrl = z.custom<string | URL>(value => {
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(value as string | URL)
|
|
24
|
+
return url.username === '' && url.password === ''
|
|
25
|
+
} catch {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
48
28
|
})
|
|
49
29
|
|
|
50
30
|
export const CouchConfig = z
|
|
51
31
|
.strictObject({
|
|
32
|
+
auth: CouchAuth.optional().describe('basic auth credentials for CouchDB requests'),
|
|
52
33
|
backoffFactor: z.number().optional().default(2).describe('multiplier for exponential backoff'),
|
|
53
34
|
bindWithRetry: z.boolean().optional().default(true).describe('should we bind with retry'),
|
|
54
|
-
couch:
|
|
35
|
+
couch: CouchUrl.describe('URL of the couch db without embedded credentials'),
|
|
55
36
|
initialDelay: z
|
|
56
37
|
.number()
|
|
57
38
|
.optional()
|
|
@@ -61,12 +42,12 @@ export const CouchConfig = z
|
|
|
61
42
|
'logging interface supporting winston-like or simple function interface'
|
|
62
43
|
),
|
|
63
44
|
maxRetries: z.number().optional().default(3).describe('maximum number of retry attempts'),
|
|
64
|
-
|
|
45
|
+
request: RequestOptions.optional().describe('default request controls for CouchDB requests'),
|
|
65
46
|
throwOnGetNotFound: z
|
|
66
47
|
.boolean()
|
|
67
48
|
.optional()
|
|
68
49
|
.default(false)
|
|
69
|
-
.describe('if
|
|
50
|
+
.describe('if true, get() throws NotFoundError on 404; otherwise it returns null'),
|
|
70
51
|
useConsoleLogger: z
|
|
71
52
|
.boolean()
|
|
72
53
|
.optional()
|
|
@@ -77,5 +58,7 @@ export const CouchConfig = z
|
|
|
77
58
|
})
|
|
78
59
|
.describe('The std config object')
|
|
79
60
|
|
|
61
|
+
export type CouchAuth = StandardSchemaV1.InferOutput<typeof CouchAuth>
|
|
62
|
+
export type CouchAuthInput = StandardSchemaV1.InferInput<typeof CouchAuth>
|
|
80
63
|
export type CouchConfig = StandardSchemaV1.InferOutput<typeof CouchConfig>
|
|
81
64
|
export type CouchConfigInput = StandardSchemaV1.InferInput<typeof CouchConfig>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
const isAbortSignal = (value: unknown): value is AbortSignal => {
|
|
4
|
+
return value instanceof AbortSignal
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type Dispatcher = RequestInit['dispatcher']
|
|
8
|
+
|
|
9
|
+
export type RequestOptions = {
|
|
10
|
+
dispatcher?: Dispatcher
|
|
11
|
+
signal?: AbortSignal
|
|
12
|
+
timeout?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type RequestOptionsInput = RequestOptions
|
|
16
|
+
|
|
17
|
+
const isDispatcher = (value: unknown): value is Dispatcher => {
|
|
18
|
+
if (typeof value !== 'object' || value === null) return false
|
|
19
|
+
return typeof (value as { dispatch?: unknown }).dispatch === 'function'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const RequestOptions: z.ZodType<RequestOptions, RequestOptionsInput> = z.strictObject({
|
|
23
|
+
dispatcher: z
|
|
24
|
+
.custom<Dispatcher>(isDispatcher, {
|
|
25
|
+
message: 'dispatcher must expose a dispatch method'
|
|
26
|
+
})
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('dispatcher to use for the request'),
|
|
29
|
+
signal: z
|
|
30
|
+
.custom<AbortSignal>(isAbortSignal, {
|
|
31
|
+
message: 'signal must be an AbortSignal'
|
|
32
|
+
})
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('abort signal for the request'),
|
|
35
|
+
timeout: z.number().nonnegative().optional().describe('request timeout in milliseconds')
|
|
36
|
+
})
|
package/schema/sugar/watch.mts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod'
|
|
|
2
2
|
import type { StandardSchemaV1 } from '../../types/standard-schema.ts'
|
|
3
3
|
|
|
4
4
|
export const WatchOptions = z
|
|
5
|
-
.
|
|
5
|
+
.strictObject({
|
|
6
6
|
include_docs: z.boolean().default(false),
|
|
7
7
|
maxRetries: z.number().describe('maximum number of retries before giving up'),
|
|
8
8
|
initialDelay: z.number().describe('initial delay between retries in milliseconds'),
|