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
package/impl/utils/errors.mts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { getCouchError } from './response.mts'
|
|
2
|
+
import type { StandardSchemaV1 } from '../../types/standard-schema.ts'
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
|
-
* Represents a network-level error emitted by Node.js or
|
|
5
|
+
* Represents a network-level error emitted by Node.js or HTTP client libraries.
|
|
3
6
|
*
|
|
4
7
|
* @public
|
|
5
8
|
*/
|
|
@@ -15,6 +18,32 @@ export interface NetworkError {
|
|
|
15
18
|
message?: string
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
type ErrorWithCause = {
|
|
22
|
+
cause?: unknown
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ErrorCategory =
|
|
26
|
+
| 'conflict'
|
|
27
|
+
| 'network'
|
|
28
|
+
| 'not_found'
|
|
29
|
+
| 'operation'
|
|
30
|
+
| 'retryable'
|
|
31
|
+
| 'validation'
|
|
32
|
+
| 'transaction'
|
|
33
|
+
|
|
34
|
+
export type ErrorOperation =
|
|
35
|
+
| 'get'
|
|
36
|
+
| 'getAtRev'
|
|
37
|
+
| 'getDBInfo'
|
|
38
|
+
| 'patch'
|
|
39
|
+
| 'patchDangerously'
|
|
40
|
+
| 'put'
|
|
41
|
+
| 'query'
|
|
42
|
+
| 'queryStream'
|
|
43
|
+
| 'remove'
|
|
44
|
+
| 'request'
|
|
45
|
+
| 'watchDocs'
|
|
46
|
+
|
|
18
47
|
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504])
|
|
19
48
|
|
|
20
49
|
const NETWORK_ERROR_STATUS_MAP = {
|
|
@@ -36,6 +65,69 @@ const isNetworkError = (value: unknown): value is NetworkError & { code: Network
|
|
|
36
65
|
return typeof candidate.code === 'string' && candidate.code in NETWORK_ERROR_STATUS_MAP
|
|
37
66
|
}
|
|
38
67
|
|
|
68
|
+
const getNestedNetworkError = (
|
|
69
|
+
value: unknown
|
|
70
|
+
): (NetworkError & { code: NetworkErrorCode }) | null => {
|
|
71
|
+
if (isNetworkError(value)) {
|
|
72
|
+
return value
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof value !== 'object' || value === null) {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const candidate = value as ErrorWithCause
|
|
80
|
+
return isNetworkError(candidate.cause) ? candidate.cause : null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Shared structured fields available on hide-a-bed operational errors.
|
|
85
|
+
*
|
|
86
|
+
* @public
|
|
87
|
+
*/
|
|
88
|
+
export type HideABedErrorOptions = {
|
|
89
|
+
category: ErrorCategory
|
|
90
|
+
cause?: unknown
|
|
91
|
+
couchError?: string
|
|
92
|
+
docId?: string
|
|
93
|
+
operation?: ErrorOperation
|
|
94
|
+
retryable: boolean
|
|
95
|
+
statusCode?: number
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Shared base class for operational errors thrown by hide-a-bed.
|
|
100
|
+
*
|
|
101
|
+
* @public
|
|
102
|
+
*/
|
|
103
|
+
export class HideABedError extends Error {
|
|
104
|
+
readonly category: ErrorCategory
|
|
105
|
+
readonly couchError?: string
|
|
106
|
+
readonly docId?: string
|
|
107
|
+
readonly operation?: ErrorOperation
|
|
108
|
+
readonly retryable: boolean
|
|
109
|
+
readonly statusCode?: number
|
|
110
|
+
|
|
111
|
+
constructor(message: string, options: HideABedErrorOptions) {
|
|
112
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause })
|
|
113
|
+
this.name = 'HideABedError'
|
|
114
|
+
this.category = options.category
|
|
115
|
+
this.couchError = options.couchError
|
|
116
|
+
this.docId = options.docId
|
|
117
|
+
this.operation = options.operation
|
|
118
|
+
this.retryable = options.retryable
|
|
119
|
+
this.statusCode = options.statusCode
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type ValidationErrorOptions = Omit<
|
|
124
|
+
Partial<HideABedErrorOptions>,
|
|
125
|
+
'category' | 'retryable'
|
|
126
|
+
> & {
|
|
127
|
+
issues: ReadonlyArray<StandardSchemaV1.Issue>
|
|
128
|
+
message?: string
|
|
129
|
+
}
|
|
130
|
+
|
|
39
131
|
/**
|
|
40
132
|
* Error thrown when a requested CouchDB document cannot be found.
|
|
41
133
|
*
|
|
@@ -45,22 +137,96 @@ const isNetworkError = (value: unknown): value is NetworkError & { code: Network
|
|
|
45
137
|
*
|
|
46
138
|
* @public
|
|
47
139
|
*/
|
|
48
|
-
export class NotFoundError extends
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
140
|
+
export class NotFoundError extends HideABedError {
|
|
141
|
+
constructor(
|
|
142
|
+
docId: string,
|
|
143
|
+
options: Omit<Partial<HideABedErrorOptions>, 'category' | 'docId' | 'retryable'> & {
|
|
144
|
+
message?: string
|
|
145
|
+
} = {}
|
|
146
|
+
) {
|
|
147
|
+
super(options.message ?? 'Document not found', {
|
|
148
|
+
category: 'not_found',
|
|
149
|
+
couchError: options.couchError ?? 'not_found',
|
|
150
|
+
cause: options.cause,
|
|
151
|
+
docId,
|
|
152
|
+
operation: options.operation,
|
|
153
|
+
retryable: false,
|
|
154
|
+
statusCode: options.statusCode ?? 404
|
|
155
|
+
})
|
|
62
156
|
this.name = 'NotFoundError'
|
|
63
|
-
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Error thrown when a single-document mutation conflicts with the current revision.
|
|
162
|
+
*
|
|
163
|
+
* @public
|
|
164
|
+
*/
|
|
165
|
+
export class ConflictError extends HideABedError {
|
|
166
|
+
constructor(
|
|
167
|
+
docId: string,
|
|
168
|
+
options: Omit<Partial<HideABedErrorOptions>, 'category' | 'docId' | 'retryable'> & {
|
|
169
|
+
message?: string
|
|
170
|
+
} = {}
|
|
171
|
+
) {
|
|
172
|
+
super(options.message ?? 'Document update conflict', {
|
|
173
|
+
category: 'conflict',
|
|
174
|
+
couchError: options.couchError ?? 'conflict',
|
|
175
|
+
cause: options.cause,
|
|
176
|
+
docId,
|
|
177
|
+
operation: options.operation,
|
|
178
|
+
retryable: false,
|
|
179
|
+
statusCode: options.statusCode ?? 409
|
|
180
|
+
})
|
|
181
|
+
this.name = 'ConflictError'
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Error thrown when an operation fails in a non-retryable way.
|
|
187
|
+
*
|
|
188
|
+
* @public
|
|
189
|
+
*/
|
|
190
|
+
export class OperationError extends HideABedError {
|
|
191
|
+
constructor(
|
|
192
|
+
message: string,
|
|
193
|
+
options: Omit<Partial<HideABedErrorOptions>, 'category' | 'retryable'> & {
|
|
194
|
+
category?: Extract<ErrorCategory, 'operation' | 'transaction'>
|
|
195
|
+
} = {}
|
|
196
|
+
) {
|
|
197
|
+
super(message, {
|
|
198
|
+
category: options.category ?? 'operation',
|
|
199
|
+
cause: options.cause,
|
|
200
|
+
couchError: options.couchError,
|
|
201
|
+
docId: options.docId,
|
|
202
|
+
operation: options.operation,
|
|
203
|
+
retryable: false,
|
|
204
|
+
statusCode: options.statusCode
|
|
205
|
+
})
|
|
206
|
+
this.name = 'OperationError'
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Error thrown when schema validation fails for a document, row, key, or value.
|
|
212
|
+
*
|
|
213
|
+
* @public
|
|
214
|
+
*/
|
|
215
|
+
export class ValidationError extends HideABedError {
|
|
216
|
+
readonly issues: ValidationErrorOptions['issues']
|
|
217
|
+
|
|
218
|
+
constructor(options: ValidationErrorOptions) {
|
|
219
|
+
super(options.message ?? 'Validation failed', {
|
|
220
|
+
category: 'validation',
|
|
221
|
+
cause: options.cause,
|
|
222
|
+
couchError: options.couchError,
|
|
223
|
+
docId: options.docId,
|
|
224
|
+
operation: options.operation,
|
|
225
|
+
retryable: false,
|
|
226
|
+
statusCode: options.statusCode
|
|
227
|
+
})
|
|
228
|
+
this.name = 'ValidationError'
|
|
229
|
+
this.issues = options.issues
|
|
64
230
|
}
|
|
65
231
|
}
|
|
66
232
|
|
|
@@ -73,22 +239,24 @@ export class NotFoundError extends Error {
|
|
|
73
239
|
*
|
|
74
240
|
* @public
|
|
75
241
|
*/
|
|
76
|
-
export class RetryableError extends
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
242
|
+
export class RetryableError extends HideABedError {
|
|
243
|
+
constructor(
|
|
244
|
+
message: string,
|
|
245
|
+
statusCode?: number,
|
|
246
|
+
options: Omit<Partial<HideABedErrorOptions>, 'category' | 'retryable' | 'statusCode'> & {
|
|
247
|
+
category?: Extract<ErrorCategory, 'network' | 'retryable'>
|
|
248
|
+
} = {}
|
|
249
|
+
) {
|
|
250
|
+
super(message, {
|
|
251
|
+
category: options.category ?? 'retryable',
|
|
252
|
+
cause: options.cause,
|
|
253
|
+
couchError: options.couchError,
|
|
254
|
+
docId: options.docId,
|
|
255
|
+
operation: options.operation,
|
|
256
|
+
retryable: true,
|
|
257
|
+
statusCode
|
|
258
|
+
})
|
|
90
259
|
this.name = 'RetryableError'
|
|
91
|
-
this.statusCode = statusCode
|
|
92
260
|
}
|
|
93
261
|
|
|
94
262
|
/**
|
|
@@ -111,11 +279,17 @@ export class RetryableError extends Error {
|
|
|
111
279
|
* @throws {@link RetryableError} When the error maps to a retryable network condition.
|
|
112
280
|
* @throws {*} Re-throws the original error when it cannot be mapped.
|
|
113
281
|
*/
|
|
114
|
-
static handleNetworkError(err: unknown): never {
|
|
115
|
-
|
|
116
|
-
|
|
282
|
+
static handleNetworkError(err: unknown, operation: ErrorOperation = 'request'): never {
|
|
283
|
+
const networkError = getNestedNetworkError(err)
|
|
284
|
+
|
|
285
|
+
if (networkError) {
|
|
286
|
+
const statusCode = NETWORK_ERROR_STATUS_MAP[networkError.code]
|
|
117
287
|
if (statusCode) {
|
|
118
|
-
throw new RetryableError(
|
|
288
|
+
throw new RetryableError('Network request failed', statusCode, {
|
|
289
|
+
category: 'network',
|
|
290
|
+
cause: err,
|
|
291
|
+
operation
|
|
292
|
+
})
|
|
119
293
|
}
|
|
120
294
|
}
|
|
121
295
|
|
|
@@ -123,7 +297,59 @@ export class RetryableError extends Error {
|
|
|
123
297
|
}
|
|
124
298
|
}
|
|
125
299
|
|
|
300
|
+
type ResponseErrorOptions = {
|
|
301
|
+
body?: unknown
|
|
302
|
+
defaultMessage: string
|
|
303
|
+
docId?: string
|
|
304
|
+
notFoundMessage?: string
|
|
305
|
+
operation: ErrorOperation
|
|
306
|
+
statusCode?: number
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function createResponseError({
|
|
310
|
+
body,
|
|
311
|
+
defaultMessage,
|
|
312
|
+
docId,
|
|
313
|
+
notFoundMessage,
|
|
314
|
+
operation,
|
|
315
|
+
statusCode
|
|
316
|
+
}: ResponseErrorOptions): HideABedError {
|
|
317
|
+
const couchError = getCouchError(body)
|
|
318
|
+
|
|
319
|
+
if (statusCode === 404 && docId) {
|
|
320
|
+
return new NotFoundError(docId, {
|
|
321
|
+
couchError,
|
|
322
|
+
message: notFoundMessage,
|
|
323
|
+
operation,
|
|
324
|
+
statusCode
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (statusCode === 409 && docId) {
|
|
329
|
+
return new ConflictError(docId, {
|
|
330
|
+
couchError,
|
|
331
|
+
operation,
|
|
332
|
+
statusCode
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (RetryableError.isRetryableStatusCode(statusCode)) {
|
|
337
|
+
return new RetryableError(defaultMessage, statusCode, {
|
|
338
|
+
couchError,
|
|
339
|
+
operation
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return new OperationError(defaultMessage, {
|
|
344
|
+
couchError,
|
|
345
|
+
docId,
|
|
346
|
+
operation,
|
|
347
|
+
statusCode
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
126
351
|
export function isConflictError(err: unknown): boolean {
|
|
352
|
+
if (err instanceof ConflictError) return true
|
|
127
353
|
if (typeof err !== 'object' || err === null) return false
|
|
128
354
|
const candidate = err as { statusCode?: unknown }
|
|
129
355
|
return candidate.statusCode === 409
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { RetryableError } from './errors.mts'
|
|
2
|
+
import type { RequestOptions } from '../../schema/request.mts'
|
|
3
|
+
import { composeAbortSignal } from './request.mts'
|
|
4
|
+
|
|
5
|
+
export type HttpMethod = 'DELETE' | 'GET' | 'POST' | 'PUT'
|
|
6
|
+
|
|
7
|
+
type NativeFetchBody = RequestInit['body']
|
|
8
|
+
|
|
9
|
+
export type FetchBody =
|
|
10
|
+
| NativeFetchBody
|
|
11
|
+
| Record<string, unknown>
|
|
12
|
+
| Array<unknown>
|
|
13
|
+
| null
|
|
14
|
+
| undefined
|
|
15
|
+
|
|
16
|
+
export type FetchAuth = {
|
|
17
|
+
password: string
|
|
18
|
+
username: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type FetchResult<TBody> = {
|
|
22
|
+
body: TBody
|
|
23
|
+
headers: Headers
|
|
24
|
+
statusCode: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type FetchRequestOptions = {
|
|
28
|
+
auth?: FetchAuth
|
|
29
|
+
body?: FetchBody
|
|
30
|
+
headers?: Record<string, string>
|
|
31
|
+
method: HttpMethod
|
|
32
|
+
operation?:
|
|
33
|
+
| 'get'
|
|
34
|
+
| 'getAtRev'
|
|
35
|
+
| 'getDBInfo'
|
|
36
|
+
| 'patch'
|
|
37
|
+
| 'patchDangerously'
|
|
38
|
+
| 'put'
|
|
39
|
+
| 'query'
|
|
40
|
+
| 'queryStream'
|
|
41
|
+
| 'remove'
|
|
42
|
+
| 'request'
|
|
43
|
+
| 'watchDocs'
|
|
44
|
+
request?: RequestOptions
|
|
45
|
+
signal?: AbortSignal
|
|
46
|
+
url: string | URL
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const JSON_HEADERS = {
|
|
50
|
+
'Content-Type': 'application/json'
|
|
51
|
+
} as const
|
|
52
|
+
|
|
53
|
+
const hasHeader = (headers: Record<string, string>, name: string) => {
|
|
54
|
+
const expected = name.toLowerCase()
|
|
55
|
+
return Object.keys(headers).some(header => header.toLowerCase() === expected)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const toBasicAuthHeader = ({ username, password }: FetchAuth) => {
|
|
59
|
+
const token = Buffer.from(`${username}:${password}`).toString('base64')
|
|
60
|
+
return `Basic ${token}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const prepareRequest = (options: FetchRequestOptions) => {
|
|
64
|
+
const auth = options.auth
|
|
65
|
+
const headers = { ...(options.headers ?? {}) }
|
|
66
|
+
|
|
67
|
+
if (auth && !hasHeader(headers, 'Authorization')) {
|
|
68
|
+
headers.Authorization = toBasicAuthHeader(auth)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { headers }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const isAbortError = (err: unknown): err is DOMException => {
|
|
75
|
+
return err instanceof DOMException && err.name === 'AbortError'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const isTimeoutError = (err: unknown): err is DOMException => {
|
|
79
|
+
return err instanceof DOMException && err.name === 'TimeoutError'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const encodeBody = (body: FetchBody): NativeFetchBody | undefined => {
|
|
83
|
+
if (body == null) return undefined
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
typeof body === 'string' ||
|
|
87
|
+
body instanceof ArrayBuffer ||
|
|
88
|
+
ArrayBuffer.isView(body) ||
|
|
89
|
+
body instanceof Blob ||
|
|
90
|
+
body instanceof FormData ||
|
|
91
|
+
body instanceof URLSearchParams ||
|
|
92
|
+
body instanceof ReadableStream
|
|
93
|
+
) {
|
|
94
|
+
return body as NativeFetchBody
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return JSON.stringify(body)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const parseJsonResponse = async (response: Response): Promise<unknown> => {
|
|
101
|
+
if (response.status === 204 || response.status === 205) {
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const text = await response.text()
|
|
106
|
+
|
|
107
|
+
if (text.trim() === '') {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(text)
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (response.ok) {
|
|
115
|
+
throw err
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return text
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function fetchCouchJson<TBody = unknown>(
|
|
123
|
+
options: FetchRequestOptions
|
|
124
|
+
): Promise<FetchResult<TBody>> {
|
|
125
|
+
let response: Response
|
|
126
|
+
const { headers } = prepareRequest(options)
|
|
127
|
+
const { signal, timedOut } = composeAbortSignal(options.signal, options.request)
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
response = await fetch(options.url, {
|
|
131
|
+
method: options.method,
|
|
132
|
+
headers: {
|
|
133
|
+
...JSON_HEADERS,
|
|
134
|
+
...headers
|
|
135
|
+
},
|
|
136
|
+
body: encodeBody(options.body),
|
|
137
|
+
signal,
|
|
138
|
+
dispatcher: options.request?.dispatcher
|
|
139
|
+
})
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (timedOut() || isTimeoutError(err)) {
|
|
142
|
+
throw new RetryableError('Request timed out', 503, {
|
|
143
|
+
category: 'network',
|
|
144
|
+
cause: err,
|
|
145
|
+
operation: options.operation
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (isAbortError(err)) {
|
|
150
|
+
throw err
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
RetryableError.handleNetworkError(err, options.operation)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const body = (await parseJsonResponse(response)) as TBody
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
body,
|
|
160
|
+
headers: response.headers,
|
|
161
|
+
statusCode: response.status
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function fetchCouchStream(
|
|
166
|
+
options: FetchRequestOptions
|
|
167
|
+
): Promise<FetchResult<ReadableStream<Uint8Array> | null>> {
|
|
168
|
+
let response: Response
|
|
169
|
+
const { headers } = prepareRequest(options)
|
|
170
|
+
const { signal, timedOut } = composeAbortSignal(options.signal, options.request)
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
response = await fetch(options.url, {
|
|
174
|
+
method: options.method,
|
|
175
|
+
headers,
|
|
176
|
+
body: encodeBody(options.body),
|
|
177
|
+
signal,
|
|
178
|
+
dispatcher: options.request?.dispatcher
|
|
179
|
+
})
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (timedOut() || isTimeoutError(err)) {
|
|
182
|
+
throw new RetryableError('Request timed out', 503, {
|
|
183
|
+
category: 'network',
|
|
184
|
+
cause: err,
|
|
185
|
+
operation: options.operation
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (isAbortError(err)) {
|
|
190
|
+
throw err
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
RetryableError.handleNetworkError(err, options.operation)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
body: response.body,
|
|
198
|
+
headers: response.headers,
|
|
199
|
+
statusCode: response.status
|
|
200
|
+
}
|
|
201
|
+
}
|
package/impl/utils/parseRows.mts
CHANGED
|
@@ -2,9 +2,26 @@
|
|
|
2
2
|
import { ViewRow } from '../../schema/couch/couch.output.schema.ts'
|
|
3
3
|
import type { StandardSchemaV1 } from '../../types/standard-schema.ts'
|
|
4
4
|
import { z } from 'zod'
|
|
5
|
+
import { OperationError, ValidationError, type ErrorOperation } from './errors.mts'
|
|
5
6
|
|
|
6
7
|
export type OnInvalidDocAction = 'throw' | 'skip'
|
|
7
8
|
|
|
9
|
+
const createValidationError = (
|
|
10
|
+
issues: ReadonlyArray<StandardSchemaV1.Issue>,
|
|
11
|
+
options: {
|
|
12
|
+
defaultMessage?: string
|
|
13
|
+
docId?: string
|
|
14
|
+
operation?: ErrorOperation
|
|
15
|
+
}
|
|
16
|
+
) => {
|
|
17
|
+
return new ValidationError({
|
|
18
|
+
docId: options.docId,
|
|
19
|
+
issues,
|
|
20
|
+
message: options.defaultMessage ?? 'Row validation failed',
|
|
21
|
+
operation: options.operation ?? 'request'
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
export async function parseRows<
|
|
9
26
|
DocSchema extends StandardSchemaV1 = StandardSchemaV1<any>,
|
|
10
27
|
KeySchema extends StandardSchemaV1 = StandardSchemaV1<any>,
|
|
@@ -12,14 +29,18 @@ export async function parseRows<
|
|
|
12
29
|
>(
|
|
13
30
|
rows: unknown,
|
|
14
31
|
options: {
|
|
32
|
+
defaultMessage?: string
|
|
15
33
|
onInvalidDoc?: OnInvalidDocAction
|
|
16
34
|
docSchema?: DocSchema
|
|
17
35
|
keySchema?: KeySchema
|
|
36
|
+
operation?: ErrorOperation
|
|
18
37
|
valueSchema?: ValueSchema
|
|
19
38
|
}
|
|
20
39
|
) {
|
|
21
40
|
if (!Array.isArray(rows)) {
|
|
22
|
-
throw new
|
|
41
|
+
throw new OperationError(options.defaultMessage ?? 'Request failed', {
|
|
42
|
+
operation: options.operation ?? 'request'
|
|
43
|
+
})
|
|
23
44
|
}
|
|
24
45
|
|
|
25
46
|
type ParsedRow = {
|
|
@@ -44,14 +65,22 @@ export async function parseRows<
|
|
|
44
65
|
if (options.keySchema) {
|
|
45
66
|
const parsedKey = await options.keySchema['~standard'].validate(row.key)
|
|
46
67
|
if (parsedKey.issues) {
|
|
47
|
-
throw parsedKey.issues
|
|
68
|
+
throw createValidationError(parsedKey.issues, {
|
|
69
|
+
defaultMessage: options.defaultMessage,
|
|
70
|
+
docId: typeof row.id === 'string' ? row.id : undefined,
|
|
71
|
+
operation: options.operation
|
|
72
|
+
})
|
|
48
73
|
}
|
|
49
74
|
parsedRow.key = parsedKey.value
|
|
50
75
|
}
|
|
51
76
|
if (options.valueSchema) {
|
|
52
77
|
const parsedValue = await options.valueSchema['~standard'].validate(row.value)
|
|
53
78
|
if (parsedValue.issues) {
|
|
54
|
-
throw parsedValue.issues
|
|
79
|
+
throw createValidationError(parsedValue.issues, {
|
|
80
|
+
defaultMessage: options.defaultMessage,
|
|
81
|
+
docId: typeof row.id === 'string' ? row.id : undefined,
|
|
82
|
+
operation: options.operation
|
|
83
|
+
})
|
|
55
84
|
}
|
|
56
85
|
parsedRow.value = parsedValue.value
|
|
57
86
|
}
|
|
@@ -70,7 +99,11 @@ export async function parseRows<
|
|
|
70
99
|
return 'skip'
|
|
71
100
|
} else {
|
|
72
101
|
// throw by default
|
|
73
|
-
throw parsedDocRes.issues
|
|
102
|
+
throw createValidationError(parsedDocRes.issues, {
|
|
103
|
+
defaultMessage: options.defaultMessage,
|
|
104
|
+
docId: typeof row.id === 'string' ? row.id : undefined,
|
|
105
|
+
operation: options.operation
|
|
106
|
+
})
|
|
74
107
|
}
|
|
75
108
|
} else {
|
|
76
109
|
parsedDoc = parsedDocRes.value
|
|
@@ -80,7 +113,11 @@ export async function parseRows<
|
|
|
80
113
|
if (options.keySchema) {
|
|
81
114
|
const parsedKeyRes = await options.keySchema['~standard'].validate(row.key)
|
|
82
115
|
if (parsedKeyRes.issues) {
|
|
83
|
-
throw parsedKeyRes.issues
|
|
116
|
+
throw createValidationError(parsedKeyRes.issues, {
|
|
117
|
+
defaultMessage: options.defaultMessage,
|
|
118
|
+
docId: typeof row.id === 'string' ? row.id : undefined,
|
|
119
|
+
operation: options.operation
|
|
120
|
+
})
|
|
84
121
|
} else {
|
|
85
122
|
parsedKey = parsedKeyRes.value
|
|
86
123
|
}
|
|
@@ -89,7 +126,11 @@ export async function parseRows<
|
|
|
89
126
|
if (options.valueSchema) {
|
|
90
127
|
const parsedValueRes = await options.valueSchema['~standard'].validate(row.value)
|
|
91
128
|
if (parsedValueRes.issues) {
|
|
92
|
-
throw parsedValueRes.issues
|
|
129
|
+
throw createValidationError(parsedValueRes.issues, {
|
|
130
|
+
defaultMessage: options.defaultMessage,
|
|
131
|
+
docId: typeof row.id === 'string' ? row.id : undefined,
|
|
132
|
+
operation: options.operation
|
|
133
|
+
})
|
|
93
134
|
} else {
|
|
94
135
|
parsedValue = parsedValueRes.value
|
|
95
136
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RequestOptions } from '../../schema/request.mts'
|
|
2
|
+
|
|
3
|
+
const definedSignals = (signals: Array<AbortSignal | undefined>): AbortSignal[] => {
|
|
4
|
+
return signals.filter((signal): signal is AbortSignal => signal != null)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const composeAbortSignal = (
|
|
8
|
+
internalSignal?: AbortSignal,
|
|
9
|
+
request?: RequestOptions
|
|
10
|
+
): {
|
|
11
|
+
signal: AbortSignal | undefined
|
|
12
|
+
timedOut: () => boolean
|
|
13
|
+
} => {
|
|
14
|
+
const timeoutSignal =
|
|
15
|
+
typeof request?.timeout === 'number' ? AbortSignal.timeout(request.timeout) : undefined
|
|
16
|
+
const signals = definedSignals([internalSignal, request?.signal, timeoutSignal])
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],
|
|
20
|
+
timedOut: () => timeoutSignal?.aborted === true
|
|
21
|
+
}
|
|
22
|
+
}
|