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/patch.mts
CHANGED
|
@@ -4,6 +4,7 @@ import { createLogger } from './utils/logger.mts'
|
|
|
4
4
|
import { setTimeout } from 'node:timers/promises'
|
|
5
5
|
import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
|
|
6
6
|
import { z } from 'zod'
|
|
7
|
+
import { ConflictError, HideABedError, OperationError, RetryableError } from './utils/errors.mts'
|
|
7
8
|
|
|
8
9
|
const PatchProperties = z
|
|
9
10
|
.looseObject({
|
|
@@ -20,7 +21,10 @@ const PatchProperties = z
|
|
|
20
21
|
* @param _properties - Properties to merge into the document (must include _rev)
|
|
21
22
|
* @returns The result of the put operation
|
|
22
23
|
*
|
|
23
|
-
* @throws
|
|
24
|
+
* @throws {ConflictError} When the supplied `_rev` does not match the current document revision.
|
|
25
|
+
* @throws {NotFoundError} When the document does not exist.
|
|
26
|
+
* @throws {RetryableError} When a retryable transport or HTTP failure occurs while reading or saving.
|
|
27
|
+
* @throws {OperationError} When a non-retryable operational failure occurs.
|
|
24
28
|
*/
|
|
25
29
|
export const patch = async (
|
|
26
30
|
configInput: CouchConfigInput,
|
|
@@ -33,13 +37,15 @@ export const patch = async (
|
|
|
33
37
|
|
|
34
38
|
logger.info(`Starting patch operation for document ${id}`)
|
|
35
39
|
logger.debug('Patch properties:', properties)
|
|
36
|
-
const doc = await get(config, id)
|
|
37
|
-
if (doc
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const doc = await get({ ...config, throwOnGetNotFound: true }, id)
|
|
41
|
+
if (!doc) {
|
|
42
|
+
throw new OperationError('Patch failed', {
|
|
43
|
+
docId: id,
|
|
44
|
+
operation: 'patch'
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
if (doc._rev !== properties._rev) {
|
|
48
|
+
throw new ConflictError(id, { operation: 'patch' })
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
const updatedDoc = { ...doc, ...properties }
|
|
@@ -60,7 +66,9 @@ export const patch = async (
|
|
|
60
66
|
* @param properties - Properties to merge into the document
|
|
61
67
|
* @returns The result of the put operation or an error if max retries are exceeded
|
|
62
68
|
*
|
|
63
|
-
* @throws
|
|
69
|
+
* @throws {NotFoundError} When the document does not exist.
|
|
70
|
+
* @throws {RetryableError} When a retryable transport or HTTP failure occurs before retries are exhausted.
|
|
71
|
+
* @throws {OperationError} When retries are exhausted or a non-retryable operational failure occurs.
|
|
64
72
|
*/
|
|
65
73
|
export const patchDangerously = async (
|
|
66
74
|
configInput: CouchConfigInput,
|
|
@@ -75,60 +83,56 @@ export const patchDangerously = async (
|
|
|
75
83
|
|
|
76
84
|
logger.info(`Starting patch operation for document ${id}`)
|
|
77
85
|
logger.debug('Patch properties:', properties)
|
|
86
|
+
let lastError: unknown
|
|
78
87
|
|
|
79
88
|
while (attempts <= maxRetries) {
|
|
80
89
|
logger.debug(`Attempt ${attempts + 1} of ${maxRetries + 1}`)
|
|
81
90
|
try {
|
|
82
|
-
const doc = await get(config, id)
|
|
91
|
+
const doc = await get({ ...config, throwOnGetNotFound: true }, id)
|
|
83
92
|
if (!doc) {
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
throw new OperationError('Patch failed', {
|
|
94
|
+
docId: id,
|
|
95
|
+
operation: 'patchDangerously'
|
|
96
|
+
})
|
|
86
97
|
}
|
|
87
|
-
|
|
88
98
|
const updatedDoc = { ...doc, ...properties }
|
|
89
99
|
logger.debug('Merged document:', updatedDoc)
|
|
90
100
|
|
|
91
101
|
const result = await put(config, updatedDoc)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// If not ok, treat as conflict and retry
|
|
100
|
-
attempts++
|
|
101
|
-
if (attempts > maxRetries) {
|
|
102
|
-
logger.error(`Failed to patch ${id} after ${maxRetries} attempts`)
|
|
103
|
-
throw new Error(`Failed to patch after ${maxRetries} attempts`)
|
|
102
|
+
logger.info(`Successfully patched document ${id}, rev: ${result.rev}`)
|
|
103
|
+
return result
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (!(err instanceof Error)) {
|
|
106
|
+
throw err
|
|
104
107
|
}
|
|
105
108
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
delay *= config.backoffFactor || 2
|
|
109
|
-
logger.debug(`Next retry delay: ${delay}ms`)
|
|
110
|
-
} catch (err) {
|
|
111
|
-
if (
|
|
112
|
-
typeof err === 'object' &&
|
|
113
|
-
err !== null &&
|
|
114
|
-
'message' in err &&
|
|
115
|
-
err.message === 'not_found'
|
|
116
|
-
) {
|
|
117
|
-
logger.warn(`Document ${id} not found during patch operation`)
|
|
118
|
-
return { ok: false, statusCode: 404, error: 'not_found' }
|
|
109
|
+
if (!(err instanceof ConflictError) && !(err instanceof RetryableError)) {
|
|
110
|
+
throw err
|
|
119
111
|
}
|
|
120
112
|
|
|
121
|
-
|
|
113
|
+
lastError = err
|
|
122
114
|
attempts++
|
|
123
115
|
if (attempts > maxRetries) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
116
|
+
logger.error(`Failed to patch ${id} after ${maxRetries} attempts`, err)
|
|
117
|
+
throw new OperationError('Patch failed', {
|
|
118
|
+
cause: err,
|
|
119
|
+
couchError: err instanceof HideABedError ? err.couchError : undefined,
|
|
120
|
+
docId: id,
|
|
121
|
+
operation: 'patchDangerously',
|
|
122
|
+
statusCode: err instanceof HideABedError ? err.statusCode : undefined
|
|
123
|
+
})
|
|
127
124
|
}
|
|
128
125
|
|
|
129
126
|
logger.warn(`Error during patch attempt ${attempts}: ${err}`)
|
|
130
127
|
await setTimeout(delay)
|
|
128
|
+
delay *= config.backoffFactor || 2
|
|
131
129
|
logger.debug(`Retrying after ${delay}ms`)
|
|
132
130
|
}
|
|
133
131
|
}
|
|
132
|
+
|
|
133
|
+
throw new OperationError('Patch failed', {
|
|
134
|
+
cause: lastError,
|
|
135
|
+
docId: id,
|
|
136
|
+
operation: 'patchDangerously'
|
|
137
|
+
})
|
|
134
138
|
}
|
package/impl/put.mts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import needle from 'needle'
|
|
2
1
|
import { createLogger } from './utils/logger.mts'
|
|
3
|
-
import {
|
|
4
|
-
import { RetryableError } from './utils/errors.mts'
|
|
2
|
+
import { ConflictError, RetryableError, createResponseError } from './utils/errors.mts'
|
|
5
3
|
import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
|
|
6
4
|
import { CouchPutResponse, type CouchDoc } from '../schema/couch/couch.output.schema.ts'
|
|
7
5
|
import { z } from 'zod'
|
|
6
|
+
import { fetchCouchJson } from './utils/fetch.mts'
|
|
7
|
+
import { isRecord, isSuccessStatusCode } from './utils/response.mts'
|
|
8
|
+
import { createCouchDocUrl } from './utils/url.mts'
|
|
9
|
+
|
|
10
|
+
type CouchMutationBody = {
|
|
11
|
+
error?: string
|
|
12
|
+
ok?: boolean
|
|
13
|
+
reason?: string
|
|
14
|
+
statusCode?: number
|
|
15
|
+
} & Record<string, unknown>
|
|
8
16
|
|
|
9
17
|
export const put = async (
|
|
10
18
|
configInput: CouchConfigInput,
|
|
@@ -12,43 +20,53 @@ export const put = async (
|
|
|
12
20
|
): Promise<z.infer<typeof CouchPutResponse>> => {
|
|
13
21
|
const config = CouchConfig.parse(configInput)
|
|
14
22
|
const logger = createLogger(config)
|
|
15
|
-
const url =
|
|
23
|
+
const url = createCouchDocUrl(doc._id, config.couch)
|
|
16
24
|
const body = doc
|
|
17
|
-
const opts = {
|
|
18
|
-
json: true,
|
|
19
|
-
headers: {
|
|
20
|
-
'Content-Type': 'application/json'
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
const mergedOpts = mergeNeedleOpts(config, opts)
|
|
24
25
|
|
|
25
26
|
logger.info(`Putting document with id: ${doc._id}`)
|
|
26
27
|
let resp
|
|
27
28
|
try {
|
|
28
|
-
resp = await
|
|
29
|
+
resp = await fetchCouchJson({
|
|
30
|
+
auth: config.auth,
|
|
31
|
+
method: 'PUT',
|
|
32
|
+
operation: 'put',
|
|
33
|
+
request: config.request,
|
|
34
|
+
url,
|
|
35
|
+
body
|
|
36
|
+
})
|
|
29
37
|
} catch (err) {
|
|
30
38
|
logger.error('Error during put operation:', err)
|
|
31
|
-
RetryableError.handleNetworkError(err)
|
|
39
|
+
RetryableError.handleNetworkError(err, 'put')
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
if (!resp) {
|
|
35
43
|
logger.error('No response received from put request')
|
|
36
|
-
throw new RetryableError('
|
|
44
|
+
throw new RetryableError('Put failed', 503, { operation: 'put' })
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
const result =
|
|
47
|
+
const result: CouchMutationBody = {
|
|
48
|
+
...(isRecord(resp.body) ? resp.body : {})
|
|
49
|
+
}
|
|
40
50
|
result.statusCode = resp.statusCode
|
|
41
51
|
|
|
42
52
|
if (resp.statusCode === 409) {
|
|
43
53
|
logger.warn(`Conflict detected for document: ${doc._id}`)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
throw new ConflictError(doc._id, {
|
|
55
|
+
couchError: typeof result.error === 'string' ? result.error : undefined,
|
|
56
|
+
operation: 'put',
|
|
57
|
+
statusCode: resp.statusCode
|
|
58
|
+
})
|
|
47
59
|
}
|
|
48
60
|
|
|
49
|
-
if (
|
|
50
|
-
logger.
|
|
51
|
-
throw
|
|
61
|
+
if (!isSuccessStatusCode('documentWrite', resp.statusCode) || !result.ok) {
|
|
62
|
+
logger.error(`Unexpected status code: ${resp.statusCode}`)
|
|
63
|
+
throw createResponseError({
|
|
64
|
+
body: resp.body,
|
|
65
|
+
defaultMessage: 'Put failed',
|
|
66
|
+
docId: doc._id,
|
|
67
|
+
operation: 'put',
|
|
68
|
+
statusCode: resp.statusCode
|
|
69
|
+
})
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
logger.info(`Successfully saved document: ${doc._id}`)
|
package/impl/query.mts
CHANGED
|
@@ -1,15 +1,57 @@
|
|
|
1
|
-
import needle, { type BodyData, type NeedleHttpVerbs } from 'needle'
|
|
2
1
|
import { createLogger } from './utils/logger.mts'
|
|
3
2
|
|
|
4
3
|
import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
|
|
5
4
|
import { z, ZodAny, ZodNever } from 'zod'
|
|
6
5
|
import { queryString } from './utils/queryString.mts'
|
|
7
|
-
import {
|
|
8
|
-
import { RetryableError } from './utils/errors.mts'
|
|
6
|
+
import { RetryableError, createResponseError } from './utils/errors.mts'
|
|
9
7
|
import { ViewOptions, type ViewString } from '../schema/couch/couch.input.schema.ts'
|
|
10
8
|
import type { CouchDoc, ViewQueryResponseValidated } from '../schema/couch/couch.output.schema.ts'
|
|
11
9
|
import type { StandardSchemaV1 } from '../types/standard-schema.ts'
|
|
12
10
|
import { parseRows, type OnInvalidDocAction } from './utils/parseRows.mts'
|
|
11
|
+
import { fetchCouchJson } from './utils/fetch.mts'
|
|
12
|
+
import { isSuccessStatusCode } from './utils/response.mts'
|
|
13
|
+
import { createCouchPathUrl } from './utils/url.mts'
|
|
14
|
+
|
|
15
|
+
type QueryBody = {
|
|
16
|
+
error?: string
|
|
17
|
+
reason?: string
|
|
18
|
+
rows?: unknown[]
|
|
19
|
+
} & Record<string, unknown>
|
|
20
|
+
|
|
21
|
+
const ValidSchema = z.custom(
|
|
22
|
+
value => {
|
|
23
|
+
return value !== null && typeof value === 'object' && '~standard' in value
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
message: 'schema must be a valid StandardSchemaV1 schema'
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const QueryValidationSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
docSchema: ValidSchema.optional(),
|
|
33
|
+
keySchema: ValidSchema.optional(),
|
|
34
|
+
onInvalidDoc: z.enum(['skip', 'throw']).optional(),
|
|
35
|
+
valueSchema: ValidSchema.optional()
|
|
36
|
+
})
|
|
37
|
+
.optional()
|
|
38
|
+
|
|
39
|
+
const QueryOptionsSchema = ViewOptions.extend({
|
|
40
|
+
validate: QueryValidationSchema
|
|
41
|
+
}).strict()
|
|
42
|
+
|
|
43
|
+
type QueryRequestOptions<
|
|
44
|
+
DocSchema extends StandardSchemaV1,
|
|
45
|
+
KeySchema extends StandardSchemaV1,
|
|
46
|
+
ValueSchema extends StandardSchemaV1
|
|
47
|
+
> = ViewOptions & {
|
|
48
|
+
validate?: {
|
|
49
|
+
onInvalidDoc?: OnInvalidDocAction
|
|
50
|
+
docSchema?: DocSchema
|
|
51
|
+
keySchema?: KeySchema
|
|
52
|
+
valueSchema?: ValueSchema
|
|
53
|
+
}
|
|
54
|
+
}
|
|
13
55
|
|
|
14
56
|
export async function query<
|
|
15
57
|
DocSchema extends StandardSchemaV1 = typeof CouchDoc,
|
|
@@ -18,14 +60,8 @@ export async function query<
|
|
|
18
60
|
>(
|
|
19
61
|
config: CouchConfigInput,
|
|
20
62
|
view: ViewString,
|
|
21
|
-
options:
|
|
63
|
+
options: QueryRequestOptions<DocSchema, KeySchema, ValueSchema> & {
|
|
22
64
|
include_docs: true
|
|
23
|
-
validate?: {
|
|
24
|
-
onInvalidDoc?: OnInvalidDocAction
|
|
25
|
-
docSchema?: DocSchema
|
|
26
|
-
keySchema?: KeySchema
|
|
27
|
-
valueSchema?: ValueSchema
|
|
28
|
-
}
|
|
29
65
|
}
|
|
30
66
|
): Promise<ViewQueryResponseValidated<DocSchema, KeySchema, ValueSchema>>
|
|
31
67
|
|
|
@@ -36,14 +72,8 @@ export async function query<
|
|
|
36
72
|
>(
|
|
37
73
|
config: CouchConfigInput,
|
|
38
74
|
view: ViewString,
|
|
39
|
-
options:
|
|
75
|
+
options: QueryRequestOptions<DocSchema, KeySchema, ValueSchema> & {
|
|
40
76
|
include_docs?: false | undefined
|
|
41
|
-
validate?: {
|
|
42
|
-
onInvalidDoc?: OnInvalidDocAction
|
|
43
|
-
docSchema?: DocSchema
|
|
44
|
-
keySchema?: KeySchema
|
|
45
|
-
valueSchema?: ValueSchema
|
|
46
|
-
}
|
|
47
77
|
}
|
|
48
78
|
): Promise<ViewQueryResponseValidated<ZodNever, KeySchema, ValueSchema>>
|
|
49
79
|
|
|
@@ -72,8 +102,8 @@ export async function query(
|
|
|
72
102
|
* @returns The parsed view response with rows validated against the supplied schemas.
|
|
73
103
|
*
|
|
74
104
|
* @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
|
|
75
|
-
* @throws {
|
|
76
|
-
* @throws {
|
|
105
|
+
* @throws {ValidationError} When row, key, value, or included document validation fails.
|
|
106
|
+
* @throws {OperationError} When CouchDB returns a non-retryable response or malformed row payload.
|
|
77
107
|
*/
|
|
78
108
|
export async function query<
|
|
79
109
|
DocSchema extends StandardSchemaV1,
|
|
@@ -82,19 +112,13 @@ export async function query<
|
|
|
82
112
|
>(
|
|
83
113
|
_config: CouchConfigInput,
|
|
84
114
|
view: ViewString,
|
|
85
|
-
options:
|
|
86
|
-
validate?: {
|
|
87
|
-
onInvalidDoc?: OnInvalidDocAction
|
|
88
|
-
docSchema?: DocSchema
|
|
89
|
-
keySchema?: KeySchema
|
|
90
|
-
valueSchema?: ValueSchema
|
|
91
|
-
}
|
|
92
|
-
} = {}
|
|
115
|
+
options: QueryRequestOptions<DocSchema, KeySchema, ValueSchema> = {}
|
|
93
116
|
): Promise<ViewQueryResponseValidated<DocSchema, KeySchema, ValueSchema>> {
|
|
94
117
|
const configParseResult = CouchConfig.safeParse(_config)
|
|
118
|
+
const parsedOptions = QueryOptionsSchema.parse(options || {})
|
|
95
119
|
const logger = createLogger(_config)
|
|
96
120
|
logger.info(`Starting view query: ${view}`)
|
|
97
|
-
logger.debug('Query options:',
|
|
121
|
+
logger.debug('Query options:', parsedOptions)
|
|
98
122
|
if (!configParseResult.success) {
|
|
99
123
|
logger.error(`Invalid configuration provided: ${z.prettifyError(configParseResult.error)}`)
|
|
100
124
|
throw configParseResult.error
|
|
@@ -102,86 +126,94 @@ export async function query<
|
|
|
102
126
|
|
|
103
127
|
const config = configParseResult.data
|
|
104
128
|
|
|
105
|
-
let qs = queryString(
|
|
106
|
-
let method:
|
|
107
|
-
let payload:
|
|
108
|
-
|
|
109
|
-
const opts = {
|
|
110
|
-
json: true,
|
|
111
|
-
headers: {
|
|
112
|
-
'Content-Type': 'application/json'
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const mergedOpts = mergeNeedleOpts(config, opts)
|
|
129
|
+
let qs = queryString(parsedOptions)
|
|
130
|
+
let method: 'GET' | 'POST' = 'GET'
|
|
131
|
+
let payload: Record<string, unknown> | null = null
|
|
117
132
|
|
|
118
133
|
// If keys are supplied, issue a POST to circumvent GET query string limits
|
|
119
134
|
// see http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options
|
|
120
|
-
if (typeof
|
|
135
|
+
if (typeof parsedOptions.keys !== 'undefined') {
|
|
121
136
|
const MAX_URL_LENGTH = 2000
|
|
122
137
|
// according to http://stackoverflow.com/a/417184/680742,
|
|
123
138
|
// the de facto URL length limit is 2000 characters
|
|
124
139
|
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
qs = queryString(_options)
|
|
140
|
+
const { keys, validate, ...queryableOptions } = parsedOptions
|
|
141
|
+
qs = queryString(queryableOptions)
|
|
128
142
|
|
|
129
|
-
const keysAsString = `keys=${JSON.stringify(
|
|
143
|
+
const keysAsString = `keys=${JSON.stringify(keys)}`
|
|
130
144
|
|
|
131
145
|
if (keysAsString.length + qs.length + 1 <= MAX_URL_LENGTH) {
|
|
132
146
|
// If the keys are short enough, do a GET. we do this to work around
|
|
133
147
|
// Safari not understanding 304s on POSTs (see pouchdb/pouchdb#1239)
|
|
134
|
-
method = '
|
|
148
|
+
method = 'GET'
|
|
135
149
|
if (qs.length > 0) qs += '&'
|
|
136
150
|
else qs = ''
|
|
137
151
|
qs += keysAsString
|
|
138
152
|
} else {
|
|
139
|
-
method = '
|
|
140
|
-
payload = { keys:
|
|
153
|
+
method = 'POST'
|
|
154
|
+
payload = { keys: parsedOptions.keys }
|
|
141
155
|
}
|
|
142
156
|
}
|
|
143
157
|
|
|
144
158
|
logger.debug('Generated query string:', qs)
|
|
145
|
-
const url =
|
|
159
|
+
const url = createCouchPathUrl(view, config.couch)
|
|
160
|
+
if (qs) url.search = qs
|
|
146
161
|
let results
|
|
147
162
|
|
|
148
163
|
try {
|
|
149
164
|
logger.debug(`Sending ${method} request to: ${url}`)
|
|
150
|
-
results =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
165
|
+
results = await fetchCouchJson<QueryBody>({
|
|
166
|
+
auth: config.auth,
|
|
167
|
+
method,
|
|
168
|
+
operation: 'query',
|
|
169
|
+
request: config.request,
|
|
170
|
+
url,
|
|
171
|
+
body: method === 'POST' ? payload : undefined
|
|
172
|
+
})
|
|
154
173
|
} catch (err) {
|
|
155
174
|
logger.error('Network error during query:', err)
|
|
156
|
-
RetryableError.handleNetworkError(err)
|
|
175
|
+
RetryableError.handleNetworkError(err, 'query')
|
|
157
176
|
}
|
|
158
177
|
|
|
159
178
|
if (!results) {
|
|
160
179
|
logger.error('No response received from query request')
|
|
161
|
-
throw new RetryableError('
|
|
180
|
+
throw new RetryableError('Query failed', 503, { operation: 'query' })
|
|
162
181
|
}
|
|
163
182
|
|
|
164
183
|
const body = results.body
|
|
165
184
|
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
185
|
+
if (!isSuccessStatusCode('viewQuery', results.statusCode) || body.error) {
|
|
186
|
+
if (body.error) {
|
|
187
|
+
logger.error(`Query error: ${JSON.stringify(body)}`)
|
|
188
|
+
} else {
|
|
189
|
+
logger.error(`Unexpected status code: ${results.statusCode}`)
|
|
190
|
+
}
|
|
170
191
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
192
|
+
throw createResponseError({
|
|
193
|
+
body,
|
|
194
|
+
defaultMessage: 'Query failed',
|
|
195
|
+
operation: 'query',
|
|
196
|
+
statusCode: results.statusCode
|
|
197
|
+
})
|
|
174
198
|
}
|
|
175
199
|
|
|
176
200
|
// If validation schemas are provided, validate each row accordingly
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
const rows: ViewQueryResponseValidated<DocSchema, KeySchema, ValueSchema>['rows'] =
|
|
202
|
+
options.validate && body.rows
|
|
203
|
+
? await parseRows<DocSchema, KeySchema, ValueSchema>(body.rows, {
|
|
204
|
+
...options.validate,
|
|
205
|
+
defaultMessage: 'Query failed',
|
|
206
|
+
operation: 'query'
|
|
207
|
+
})
|
|
208
|
+
: ((body.rows ?? []) as ViewQueryResponseValidated<DocSchema, KeySchema, ValueSchema>['rows'])
|
|
180
209
|
|
|
181
210
|
logger.info(`Successfully executed view query: ${view}`)
|
|
182
|
-
logger.debug('Query response:', body)
|
|
211
|
+
logger.debug('Query response:', { ...body, rows })
|
|
183
212
|
|
|
184
|
-
return
|
|
213
|
+
return {
|
|
214
|
+
...body,
|
|
215
|
+
rows
|
|
216
|
+
}
|
|
185
217
|
}
|
|
186
218
|
|
|
187
219
|
export type QueryBound = {
|
|
@@ -191,14 +223,8 @@ export type QueryBound = {
|
|
|
191
223
|
ValueSchema extends StandardSchemaV1 = ZodAny
|
|
192
224
|
>(
|
|
193
225
|
view: ViewString,
|
|
194
|
-
options:
|
|
226
|
+
options: QueryRequestOptions<DocSchema, KeySchema, ValueSchema> & {
|
|
195
227
|
include_docs: true
|
|
196
|
-
validate?: {
|
|
197
|
-
onInvalidDoc?: OnInvalidDocAction
|
|
198
|
-
docSchema?: DocSchema
|
|
199
|
-
keySchema?: KeySchema
|
|
200
|
-
valueSchema?: ValueSchema
|
|
201
|
-
}
|
|
202
228
|
}
|
|
203
229
|
): Promise<ViewQueryResponseValidated<DocSchema, KeySchema, ValueSchema>>
|
|
204
230
|
<
|
|
@@ -207,14 +233,8 @@ export type QueryBound = {
|
|
|
207
233
|
ValueSchema extends StandardSchemaV1 = ZodAny
|
|
208
234
|
>(
|
|
209
235
|
view: ViewString,
|
|
210
|
-
options:
|
|
236
|
+
options: QueryRequestOptions<DocSchema, KeySchema, ValueSchema> & {
|
|
211
237
|
include_docs?: false | undefined
|
|
212
|
-
validate?: {
|
|
213
|
-
onInvalidDoc?: OnInvalidDocAction
|
|
214
|
-
docSchema?: DocSchema
|
|
215
|
-
keySchema?: KeySchema
|
|
216
|
-
valueSchema?: ValueSchema
|
|
217
|
-
}
|
|
218
238
|
}
|
|
219
239
|
): Promise<ViewQueryResponseValidated<ZodNever, KeySchema, ValueSchema>>
|
|
220
240
|
(
|
package/impl/remove.mts
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
import needle from 'needle'
|
|
2
1
|
import { createLogger } from './utils/logger.mts'
|
|
3
|
-
import {
|
|
4
|
-
import { RetryableError } from './utils/errors.mts'
|
|
2
|
+
import { NotFoundError, RetryableError, createResponseError } from './utils/errors.mts'
|
|
5
3
|
import { CouchPutResponse } from '../schema/couch/couch.output.schema.ts'
|
|
6
4
|
import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
|
|
5
|
+
import { fetchCouchJson } from './utils/fetch.mts'
|
|
6
|
+
import { isRecord, isSuccessStatusCode } from './utils/response.mts'
|
|
7
|
+
import { createCouchDocUrl } from './utils/url.mts'
|
|
8
|
+
|
|
9
|
+
type CouchMutationBody = {
|
|
10
|
+
error?: string
|
|
11
|
+
ok?: boolean
|
|
12
|
+
reason?: string
|
|
13
|
+
statusCode?: number
|
|
14
|
+
} & Record<string, unknown>
|
|
7
15
|
|
|
8
16
|
export const remove = async (configInput: CouchConfigInput, id: string, rev: string) => {
|
|
9
17
|
const config = CouchConfig.parse(configInput)
|
|
10
18
|
const logger = createLogger(config)
|
|
11
|
-
const url =
|
|
12
|
-
|
|
13
|
-
json: true,
|
|
14
|
-
headers: {
|
|
15
|
-
'Content-Type': 'application/json'
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
const mergedOpts = mergeNeedleOpts(config, opts)
|
|
19
|
+
const url = createCouchDocUrl(id, config.couch)
|
|
20
|
+
url.searchParams.set('rev', rev)
|
|
19
21
|
|
|
20
22
|
logger.info(`Deleting document with id: ${id}`)
|
|
21
23
|
let resp
|
|
22
24
|
try {
|
|
23
|
-
resp = await
|
|
25
|
+
resp = await fetchCouchJson({
|
|
26
|
+
auth: config.auth,
|
|
27
|
+
method: 'DELETE',
|
|
28
|
+
operation: 'remove',
|
|
29
|
+
request: config.request,
|
|
30
|
+
url
|
|
31
|
+
})
|
|
24
32
|
} catch (err) {
|
|
25
33
|
logger.error('Error during delete operation:', err)
|
|
26
|
-
RetryableError.handleNetworkError(err)
|
|
34
|
+
RetryableError.handleNetworkError(err, 'remove')
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
if (!resp) {
|
|
30
38
|
logger.error('No response received from delete request')
|
|
31
|
-
throw new RetryableError('
|
|
39
|
+
throw new RetryableError('Remove failed', 503, { operation: 'remove' })
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
result = JSON.parse(resp.body)
|
|
38
|
-
} catch {
|
|
39
|
-
result = {}
|
|
40
|
-
}
|
|
41
|
-
} else {
|
|
42
|
-
result = resp.body || {}
|
|
42
|
+
const result: CouchMutationBody = {
|
|
43
|
+
...(isRecord(resp.body) ? resp.body : {})
|
|
43
44
|
}
|
|
44
45
|
result.statusCode = resp.statusCode
|
|
45
46
|
|
|
46
47
|
if (resp.statusCode === 404) {
|
|
47
48
|
logger.warn(`Document not found for deletion: ${id}`)
|
|
48
|
-
|
|
49
|
-
result.error = 'not_found'
|
|
50
|
-
return CouchPutResponse.parse(result)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
54
|
-
logger.warn(`Retryable status code received: ${resp.statusCode}`)
|
|
55
|
-
throw new RetryableError(result.reason || 'retryable error', resp.statusCode)
|
|
49
|
+
throw new NotFoundError(id, { operation: 'remove', statusCode: resp.statusCode })
|
|
56
50
|
}
|
|
57
51
|
|
|
58
|
-
if (resp.statusCode
|
|
52
|
+
if (!isSuccessStatusCode('documentDelete', resp.statusCode) || !result.ok) {
|
|
59
53
|
logger.error(`Unexpected status code: ${resp.statusCode}`)
|
|
60
|
-
throw
|
|
54
|
+
throw createResponseError({
|
|
55
|
+
body: resp.body,
|
|
56
|
+
defaultMessage: 'Remove failed',
|
|
57
|
+
docId: id,
|
|
58
|
+
operation: 'remove',
|
|
59
|
+
statusCode: resp.statusCode
|
|
60
|
+
})
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
logger.info(`Successfully deleted document: ${id}`)
|