hide-a-bed 6.0.0-beta.0 → 6.0.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 (107) hide show
  1. package/migration_guides/v6.md +70 -0
  2. package/package.json +4 -1
  3. package/docs/.nojekyll +0 -1
  4. package/docs/assets/hierarchy.js +0 -1
  5. package/docs/assets/highlight.css +0 -113
  6. package/docs/assets/icons.js +0 -18
  7. package/docs/assets/icons.svg +0 -1
  8. package/docs/assets/main.js +0 -60
  9. package/docs/assets/navigation.js +0 -1
  10. package/docs/assets/search.js +0 -1
  11. package/docs/assets/style.css +0 -1633
  12. package/docs/classes/QueryBuilder.html +0 -42
  13. package/docs/functions/bindConfig.html +0 -4
  14. package/docs/functions/bulkGet.html +0 -14
  15. package/docs/functions/bulkGetDictionary.html +0 -10
  16. package/docs/functions/bulkRemove.html +0 -12
  17. package/docs/functions/bulkRemoveMap.html +0 -11
  18. package/docs/functions/bulkSave.html +0 -10
  19. package/docs/functions/bulkSaveTransaction.html +0 -23
  20. package/docs/functions/createLock.html +0 -7
  21. package/docs/functions/createQuery.html +0 -1
  22. package/docs/functions/get.html +0 -1
  23. package/docs/functions/getAtRev.html +0 -1
  24. package/docs/functions/getDBInfo.html +0 -10
  25. package/docs/functions/patch.html +0 -8
  26. package/docs/functions/patchDangerously.html +0 -9
  27. package/docs/functions/put.html +0 -1
  28. package/docs/functions/query.html +0 -15
  29. package/docs/functions/queryStream.html +0 -6
  30. package/docs/functions/remove.html +0 -1
  31. package/docs/functions/removeLock.html +0 -6
  32. package/docs/functions/watchDocs.html +0 -9
  33. package/docs/functions/withRetry.html +0 -6
  34. package/docs/hierarchy.html +0 -1
  35. package/docs/index.html +0 -483
  36. package/docs/interfaces/NetworkError.html +0 -6
  37. package/docs/interfaces/NotFoundError.html +0 -10
  38. package/docs/interfaces/RetryOptions.html +0 -10
  39. package/docs/interfaces/RetryableError.html +0 -10
  40. package/docs/interfaces/StandardSchemaV1.FailureResult.html +0 -4
  41. package/docs/interfaces/StandardSchemaV1.Issue.html +0 -6
  42. package/docs/interfaces/StandardSchemaV1.Options.html +0 -3
  43. package/docs/interfaces/StandardSchemaV1.PathSegment.html +0 -4
  44. package/docs/interfaces/StandardSchemaV1.Props.html +0 -10
  45. package/docs/interfaces/StandardSchemaV1.SuccessResult.html +0 -6
  46. package/docs/interfaces/StandardSchemaV1.Types.html +0 -6
  47. package/docs/interfaces/StandardSchemaV1.html +0 -4
  48. package/docs/modules/StandardSchemaV1.html +0 -1
  49. package/docs/modules.html +0 -1
  50. package/docs/types/BoundInstance.html +0 -1
  51. package/docs/types/BulkGetBound.html +0 -2
  52. package/docs/types/BulkGetDictionaryBound.html +0 -1
  53. package/docs/types/BulkGetDictionaryOptions.html +0 -2
  54. package/docs/types/BulkGetDictionaryResult.html +0 -3
  55. package/docs/types/BulkGetOptions.html +0 -3
  56. package/docs/types/BulkGetResponse.html +0 -1
  57. package/docs/types/CouchConfig-1.html +0 -1
  58. package/docs/types/CouchConfig.html +0 -1
  59. package/docs/types/CouchConfigInput.html +0 -1
  60. package/docs/types/CouchDoc-1.html +0 -1
  61. package/docs/types/CouchDoc.html +0 -2
  62. package/docs/types/CouchDocInput.html +0 -2
  63. package/docs/types/GetAtRevBound.html +0 -1
  64. package/docs/types/GetBound.html +0 -1
  65. package/docs/types/GetOptions.html +0 -2
  66. package/docs/types/LockDoc-1.html +0 -1
  67. package/docs/types/LockDoc.html +0 -1
  68. package/docs/types/LockOptions-1.html +0 -1
  69. package/docs/types/LockOptions.html +0 -1
  70. package/docs/types/LockOptionsInput.html +0 -1
  71. package/docs/types/OnInvalidDocAction.html +0 -1
  72. package/docs/types/OnRow.html +0 -1
  73. package/docs/types/QueryBound.html +0 -1
  74. package/docs/types/SimpleViewOptions-1.html +0 -1
  75. package/docs/types/SimpleViewOptions.html +0 -1
  76. package/docs/types/StandardSchemaV1.InferInput.html +0 -2
  77. package/docs/types/StandardSchemaV1.InferOutput.html +0 -2
  78. package/docs/types/StandardSchemaV1.Result.html +0 -2
  79. package/docs/types/ViewQueryResponse-1.html +0 -1
  80. package/docs/types/ViewQueryResponse.html +0 -2
  81. package/docs/types/ViewQueryResponseValidated.html +0 -2
  82. package/docs/types/ViewRow-1.html +0 -1
  83. package/docs/types/ViewRow.html +0 -2
  84. package/docs/types/ViewRowValidated.html +0 -7
  85. package/docs/types/ViewString.html +0 -1
  86. package/docs/types/WatchOptionsInput.html +0 -1
  87. package/docs/types/WatchOptionsSchema-1.html +0 -1
  88. package/docs/types/WatchOptionsSchema.html +0 -1
  89. package/impl/bulkGet.test.mts +0 -159
  90. package/impl/bulkRemove.test.mts +0 -102
  91. package/impl/bulkSave.test.mts +0 -319
  92. package/impl/get.test.mts +0 -114
  93. package/impl/getDBInfo.test.mts +0 -62
  94. package/impl/patch.test.mts +0 -142
  95. package/impl/put.test.mts +0 -114
  96. package/impl/query.test.mts +0 -280
  97. package/impl/remove.test.mts +0 -82
  98. package/impl/retry.test.mts +0 -77
  99. package/impl/stream.test.mts +0 -205
  100. package/impl/sugar/lock.test.mts +0 -113
  101. package/impl/sugar/watch.test.mts +0 -155
  102. package/impl/utils/errors.test.mts +0 -58
  103. package/impl/utils/logger.test.mts +0 -129
  104. package/impl/utils/parseRows.test.mts +0 -183
  105. package/impl/utils/queryBuilder.test.mts +0 -83
  106. package/impl/utils/queryString.test.mts +0 -53
  107. package/index.test.mts +0 -415
@@ -1,319 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import test, { suite } from 'node:test'
3
- import needle from 'needle'
4
- import type { CouchConfigInput } from '../schema/config.mts'
5
- import { bulkSave, bulkSaveTransaction } from './bulkSave.mts'
6
- import { RetryableError } from './utils/errors.mts'
7
- import {
8
- TransactionRollbackError,
9
- TransactionVersionConflictError
10
- } from './utils/transactionErrors.mts'
11
- import { TEST_DB_URL } from '../test/setup-db.mts'
12
-
13
- const baseConfig: CouchConfigInput = {
14
- couch: TEST_DB_URL,
15
- useConsoleLogger: true
16
- }
17
-
18
- const transactionBaseConfig: CouchConfigInput = {
19
- couch: TEST_DB_URL,
20
- bindWithRetry: false,
21
- useConsoleLogger: true
22
- }
23
-
24
- type EventRecord = { event: string; payload: unknown }
25
-
26
- function createTestEmitter() {
27
- const events: EventRecord[] = []
28
- const handlers = new Map<string, Array<(payload: unknown) => Promise<void> | void>>()
29
-
30
- return {
31
- events,
32
- on(event: string, handler: (payload: unknown) => Promise<void> | void) {
33
- const list = handlers.get(event) ?? []
34
- list.push(handler)
35
- handlers.set(event, list)
36
- },
37
- async emit(event: string, payload: unknown) {
38
- events.push({ event, payload })
39
- const list = handlers.get(event)
40
- if (!list) return
41
- for (const handler of list) {
42
- await handler(payload)
43
- }
44
- }
45
- }
46
- }
47
-
48
- async function saveDoc(dbUrl: string, id: string, body: Record<string, unknown>) {
49
- const response = await needle('put', `${dbUrl}/${id}`, { _id: id, ...body }, { json: true })
50
-
51
- if (response.statusCode !== 201 && response.statusCode !== 200) {
52
- throw new Error(`Failed to save document ${id}: ${response.statusCode}`)
53
- }
54
-
55
- return response.body as { rev: string }
56
- }
57
-
58
- async function getDocFrom(dbUrl: string, id: string) {
59
- return needle('get', `${dbUrl}/${id}`, null, { json: true })
60
- }
61
-
62
- async function getDoc(id: string) {
63
- return getDocFrom(TEST_DB_URL, id)
64
- }
65
-
66
- suite('bulkSave', () => {
67
- test('rejects invalid config arguments', async () => {
68
- await assert.rejects(async () => {
69
- // @ts-expect-error intentionally passing unsupported option
70
- await bulkSave({ couch: TEST_DB_URL, unsupported: true }, [
71
- { _id: 'bad-config-doc', count: 1 }
72
- ])
73
- })
74
- })
75
-
76
- test('throws error if called with no docs', async () => {
77
- await assert.rejects(async () => {
78
- // @ts-expect-error testing no docs
79
- await bulkSave(baseConfig, null)
80
- })
81
- await assert.rejects(async () => {
82
- await bulkSave(baseConfig, [])
83
- })
84
- })
85
-
86
- test('propagates retryable network failures', async () => {
87
- const offlineConfig: CouchConfigInput = {
88
- couch: 'http://localhost:6554/offline-bulk-save'
89
- }
90
-
91
- await assert.rejects(
92
- () => bulkSave(offlineConfig, [{ _id: 'offline-doc', count: 1 }]),
93
- (err: unknown) => err instanceof RetryableError && err.statusCode === 503
94
- )
95
- })
96
-
97
- test('integration with pouchdb-server', async t => {
98
- let docTwoInitialRev: string | undefined
99
- const docs = [
100
- { _id: `bulk-save-doc-1-${Date.now()}`, type: 'integration', count: 1 },
101
- { _id: `bulk-save-doc-2-${Date.now()}`, type: 'integration', count: 2 }
102
- ]
103
-
104
- await t.test('creates documents via _bulk_docs', async () => {
105
- const results = await bulkSave(baseConfig, docs)
106
- assert.strictEqual(results.length, 2)
107
- const [first, second] = results
108
- assert.ok(first)
109
- assert.strictEqual(first.id, docs[0]._id)
110
- assert.strictEqual(first.ok, true)
111
- assert.ok(second)
112
- assert.strictEqual(second.id, docs[1]._id)
113
- assert.strictEqual(second.ok, true)
114
-
115
- docTwoInitialRev = second.rev ?? undefined
116
- assert.ok(typeof docTwoInitialRev === 'string')
117
-
118
- const { statusCode, body } = await getDoc(docs[0]._id)
119
- assert.strictEqual(statusCode, 200)
120
- assert.strictEqual(body?.type, 'integration')
121
- assert.strictEqual(body?.count, 1)
122
- })
123
-
124
- await t.test('updates documents when revision supplied', async () => {
125
- const current = await getDoc(docs[1]._id)
126
- assert.strictEqual(current.statusCode, 200)
127
- const updateResults = await bulkSave(baseConfig, [
128
- {
129
- _id: docs[1]._id,
130
- _rev: current.body?._rev,
131
- type: 'integration',
132
- count: 3
133
- }
134
- ])
135
-
136
- assert.strictEqual(updateResults.length, 1)
137
- const [updated] = updateResults
138
- assert.ok(updated)
139
- assert.strictEqual(updated.ok, true)
140
- assert.ok(updated.rev)
141
-
142
- const { body } = await getDoc(docs[1]._id)
143
- assert.strictEqual(body?.count, 3)
144
- })
145
-
146
- await t.test('reports conflicts when revision is stale', async () => {
147
- if (!docTwoInitialRev) throw new Error('Expected initial revision to be captured')
148
-
149
- const conflictResults = await bulkSave(baseConfig, [
150
- {
151
- _id: docs[1]._id,
152
- _rev: docTwoInitialRev,
153
- type: 'integration',
154
- count: 99
155
- }
156
- ])
157
-
158
- assert.strictEqual(conflictResults.length, 1)
159
- const [conflict] = conflictResults
160
- assert.ok(conflict)
161
- assert.strictEqual(conflict.id, docs[1]._id)
162
- assert.strictEqual(conflict.error, 'conflict')
163
- assert.ok(conflict.reason)
164
- })
165
- })
166
- })
167
-
168
- suite('bulkSaveTransaction', () => {
169
- test('integration with pouchdb-server', async t => {
170
- await t.test('completes transaction for new and existing docs', async () => {
171
- const emitter = createTestEmitter()
172
- const config: CouchConfigInput = {
173
- ...transactionBaseConfig,
174
- '~emitter': emitter
175
- }
176
-
177
- const existingId = `txn-existing-success-${Date.now()}`
178
- const newId = `txn-new-success-${Date.now()}`
179
- const transactionId = `bulk-transaction-success-${Date.now()}`
180
-
181
- const existing = await saveDoc(TEST_DB_URL, existingId, {
182
- type: 'transaction',
183
- count: 1
184
- })
185
-
186
- const docs = [
187
- {
188
- _id: existingId,
189
- _rev: existing.rev,
190
- type: 'transaction',
191
- count: 2
192
- },
193
- { _id: newId, type: 'transaction', count: 1 }
194
- ]
195
-
196
- const results = await bulkSaveTransaction(config, transactionId, docs)
197
- assert.strictEqual(results.length, 2)
198
- assert.ok(results[0]?.ok)
199
- assert.strictEqual(results[0]?.id, existingId)
200
- assert.ok(results[1]?.ok)
201
- assert.strictEqual(results[1]?.id, newId)
202
-
203
- const updatedExisting = await getDocFrom(TEST_DB_URL, existingId)
204
- assert.strictEqual(updatedExisting.statusCode, 200)
205
- assert.strictEqual(updatedExisting.body?.count, 2)
206
-
207
- const createdDoc = await getDocFrom(TEST_DB_URL, newId)
208
- assert.strictEqual(createdDoc.statusCode, 200)
209
- assert.strictEqual(createdDoc.body?.count, 1)
210
-
211
- const transactionDoc = await getDocFrom(TEST_DB_URL, `txn:${transactionId}`)
212
- assert.strictEqual(transactionDoc.statusCode, 200)
213
- assert.strictEqual(transactionDoc.body?.status, 'completed')
214
-
215
- assert.ok(emitter.events.some(({ event }) => event === 'transaction-created'))
216
- assert.ok(emitter.events.some(({ event }) => event === 'transaction-completed'))
217
- })
218
-
219
- await t.test('throws TransactionVersionConflictError when revisions mismatch', async () => {
220
- const emitter = createTestEmitter()
221
- const config: CouchConfigInput = {
222
- ...transactionBaseConfig,
223
- '~emitter': emitter
224
- }
225
-
226
- const docId = `txn-conflict-doc-${Date.now()}`
227
- const transactionId = `bulk-transaction-conflict-${Date.now()}`
228
-
229
- const first = await saveDoc(TEST_DB_URL, docId, {
230
- type: 'conflict',
231
- count: 1
232
- })
233
- await saveDoc(TEST_DB_URL, docId, {
234
- _rev: first.rev,
235
- type: 'conflict',
236
- count: 2
237
- })
238
- await assert.rejects(
239
- () =>
240
- bulkSaveTransaction(config, transactionId, [
241
- {
242
- _id: docId,
243
- _rev: first.rev,
244
- type: 'conflict',
245
- count: 3
246
- }
247
- ]),
248
- (err: unknown) =>
249
- err instanceof TransactionVersionConflictError && err.conflictingIds.includes(docId)
250
- )
251
-
252
- assert.ok(emitter.events.some(({ event }) => event === 'transaction-created'))
253
- assert.ok(emitter.events.some(({ event }) => event === 'transaction-revs-fetched'))
254
- })
255
-
256
- await t.test('rolls back changes when bulk save fails', async () => {
257
- const emitter = createTestEmitter()
258
- const config: CouchConfigInput = {
259
- ...transactionBaseConfig,
260
- '~emitter': emitter
261
- }
262
-
263
- const successId = `txn-rollback-existing-${Date.now()}`
264
- const conflictId = `txn-rollback-conflict-${Date.now()}`
265
- const transactionId = `bulk-transaction-rollback-${Date.now()}`
266
-
267
- const existing = await saveDoc(TEST_DB_URL, successId, {
268
- type: 'rollback',
269
- count: 1
270
- })
271
- const conflicting = await saveDoc(TEST_DB_URL, conflictId, {
272
- type: 'rollback',
273
- count: 1
274
- })
275
-
276
- emitter.on('transaction-revs-checked', async () => {
277
- await needle(
278
- 'put',
279
- `${TEST_DB_URL}/${conflictId}`,
280
- {
281
- _id: conflictId,
282
- _rev: conflicting.rev,
283
- type: 'rollback',
284
- count: 99
285
- },
286
- { json: true }
287
- )
288
- })
289
-
290
- await assert.rejects(
291
- () =>
292
- bulkSaveTransaction(config, transactionId, [
293
- { _id: successId, _rev: existing.rev, type: 'rollback', count: 2 },
294
- {
295
- _id: conflictId,
296
- _rev: conflicting.rev,
297
- type: 'rollback',
298
- count: 2
299
- }
300
- ]),
301
- (err: unknown) => err instanceof TransactionRollbackError
302
- )
303
-
304
- const rolledBack = await getDocFrom(TEST_DB_URL, successId)
305
- assert.strictEqual(rolledBack.statusCode, 200)
306
- assert.strictEqual(rolledBack.body?.count, 1)
307
-
308
- const conflicted = await getDocFrom(TEST_DB_URL, conflictId)
309
- assert.strictEqual(conflicted.statusCode, 200)
310
- assert.strictEqual(conflicted.body?.count, 99)
311
-
312
- const transactionDoc = await getDocFrom(TEST_DB_URL, `txn:${transactionId}`)
313
- assert.strictEqual(transactionDoc.statusCode, 200)
314
- assert.strictEqual(transactionDoc.body?.status, 'rolled_back')
315
-
316
- assert.ok(emitter.events.some(({ event }) => event === 'transaction-rolled-back'))
317
- })
318
- })
319
- })
package/impl/get.test.mts DELETED
@@ -1,114 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import test, { suite } from 'node:test'
3
- import needle from 'needle'
4
- import { z } from 'zod'
5
- import type { CouchConfigInput } from '../schema/config.mts'
6
- import { get, getAtRev } from './get.mts'
7
- import { NotFoundError, RetryableError } from './utils/errors.mts'
8
- import { TEST_DB_URL } from '../test/setup-db.mts'
9
-
10
- const baseConfig: CouchConfigInput = {
11
- couch: TEST_DB_URL
12
- }
13
-
14
- type DocBody = Record<string, unknown>
15
-
16
- async function saveDoc(id: string, body: DocBody) {
17
- const response = await needle(
18
- 'put',
19
- `${TEST_DB_URL}/${id}`,
20
- {
21
- _id: id,
22
- ...body
23
- },
24
- { json: true }
25
- )
26
-
27
- if (response.statusCode !== 201 && response.statusCode !== 200) {
28
- throw new Error(`Failed to save document ${id}: ${response.statusCode}`)
29
- }
30
-
31
- return response.body as { rev: string }
32
- }
33
-
34
- suite('get', () => {
35
- test('integration with pouchdb-server', async t => {
36
- const doc_valid_id = `doc-valid-${Date.now()}`
37
- const doc_invalid_id = `doc-invalid-${Date.now()}`
38
- const doc_rev_id = `doc-rev-${Date.now()}`
39
- await saveDoc(doc_valid_id, { kind: 'example', count: 7 })
40
- await saveDoc(doc_invalid_id, { kind: 'example', count: 'oops' })
41
- const firstRev = await saveDoc(doc_rev_id, { version: 1 })
42
- await saveDoc(doc_rev_id, { _rev: firstRev.rev, version: 2 })
43
-
44
- await t.test('returns documents and validates schema', async () => {
45
- const schema = z.looseObject({
46
- _id: z.string(),
47
- kind: z.literal('example'),
48
- count: z.number()
49
- })
50
-
51
- const doc = await get(baseConfig, doc_valid_id, {
52
- validate: { docSchema: schema }
53
- })
54
- assert.ok(doc)
55
- assert.strictEqual(doc?.kind, 'example')
56
- assert.strictEqual(doc?.count, 7)
57
-
58
- await assert.rejects(
59
- () => get(baseConfig, doc_invalid_id, { validate: { docSchema: schema } }),
60
- (err: unknown) => {
61
- return (
62
- Array.isArray(err) &&
63
- err[0].message === 'Invalid input: expected number, received string'
64
- )
65
- }
66
- )
67
- })
68
-
69
- await t.test('returns null when not found by default', async () => {
70
- const missing = await get(baseConfig, 'doc-missing')
71
- assert.strictEqual(missing, null)
72
- })
73
-
74
- await t.test('throws NotFoundError when configured', async () => {
75
- const strictConfig: CouchConfigInput = {
76
- ...baseConfig,
77
- throwOnGetNotFound: true
78
- }
79
-
80
- await assert.rejects(
81
- () => get(strictConfig, 'doc-missing'),
82
- (err: unknown) => err instanceof NotFoundError && err.docId === 'doc-missing'
83
- )
84
- })
85
-
86
- await t.test('getAtRev returns specific revision', async () => {
87
- const versionedSchema = z.looseObject({
88
- _id: z.string(),
89
- version: z.number()
90
- })
91
-
92
- const latest = await get(baseConfig, doc_rev_id, {
93
- validate: { docSchema: versionedSchema }
94
- })
95
- assert.strictEqual(latest?.version, 2)
96
-
97
- const early = await getAtRev(baseConfig, doc_rev_id, firstRev.rev, {
98
- validate: { docSchema: versionedSchema }
99
- })
100
- assert.strictEqual(early?.version, 1)
101
- })
102
-
103
- await t.test('propagates retryable network errors', async () => {
104
- const offlineConfig: CouchConfigInput = {
105
- couch: 'http://localhost:6553/offline-db'
106
- }
107
-
108
- await assert.rejects(
109
- () => get(offlineConfig, 'doc-valid'),
110
- (err: unknown) => err instanceof RetryableError && err.statusCode === 503
111
- )
112
- })
113
- })
114
- })
@@ -1,62 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import { createServer } from 'node:http'
3
- import test, { suite } from 'node:test'
4
- import type { CouchConfigInput } from '../schema/config.mts'
5
- import { getDBInfo } from './getDBInfo.mts'
6
- import { RetryableError } from './utils/errors.mts'
7
- import { TEST_DB_URL } from '../test/setup-db.mts'
8
-
9
- suite('getDBInfo', () => {
10
- test('it should throw if provided config is invalid', async () => {
11
- await assert.rejects(async () => {
12
- await getDBInfo({
13
- // @ts-expect-error testing invalid config
14
- notAnOption: true,
15
- // @ts-expect-error testing invalid config
16
- couch: DB_URL,
17
- useConsoleLogger: true
18
- })
19
- })
20
- })
21
- test('integration with pouchdb-server', async t => {
22
- await t.test('returns database metadata', async () => {
23
- const config: CouchConfigInput = { couch: TEST_DB_URL }
24
- const info = await getDBInfo(config)
25
- assert.strictEqual(info.db_name, 'hide-a-bed-test-db')
26
- assert.ok(typeof info.doc_count === 'number')
27
- })
28
- })
29
-
30
- test('throws RetryableError when server marks response retryable', async t => {
31
- const port = 8993
32
- const server = createServer((_req, res) => {
33
- res.statusCode = 503
34
- res.setHeader('Content-Type', 'application/json')
35
- res.end(JSON.stringify({ reason: 'maintenance' }))
36
- })
37
-
38
- await new Promise<void>(resolve => {
39
- server.listen(port, resolve)
40
- })
41
- t.after(() => {
42
- server.close()
43
- })
44
-
45
- await assert.rejects(
46
- () => getDBInfo({ couch: `http://localhost:${port}/retryable` }),
47
- (err: unknown) => {
48
- assert.ok(err instanceof RetryableError)
49
- assert.strictEqual(err.statusCode, 503)
50
- assert.strictEqual(err.message, 'maintenance')
51
- return true
52
- }
53
- )
54
- })
55
-
56
- test('converts network failures into RetryableError', async () => {
57
- await assert.rejects(
58
- () => getDBInfo({ couch: 'http://localhost:6555/offline-db' }),
59
- (err: unknown) => err instanceof RetryableError && err.statusCode === 503
60
- )
61
- })
62
- })
@@ -1,142 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import test, { suite } from 'node:test'
3
- import needle from 'needle'
4
- import type { CouchConfigInput } from '../schema/config.mts'
5
- import { get } from './get.mts'
6
- import { patch, patchDangerously } from './patch.mts'
7
- import { TEST_DB_URL } from '../test/setup-db.mts'
8
-
9
- const baseConfig: CouchConfigInput = {
10
- couch: TEST_DB_URL,
11
- initialDelay: 10,
12
- maxRetries: 2,
13
- backoffFactor: 1.2
14
- }
15
-
16
- type DocBody = Record<string, unknown>
17
-
18
- async function saveDoc(id: string, body: DocBody) {
19
- const response = await needle(
20
- 'put',
21
- `${TEST_DB_URL}/${id}`,
22
- {
23
- _id: id,
24
- ...body
25
- },
26
- { json: true }
27
- )
28
-
29
- if (response.statusCode !== 201 && response.statusCode !== 200) {
30
- throw new Error(`Failed to save document ${id}: ${response.statusCode}`)
31
- }
32
-
33
- return response.body as { rev: string }
34
- }
35
-
36
- suite('patch', () => {
37
- test('it will throw if provided config is invalid', async () => {
38
- await assert.rejects(async () => {
39
- // @ts-expect-error testing invalid config
40
- await patch({ notAnOption: true, couch: DB_URL, useConsoleLogger: true }, 'doc1', {
41
- foo: 'bar'
42
- })
43
- })
44
-
45
- await assert.rejects(async () => {
46
- await patchDangerously(
47
- // @ts-expect-error testing invalid config
48
- { anotherBadOption: 123, couch: DB_URL, useConsoleLogger: true },
49
- 'doc1',
50
- { foo: 'bar' }
51
- )
52
- })
53
- })
54
-
55
- test('patch should throw if document revision is not provided', async () => {
56
- await assert.rejects(
57
- async () => {
58
- // @ts-expect-error testing missing _rev
59
- await patch(baseConfig, 'doc-no-rev', { foo: 'bar' })
60
- },
61
- {
62
- message: /_rev is required for patch operations/
63
- }
64
- )
65
- })
66
-
67
- test('integration with pouchdb-server', async t => {
68
- const patch_doc_id = `patch-doc-${Date.now()}`
69
- const initial = await saveDoc(patch_doc_id, { message: 'original' })
70
-
71
- await t.test('patch updates document when revision matches', async () => {
72
- const result = await patch(baseConfig, patch_doc_id, {
73
- _rev: initial.rev,
74
- message: 'patched',
75
- updated: true
76
- })
77
- assert.ok(result.ok)
78
- assert.ok(result.rev)
79
-
80
- const doc = await get(baseConfig, patch_doc_id)
81
- assert.strictEqual(doc?.message, 'patched')
82
- assert.strictEqual(doc?.updated, true)
83
- })
84
-
85
- await t.test('patch returns conflict on stale revision', async () => {
86
- const current = await get(baseConfig, patch_doc_id)
87
- const staleRev = initial.rev
88
-
89
- const conflict = await patch(baseConfig, patch_doc_id, {
90
- _rev: staleRev,
91
- message: 'should-fail'
92
- })
93
- assert.strictEqual(conflict.ok, false)
94
- assert.strictEqual(conflict.statusCode, 409)
95
- assert.strictEqual(conflict.error, 'conflict')
96
-
97
- const doc = await get(baseConfig, patch_doc_id)
98
- assert.strictEqual(doc?.message, current?.message)
99
- })
100
-
101
- await t.test('patchDangerously merges properties without revision', async () => {
102
- const result = await patchDangerously(baseConfig, patch_doc_id, {
103
- description: 'dangerously updated'
104
- })
105
- assert.ok(result?.ok)
106
-
107
- const doc = await get(baseConfig, patch_doc_id)
108
- assert.strictEqual(doc?.description, 'dangerously updated')
109
- })
110
-
111
- await t.test('patchDangerously returns not_found when document missing', async () => {
112
- const response = await patchDangerously(baseConfig, 'missing-doc', {
113
- message: 'noop'
114
- })
115
- assert.strictEqual(response?.ok, false)
116
- assert.strictEqual(response?.statusCode, 404)
117
- assert.strictEqual(response?.error, 'not_found')
118
- })
119
-
120
- await t.test('patchDangerously reports failure after exhausting retries', async () => {
121
- const doc = await get(baseConfig, patch_doc_id)
122
- const conflictConfig: CouchConfigInput = {
123
- ...baseConfig,
124
- maxRetries: 1,
125
- initialDelay: 1,
126
- backoffFactor: 1
127
- }
128
-
129
- const response = await patchDangerously(conflictConfig, patch_doc_id, {
130
- _rev: initial.rev,
131
- conflicted: true
132
- })
133
- assert.strictEqual(response?.ok, false)
134
- assert.strictEqual(response?.statusCode, 500)
135
- assert.match(response?.error ?? '', /Failed to patch after 1 attempts/)
136
-
137
- const current = await get(baseConfig, patch_doc_id)
138
- assert.strictEqual(current?.conflicted, undefined)
139
- assert.strictEqual(current?._rev, doc?._rev)
140
- })
141
- })
142
- })