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
package/eslint.config.js CHANGED
@@ -11,5 +11,10 @@ export default defineConfig([
11
11
  extends: ['js/recommended'],
12
12
  languageOptions: { globals: globals.browser }
13
13
  },
14
- tseslint.configs.recommended
14
+ tseslint.configs.recommended,
15
+ {
16
+ rules: {
17
+ '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }]
18
+ }
19
+ }
15
20
  ])
@@ -20,7 +20,34 @@ import { remove } from './remove.mts'
20
20
  import { createLock, removeLock } from './sugar/lock.mts'
21
21
  import { watchDocs } from './sugar/watch.mts'
22
22
 
23
- export type BoundInstance = ReturnType<typeof doBind> & {
23
+ type BoundConfigMethod<T> = T extends (...args: infer Args) => infer Result
24
+ ? Args extends [unknown, ...infer Rest]
25
+ ? (...args: Rest) => Result
26
+ : never
27
+ : never
28
+
29
+ type BoundMethods = {
30
+ bulkGet: BulkGetBound
31
+ bulkGetDictionary: BulkGetDictionaryBound
32
+ get: GetBound
33
+ getAtRev: GetAtRevBound
34
+ query: QueryBound
35
+ bulkRemove: BoundConfigMethod<typeof bulkRemove>
36
+ bulkRemoveMap: BoundConfigMethod<typeof bulkRemoveMap>
37
+ bulkSave: BoundConfigMethod<typeof bulkSave>
38
+ bulkSaveTransaction: BoundConfigMethod<typeof bulkSaveTransaction>
39
+ getDBInfo: BoundConfigMethod<typeof getDBInfo>
40
+ patch: BoundConfigMethod<typeof patch>
41
+ patchDangerously: BoundConfigMethod<typeof patchDangerously>
42
+ put: BoundConfigMethod<typeof put>
43
+ queryStream: BoundConfigMethod<typeof queryStream>
44
+ remove: BoundConfigMethod<typeof remove>
45
+ createLock: BoundConfigMethod<typeof createLock>
46
+ removeLock: BoundConfigMethod<typeof removeLock>
47
+ watchDocs: BoundConfigMethod<typeof watchDocs>
48
+ }
49
+
50
+ export type BoundInstance = BoundMethods & {
24
51
  options(overrides: Partial<z.input<typeof CouchConfig>>): BoundInstance
25
52
  }
26
53
 
@@ -81,7 +108,7 @@ export function getBoundWithRetry<
81
108
  * @param config The CouchDB configuration
82
109
  * @returns An object with CouchDB operations bound to the provided configuration
83
110
  */
84
- function doBind(config: CouchConfig) {
111
+ function doBind(config: CouchConfig): BoundMethods {
85
112
  // Default retry options
86
113
  const retryOptions = {
87
114
  maxRetries: config.maxRetries ?? 10,
@@ -90,7 +117,7 @@ function doBind(config: CouchConfig) {
90
117
  }
91
118
 
92
119
  // Create the object without the config property first
93
- const result = {
120
+ const result: BoundMethods = {
94
121
  /**
95
122
  * These functions use overloaded signatures when bound.
96
123
  * To preserve the overloads we need dedicated Bound types
package/impl/bulkGet.mts CHANGED
@@ -1,8 +1,6 @@
1
- import needle from 'needle'
2
1
  import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
3
2
  import { createLogger } from './utils/logger.mts'
4
- import { mergeNeedleOpts } from './utils/mergeNeedleOpts.mts'
5
- import { RetryableError } from './utils/errors.mts'
3
+ import { RetryableError, createResponseError } from './utils/errors.mts'
6
4
  import {
7
5
  ViewQueryResponse,
8
6
  type ViewQueryResponseValidated,
@@ -11,6 +9,15 @@ import {
11
9
  } from '../schema/couch/couch.output.schema.ts'
12
10
  import type { StandardSchemaV1 } from '../types/standard-schema.ts'
13
11
  import { parseRows, type OnInvalidDocAction } from './utils/parseRows.mts'
12
+ import { fetchCouchJson } from './utils/fetch.mts'
13
+ import { isSuccessStatusCode } from './utils/response.mts'
14
+ import { createCouchPathUrl } from './utils/url.mts'
15
+
16
+ type BulkGetBody = {
17
+ error?: string
18
+ reason?: string
19
+ rows?: unknown[]
20
+ } & Record<string, unknown>
14
21
 
15
22
  export type BulkGetResponse<DocSchema extends StandardSchemaV1 = StandardSchemaV1<CouchDoc>> =
16
23
  ViewQueryResponseValidated<
@@ -39,13 +46,13 @@ export type BulkGetOptions<DocSchema extends StandardSchemaV1> = {
39
46
  * @returns The raw response body from CouchDB
40
47
  *
41
48
  * @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
42
- * @throws {Error} When CouchDB returns a non-retryable error payload.
49
+ * @throws {OperationError} When CouchDB returns a non-retryable request-level failure.
43
50
  */
44
51
  async function executeBulkGet(
45
52
  _config: CouchConfigInput,
46
53
  ids: Array<string | undefined>,
47
54
  includeDocs: boolean
48
- ) {
55
+ ): Promise<BulkGetBody | undefined> {
49
56
  const configParseResult = CouchConfig.safeParse(_config)
50
57
  const logger = createLogger(_config)
51
58
  logger.info(`Starting bulk get for ${ids.length} documents`)
@@ -56,30 +63,40 @@ async function executeBulkGet(
56
63
  }
57
64
 
58
65
  const config = configParseResult.data
59
- const url = `${config.couch}/_all_docs${includeDocs ? '?include_docs=true' : ''}`
60
- const payload = { keys: ids }
61
- const opts = {
62
- json: true,
63
- headers: {
64
- 'Content-Type': 'application/json'
65
- }
66
+ const url = createCouchPathUrl('_all_docs', config.couch)
67
+ if (includeDocs) {
68
+ url.searchParams.append('include_docs', 'true')
66
69
  }
67
- const mergedOpts = mergeNeedleOpts(config, opts)
70
+ const payload = { keys: ids }
68
71
 
69
72
  try {
70
- const resp = await needle('post', url, payload, mergedOpts)
73
+ const resp = await fetchCouchJson<BulkGetBody>({
74
+ auth: config.auth,
75
+ method: 'POST',
76
+ operation: 'request',
77
+ request: config.request,
78
+ url,
79
+ body: payload
80
+ })
71
81
  if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
72
82
  logger.warn(`Retryable status code received: ${resp.statusCode}`)
73
- throw new RetryableError('retryable error during bulk get', resp.statusCode)
83
+ throw new RetryableError('Bulk get failed', resp.statusCode, {
84
+ operation: 'request'
85
+ })
74
86
  }
75
- if (resp.statusCode !== 200) {
87
+ if (!isSuccessStatusCode('bulkGet', resp.statusCode)) {
76
88
  logger.error(`Unexpected status code: ${resp.statusCode}`)
77
- throw new Error('could not fetch')
89
+ throw createResponseError({
90
+ body: resp.body,
91
+ defaultMessage: 'Bulk get failed',
92
+ operation: 'request',
93
+ statusCode: resp.statusCode
94
+ })
78
95
  }
79
96
  return resp.body
80
97
  } catch (err) {
81
98
  logger.error('Network error during bulk get:', err)
82
- RetryableError.handleNetworkError(err)
99
+ RetryableError.handleNetworkError(err, 'request')
83
100
  }
84
101
  }
85
102
 
@@ -95,8 +112,8 @@ async function executeBulkGet(
95
112
  * @returns The bulk get response with rows optionally validated against the supplied document schema.
96
113
  *
97
114
  * @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
98
- * @throws {Error<StandardSchemaV1.FailureResult["issues"]>} When the configuration or validation schemas fail to parse.
99
- * @throws {Error} When CouchDB returns a non-retryable error payload.
115
+ * @throws {ValidationError} When returned documents fail schema validation.
116
+ * @throws {OperationError} When CouchDB returns a non-retryable request-level failure.
100
117
  */
101
118
  async function _bulkGetWithOptions<DocSchema extends StandardSchemaV1 = typeof CouchDoc>(
102
119
  config: CouchConfigInput,
@@ -107,16 +124,22 @@ async function _bulkGetWithOptions<DocSchema extends StandardSchemaV1 = typeof C
107
124
  const body = await executeBulkGet(config, ids, includeDocs)
108
125
 
109
126
  if (!body) {
110
- throw new RetryableError('no response', 503)
127
+ throw new RetryableError('Bulk get failed', 503, { operation: 'request' })
111
128
  }
112
129
 
113
130
  if (body.error) {
114
- throw new Error(typeof body.reason === 'string' ? body.reason : 'could not fetch')
131
+ throw createResponseError({
132
+ body,
133
+ defaultMessage: 'Bulk get failed',
134
+ operation: 'request'
135
+ })
115
136
  }
116
137
 
117
138
  const docSchema = options.validate?.docSchema || CouchDoc
118
139
  const rows = await parseRows(body.rows, {
140
+ defaultMessage: 'Bulk get failed',
119
141
  onInvalidDoc: options.validate?.onInvalidDoc,
142
+ operation: 'request',
120
143
  docSchema
121
144
  })
122
145
 
@@ -144,8 +167,8 @@ async function _bulkGetWithOptions<DocSchema extends StandardSchemaV1 = typeof C
144
167
  * @returns The bulk get response with rows optionally validated against the supplied document schema.
145
168
  *
146
169
  * @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
147
- * @throws {Error<StandardSchemaV1.FailureResult["issues"]>} When the configuration or validation schemas fail to parse.
148
- * @throws {Error} When CouchDB returns a non-retryable error payload.
170
+ * @throws {ValidationError} When returned documents fail schema validation.
171
+ * @throws {OperationError} When CouchDB returns a non-retryable request-level failure.
149
172
  */
150
173
  export async function bulkGet<DocSchema extends StandardSchemaV1 = typeof CouchDoc>(
151
174
  config: CouchConfigInput,
@@ -196,7 +219,7 @@ export type BulkGetDictionaryResult<
196
219
  /**
197
220
  * Bulk get documents by IDs and return a dictionary of found and not found documents.
198
221
  *
199
- * @template DocSchema - Schema used to validate each returned document, if provided. Note: if a document is found and it fails validation this will throw a Error<StandardSchemaV1.FailureResult["issues"]>.
222
+ * @template DocSchema - Schema used to validate each returned document, if provided. Note: if a document is found and it fails validation this will throw a ValidationError.
200
223
  *
201
224
  * @param config - CouchDB configuration data that is validated before use.
202
225
  * @param ids - Array of document IDs to retrieve.
@@ -205,8 +228,8 @@ export type BulkGetDictionaryResult<
205
228
  * @returns An object containing found documents and not found rows.
206
229
  *
207
230
  * @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
208
- * @throws {Error<StandardSchemaV1.FailureResult["issues"]>} When the configuration or validation schemas fail to parse.
209
- * @throws {Error} When CouchDB returns a non-retryable error payload.
231
+ * @throws {ValidationError} When returned documents fail schema validation.
232
+ * @throws {OperationError} When CouchDB returns a non-retryable request-level failure.
210
233
  */
211
234
  export async function bulkGetDictionary<DocSchema extends StandardSchemaV1 = typeof CouchDoc>(
212
235
  config: CouchConfigInput,
@@ -27,7 +27,8 @@ import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
27
27
  * console.log(results);
28
28
  * ```
29
29
  *
30
- * @throws Will throw an error if the provided configuration is invalid or if the bulk delete operation fails.
30
+ * @throws {RetryableError} When the bulk request fails with a retryable transport or HTTP error.
31
+ * @throws {OperationError} When CouchDB returns a non-retryable request-level failure.
31
32
  */
32
33
  export const bulkRemove = async (configInput: CouchConfigInput, ids: string[]) => {
33
34
  const config = CouchConfig.parse(configInput)
@@ -71,7 +72,8 @@ export const bulkRemove = async (configInput: CouchConfigInput, ids: string[]) =
71
72
  * console.log(results);
72
73
  * ```
73
74
  *
74
- * @throws Will throw an error if the provided configuration is invalid or if any delete operation fails.
75
+ * @throws {RetryableError} When a request-level transport or retryable HTTP failure occurs.
76
+ * @throws {OperationError} When a request fails in a non-retryable way.
75
77
  */
76
78
  export const bulkRemoveMap = async (configInput: CouchConfigInput, ids: string[]) => {
77
79
  const config = CouchConfig.parse(configInput)
package/impl/bulkSave.mts CHANGED
@@ -1,6 +1,4 @@
1
- import needle from 'needle'
2
1
  import { createLogger } from './utils/logger.mts'
3
- import { mergeNeedleOpts } from './utils/mergeNeedleOpts.mts'
4
2
  import { bulkGetDictionary } from './bulkGet.mts'
5
3
  import { setupEmitter } from './utils/trackedEmitter.mts'
6
4
  import {
@@ -14,10 +12,18 @@ import {
14
12
  CouchDoc,
15
13
  type CouchDocInput
16
14
  } from '../schema/couch/couch.output.schema.ts'
17
- import type { CouchConfigInput } from '../schema/config.mts'
18
- import { RetryableError } from './utils/errors.mts'
15
+ import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
16
+ import {
17
+ ConflictError,
18
+ OperationError,
19
+ RetryableError,
20
+ createResponseError
21
+ } from './utils/errors.mts'
19
22
  import { withRetry } from './retry.mts'
20
23
  import { put } from './put.mts'
24
+ import { fetchCouchJson } from './utils/fetch.mts'
25
+ import { isSuccessStatusCode } from './utils/response.mts'
26
+ import { createCouchPathUrl } from './utils/url.mts'
21
27
 
22
28
  /**
23
29
  * Bulk saves documents to CouchDB using the _bulk_docs endpoint.
@@ -30,44 +36,54 @@ import { put } from './put.mts'
30
36
  * @returns {Promise<BulkSaveResponse>} - The response from CouchDB after the bulk save operation.
31
37
  *
32
38
  * @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
33
- * @throws {Error} When CouchDB returns a non-retryable error payload.
39
+ * @throws {OperationError} When bulk save input is invalid or CouchDB returns a non-retryable request-level failure.
34
40
  */
35
41
  export const bulkSave = async (config: CouchConfigInput, docs: CouchDocInput[]) => {
36
- const logger = createLogger(config)
42
+ const parsedConfig = CouchConfig.parse(config)
43
+ const logger = createLogger(parsedConfig)
37
44
 
38
45
  if (docs == null || !docs.length) {
39
46
  logger.error('bulkSave called with no docs')
40
- throw new Error('no docs provided')
47
+ throw new OperationError('Bulk save requires at least one document', {
48
+ operation: 'request'
49
+ })
41
50
  }
42
51
 
43
52
  logger.info(`Starting bulk save of ${docs.length} documents`)
44
- const url = `${config.couch}/_bulk_docs`
53
+ const url = createCouchPathUrl('_bulk_docs', parsedConfig.couch)
45
54
  const body = { docs }
46
- const opts = {
47
- json: true,
48
- headers: {
49
- 'Content-Type': 'application/json'
50
- }
51
- }
52
- const mergedOpts = mergeNeedleOpts(config, opts)
53
55
  let resp
54
56
  try {
55
- resp = await needle('post', url, body, mergedOpts)
57
+ resp = await fetchCouchJson({
58
+ auth: parsedConfig.auth,
59
+ method: 'POST',
60
+ operation: 'request',
61
+ request: parsedConfig.request,
62
+ url,
63
+ body
64
+ })
56
65
  } catch (err) {
57
66
  logger.error('Network error during bulk save:', err)
58
- RetryableError.handleNetworkError(err)
67
+ RetryableError.handleNetworkError(err, 'request')
59
68
  }
60
69
  if (!resp) {
61
70
  logger.error('No response received from bulk save request')
62
- throw new RetryableError('no response', 503)
71
+ throw new RetryableError('Bulk save failed', 503, { operation: 'request' })
63
72
  }
64
73
  if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
65
74
  logger.warn(`Retryable status code received: ${resp.statusCode}`)
66
- throw new RetryableError('retryable error during bulk save', resp.statusCode)
75
+ throw new RetryableError('Bulk save failed', resp.statusCode, {
76
+ operation: 'request'
77
+ })
67
78
  }
68
- if (resp.statusCode !== 201) {
79
+ if (!isSuccessStatusCode('bulkSave', resp.statusCode)) {
69
80
  logger.error(`Unexpected status code: ${resp.statusCode}`)
70
- throw new Error('could not save')
81
+ throw createResponseError({
82
+ body: resp.body,
83
+ defaultMessage: 'Bulk save failed',
84
+ operation: 'request',
85
+ statusCode: resp.statusCode
86
+ })
71
87
  }
72
88
  const results = resp?.body || []
73
89
  return BulkSaveResponse.parse(results)
@@ -151,18 +167,24 @@ export const bulkSaveTransaction = async (
151
167
  }
152
168
 
153
169
  // Save transaction document
154
- let transactionResponse = await _put(transactionDoc)
170
+ let transactionResponse
171
+ try {
172
+ transactionResponse = await _put(transactionDoc)
173
+ } catch (error) {
174
+ if (error instanceof ConflictError) {
175
+ throw new TransactionSetupError('Failed to create transaction document', {
176
+ error: error.couchError,
177
+ response: error
178
+ })
179
+ }
180
+
181
+ throw error
182
+ }
155
183
  logger.debug('Transaction document created:', transactionDoc, transactionResponse)
156
184
  await emitter.emit('transaction-created', {
157
185
  transactionResponse,
158
186
  txnDoc: transactionDoc
159
187
  })
160
- if (transactionResponse.error) {
161
- throw new TransactionSetupError('Failed to create transaction document', {
162
- error: transactionResponse.error,
163
- response: transactionResponse
164
- })
165
- }
166
188
 
167
189
  // Get current revisions of all documents
168
190
  const existingDocs = await bulkGetDictionary(
package/impl/get.mts CHANGED
@@ -1,11 +1,17 @@
1
- import needle from 'needle'
2
1
  import { z } from 'zod'
3
- import type { CouchConfigInput } from '../schema/config.mts'
4
2
  import { createLogger } from './utils/logger.mts'
5
- import { mergeNeedleOpts } from './utils/mergeNeedleOpts.mts'
6
- import { RetryableError, NotFoundError } from './utils/errors.mts'
3
+ import {
4
+ RetryableError,
5
+ NotFoundError,
6
+ ValidationError,
7
+ createResponseError
8
+ } from './utils/errors.mts'
7
9
  import type { StandardSchemaV1 } from '../types/standard-schema.ts'
8
10
  import { CouchDoc } from '../schema/couch/couch.output.schema.ts'
11
+ import { fetchCouchJson } from './utils/fetch.mts'
12
+ import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
13
+ import { isSuccessStatusCode } from './utils/response.mts'
14
+ import { createCouchDocUrl } from './utils/url.mts'
9
15
 
10
16
  export type GetOptions<DocSchema extends StandardSchemaV1> = {
11
17
  validate?: {
@@ -26,7 +32,7 @@ const ValidSchema = z.custom(
26
32
  }
27
33
  )
28
34
 
29
- export const CouchGetOptions = z.object({
35
+ export const CouchGetOptions = z.strictObject({
30
36
  rev: z.string().optional().describe('the couch doc revision'),
31
37
  validate: z
32
38
  .object({
@@ -37,74 +43,74 @@ export const CouchGetOptions = z.object({
37
43
  })
38
44
 
39
45
  async function _getWithOptions<DocSchema extends StandardSchemaV1>(
40
- config: CouchConfigInput,
46
+ configInput: CouchConfigInput,
41
47
  id: string,
42
48
  options: InternalGetOptions<DocSchema>
43
49
  ): Promise<StandardSchemaV1.InferOutput<DocSchema> | null> {
44
- const parsedOptions = CouchGetOptions.parse({
45
- rev: options.rev,
46
- validate: options.validate
47
- })
50
+ const config = CouchConfig.parse(configInput)
51
+ const parsedOptions = CouchGetOptions.parse(options)
48
52
 
49
53
  const logger = createLogger(config)
50
54
  const rev = parsedOptions.rev
51
- const path = rev ? `${id}?rev=${rev}` : id
52
- const url = `${config.couch}/${path}`
55
+ const operation = rev ? 'getAtRev' : 'get'
56
+ const url = createCouchDocUrl(id, config.couch)
53
57
 
54
- const httpOptions = {
55
- json: true,
56
- headers: {
57
- 'Content-Type': 'application/json'
58
- }
58
+ if (rev) {
59
+ url.searchParams.set('rev', rev)
59
60
  }
60
-
61
- const requestOptions = mergeNeedleOpts(config, httpOptions)
62
61
  logger.info(`Getting document with id: ${id}, rev ${rev ?? 'latest'}`)
63
62
 
64
63
  try {
65
- const resp = await needle('get', url, null, requestOptions)
64
+ const resp = await fetchCouchJson({
65
+ auth: config.auth,
66
+ method: 'GET',
67
+ operation,
68
+ request: config.request,
69
+ url
70
+ })
66
71
  if (!resp) {
67
72
  logger.error('No response received from get request')
68
- throw new RetryableError('no response', 503)
73
+ throw new RetryableError('Request failed', 503, { operation })
69
74
  }
70
75
 
71
76
  const body = resp.body ?? null
72
77
 
73
78
  if (resp.statusCode === 404) {
74
- if (config.throwOnGetNotFound) {
75
- const reason = typeof body?.reason === 'string' ? body.reason : 'not_found'
76
- logger.warn(`Document not found (throwing error): ${id}, rev ${rev ?? 'latest'}`)
77
- throw new NotFoundError(id, reason)
79
+ logger.warn(`Document not found: ${id}, rev ${rev ?? 'latest'}`)
80
+ if (config.throwOnGetNotFound === false) {
81
+ return null
78
82
  }
79
-
80
- logger.debug(`Document not found (returning undefined): ${id}, rev ${rev ?? 'latest'}`)
81
- return null
82
- }
83
-
84
- if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
85
- const reason = typeof body?.reason === 'string' ? body.reason : 'retryable error'
86
- logger.warn(`Retryable status code received: ${resp.statusCode}`)
87
- throw new RetryableError(reason, resp.statusCode)
83
+ throw new NotFoundError(id, { operation, statusCode: resp.statusCode })
88
84
  }
89
85
 
90
- if (resp.statusCode !== 200) {
91
- const reason = typeof body?.reason === 'string' ? body.reason : 'failed'
86
+ if (!isSuccessStatusCode('documentRead', resp.statusCode)) {
92
87
  logger.error(`Unexpected status code: ${resp.statusCode}`)
93
- throw new Error(reason)
88
+ throw createResponseError({
89
+ body,
90
+ defaultMessage: 'Failed to fetch document',
91
+ docId: id,
92
+ operation,
93
+ statusCode: resp.statusCode
94
+ })
94
95
  }
95
96
 
96
97
  const docSchema = (parsedOptions.validate?.docSchema ?? CouchDoc) as DocSchema
97
98
  const typedDoc = await docSchema['~standard'].validate(body)
98
99
 
99
100
  if (typedDoc.issues) {
100
- throw typedDoc.issues
101
+ throw new ValidationError({
102
+ docId: id,
103
+ issues: typedDoc.issues,
104
+ message: 'Document validation failed',
105
+ operation
106
+ })
101
107
  }
102
108
 
103
109
  logger.info(`Successfully retrieved document: ${id}, rev ${rev ?? 'latest'}`)
104
110
  return typedDoc.value
105
111
  } catch (err) {
106
112
  logger.error('Error during get operation:', err)
107
- RetryableError.handleNetworkError(err)
113
+ RetryableError.handleNetworkError(err, operation)
108
114
  }
109
115
  }
110
116
 
@@ -127,7 +133,10 @@ export async function getAtRev<DocSchema extends StandardSchemaV1>(
127
133
  rev: string,
128
134
  options?: GetOptions<DocSchema>
129
135
  ): Promise<StandardSchemaV1.InferOutput<DocSchema> | null> {
130
- return _getWithOptions<DocSchema>(config, id, { ...options, rev })
136
+ return _getWithOptions<DocSchema>(config, id, {
137
+ ...options,
138
+ rev
139
+ })
131
140
  }
132
141
 
133
142
  export type GetAtRevBound = <DocSchema extends StandardSchemaV1 = typeof CouchDoc>(
@@ -1,9 +1,10 @@
1
- import needle, { type NeedleResponse } from 'needle'
2
- import { RetryableError } from './utils/errors.mts'
1
+ import { RetryableError, createResponseError } from './utils/errors.mts'
3
2
  import { createLogger } from './utils/logger.mts'
4
- import { mergeNeedleOpts } from './utils/mergeNeedleOpts.mts'
5
3
  import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
6
4
  import { CouchDBInfo } from '../schema/couch/couch.output.schema.ts'
5
+ import { fetchCouchJson } from './utils/fetch.mts'
6
+ import { isSuccessStatusCode } from './utils/response.mts'
7
+ import { createCouchDbUrl } from './utils/url.mts'
7
8
 
8
9
  /**
9
10
  * Fetches and returns CouchDB database information.
@@ -13,7 +14,7 @@ import { CouchDBInfo } from '../schema/couch/couch.output.schema.ts'
13
14
  * @param configInput - The CouchDB configuration input.
14
15
  * @returns A promise that resolves to the CouchDB database information.
15
16
  * @throws {RetryableError} `RetryableError` If a retryable error occurs during the request.
16
- * @throws {Error} `Error` For other non-retryable errors.
17
+ * @throws {OperationError} `OperationError` For other non-retryable response failures.
17
18
  *
18
19
  * @example
19
20
  * ```ts
@@ -33,35 +34,36 @@ import { CouchDBInfo } from '../schema/couch/couch.output.schema.ts'
33
34
  export const getDBInfo = async (configInput: CouchConfigInput) => {
34
35
  const config = CouchConfig.parse(configInput)
35
36
  const logger = createLogger(config)
36
- const url = `${config.couch}`
37
+ const url = createCouchDbUrl(config.couch)
37
38
 
38
- let resp: NeedleResponse | undefined
39
+ let resp
39
40
  try {
40
- resp = await needle(
41
- 'get',
42
- url,
43
- mergeNeedleOpts(config, {
44
- json: true,
45
- headers: {
46
- 'Content-Type': 'application/json'
47
- }
41
+ resp = await fetchCouchJson({
42
+ auth: config.auth,
43
+ method: 'GET',
44
+ operation: 'getDBInfo',
45
+ request: config.request,
46
+ url
47
+ })
48
+
49
+ if (!isSuccessStatusCode('database', resp.statusCode)) {
50
+ logger.error(`Non-success status code received: ${resp.statusCode}`)
51
+ throw createResponseError({
52
+ body: resp.body,
53
+ defaultMessage: 'Failed to fetch database info',
54
+ operation: 'getDBInfo',
55
+ statusCode: resp.statusCode
48
56
  })
49
- )
57
+ }
50
58
  } catch (err) {
51
59
  logger.error('Error during get operation:', err)
52
- RetryableError.handleNetworkError(err)
60
+ RetryableError.handleNetworkError(err, 'getDBInfo')
53
61
  }
54
62
 
55
63
  if (!resp) {
56
64
  logger.error('No response received from get request')
57
- throw new RetryableError('no response', 503)
58
- }
59
-
60
- const result = resp.body
61
- if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
62
- logger.warn(`Retryable status code received: ${resp.statusCode}`)
63
- throw new RetryableError(result.reason ?? 'retryable error', resp.statusCode)
65
+ throw new RetryableError('Failed to fetch database info', 503, { operation: 'getDBInfo' })
64
66
  }
65
67
 
66
- return CouchDBInfo.parse(result)
68
+ return CouchDBInfo.parse(resp.body)
67
69
  }