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.
Files changed (100) hide show
  1. package/README.md +89 -28
  2. package/dist/cjs/index.cjs +888 -443
  3. package/dist/esm/index.mjs +883 -443
  4. package/eslint.config.js +6 -1
  5. package/impl/bindConfig.mts +30 -3
  6. package/impl/bulkGet.mts +50 -27
  7. package/impl/bulkRemove.mts +4 -2
  8. package/impl/bulkSave.mts +50 -28
  9. package/impl/get.mts +49 -40
  10. package/impl/getDBInfo.mts +26 -24
  11. package/impl/patch.mts +46 -42
  12. package/impl/put.mts +39 -21
  13. package/impl/query.mts +101 -81
  14. package/impl/remove.mts +33 -33
  15. package/impl/stream.mts +163 -102
  16. package/impl/sugar/watch.mts +165 -97
  17. package/impl/utils/errors.mts +261 -35
  18. package/impl/utils/fetch.mts +201 -0
  19. package/impl/utils/parseRows.mts +47 -6
  20. package/impl/utils/request.mts +22 -0
  21. package/impl/utils/response.mts +50 -0
  22. package/impl/utils/transactionErrors.mts +14 -8
  23. package/impl/utils/url.mts +21 -0
  24. package/index.mts +19 -2
  25. package/migration_guides/v7.md +353 -0
  26. package/package.json +4 -4
  27. package/schema/config.mts +17 -34
  28. package/schema/request.mts +36 -0
  29. package/schema/sugar/watch.mts +1 -1
  30. package/tsconfig.json +9 -1
  31. package/types/output/impl/bindConfig.d.mts +31 -149
  32. package/types/output/impl/bindConfig.d.mts.map +1 -1
  33. package/types/output/impl/bindConfig.test.d.mts +2 -0
  34. package/types/output/impl/bindConfig.test.d.mts.map +1 -0
  35. package/types/output/impl/bulkGet.d.mts +5 -5
  36. package/types/output/impl/bulkGet.d.mts.map +1 -1
  37. package/types/output/impl/bulkRemove.d.mts +4 -2
  38. package/types/output/impl/bulkRemove.d.mts.map +1 -1
  39. package/types/output/impl/bulkSave.d.mts +2 -2
  40. package/types/output/impl/bulkSave.d.mts.map +1 -1
  41. package/types/output/impl/get.d.mts +2 -2
  42. package/types/output/impl/get.d.mts.map +1 -1
  43. package/types/output/impl/getDBInfo.d.mts +1 -1
  44. package/types/output/impl/getDBInfo.d.mts.map +1 -1
  45. package/types/output/impl/patch.d.mts +8 -3
  46. package/types/output/impl/patch.d.mts.map +1 -1
  47. package/types/output/impl/put.d.mts.map +1 -1
  48. package/types/output/impl/query.d.mts +8 -23
  49. package/types/output/impl/query.d.mts.map +1 -1
  50. package/types/output/impl/remove.d.mts.map +1 -1
  51. package/types/output/impl/request-controls.test.d.mts +2 -0
  52. package/types/output/impl/request-controls.test.d.mts.map +1 -0
  53. package/types/output/impl/stream.d.mts +1 -1
  54. package/types/output/impl/stream.d.mts.map +1 -1
  55. package/types/output/impl/sugar/watch.d.mts +7 -5
  56. package/types/output/impl/sugar/watch.d.mts.map +1 -1
  57. package/types/output/impl/utils/errors.d.mts +84 -26
  58. package/types/output/impl/utils/errors.d.mts.map +1 -1
  59. package/types/output/impl/utils/fetch.d.mts +27 -0
  60. package/types/output/impl/utils/fetch.d.mts.map +1 -0
  61. package/types/output/impl/utils/fetch.test.d.mts +2 -0
  62. package/types/output/impl/utils/fetch.test.d.mts.map +1 -0
  63. package/types/output/impl/utils/parseRows.d.mts +3 -0
  64. package/types/output/impl/utils/parseRows.d.mts.map +1 -1
  65. package/types/output/impl/utils/request.d.mts +6 -0
  66. package/types/output/impl/utils/request.d.mts.map +1 -0
  67. package/types/output/impl/utils/response.d.mts +7 -0
  68. package/types/output/impl/utils/response.d.mts.map +1 -0
  69. package/types/output/impl/utils/response.test.d.mts +2 -0
  70. package/types/output/impl/utils/response.test.d.mts.map +1 -0
  71. package/types/output/impl/utils/trackedEmitter.test.d.mts +2 -0
  72. package/types/output/impl/utils/trackedEmitter.test.d.mts.map +1 -0
  73. package/types/output/impl/utils/transactionErrors.d.mts +5 -4
  74. package/types/output/impl/utils/transactionErrors.d.mts.map +1 -1
  75. package/types/output/impl/utils/transactionErrors.test.d.mts +2 -0
  76. package/types/output/impl/utils/transactionErrors.test.d.mts.map +1 -0
  77. package/types/output/impl/utils/url.d.mts +4 -0
  78. package/types/output/impl/utils/url.d.mts.map +1 -0
  79. package/types/output/impl/utils/url.test.d.mts +2 -0
  80. package/types/output/impl/utils/url.test.d.mts.map +1 -0
  81. package/types/output/index.d.mts +5 -2
  82. package/types/output/index.d.mts.map +1 -1
  83. package/types/output/schema/config.d.mts +13 -69
  84. package/types/output/schema/config.d.mts.map +1 -1
  85. package/types/output/schema/config.test.d.mts +2 -0
  86. package/types/output/schema/config.test.d.mts.map +1 -0
  87. package/types/output/schema/request.d.mts +10 -0
  88. package/types/output/schema/request.d.mts.map +1 -0
  89. package/types/output/schema/sugar/lock.test.d.mts +2 -0
  90. package/types/output/schema/sugar/lock.test.d.mts.map +1 -0
  91. package/types/output/schema/sugar/watch.d.mts +1 -1
  92. package/types/output/schema/sugar/watch.d.mts.map +1 -1
  93. package/types/output/schema/sugar/watch.test.d.mts +2 -0
  94. package/types/output/schema/sugar/watch.test.d.mts.map +1 -0
  95. package/impl/utils/mergeNeedleOpts.mts +0 -16
  96. package/schema/util.mts +0 -8
  97. package/types/output/impl/utils/mergeNeedleOpts.d.mts +0 -53
  98. package/types/output/impl/utils/mergeNeedleOpts.d.mts.map +0 -1
  99. package/types/output/schema/util.d.mts +0 -85
  100. package/types/output/schema/util.d.mts.map +0 -1
@@ -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 libraries such as `needle`.
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 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)
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
- this.docId = docId
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 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)
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
- if (isNetworkError(err)) {
116
- const statusCode = NETWORK_ERROR_STATUS_MAP[err.code]
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(`Network error: ${err.code}`, statusCode)
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
+ }
@@ -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 Error('invalid rows format')
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
+ }