hide-a-bed 5.2.7 → 6.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 (363) hide show
  1. package/.prettierrc +7 -0
  2. package/README.md +270 -218
  3. package/dist/cjs/index.cjs +1952 -0
  4. package/dist/esm/index.mjs +1898 -0
  5. package/docs/.nojekyll +1 -0
  6. package/docs/assets/hierarchy.js +1 -0
  7. package/docs/assets/highlight.css +113 -0
  8. package/docs/assets/icons.js +18 -0
  9. package/docs/assets/icons.svg +1 -0
  10. package/docs/assets/main.js +60 -0
  11. package/docs/assets/navigation.js +1 -0
  12. package/docs/assets/search.js +1 -0
  13. package/docs/assets/style.css +1633 -0
  14. package/docs/classes/QueryBuilder.html +42 -0
  15. package/docs/functions/bindConfig.html +4 -0
  16. package/docs/functions/bulkGet.html +14 -0
  17. package/docs/functions/bulkGetDictionary.html +10 -0
  18. package/docs/functions/bulkRemove.html +12 -0
  19. package/docs/functions/bulkRemoveMap.html +11 -0
  20. package/docs/functions/bulkSave.html +10 -0
  21. package/docs/functions/bulkSaveTransaction.html +23 -0
  22. package/docs/functions/createLock.html +7 -0
  23. package/docs/functions/createQuery.html +1 -0
  24. package/docs/functions/get.html +1 -0
  25. package/docs/functions/getAtRev.html +1 -0
  26. package/docs/functions/getDBInfo.html +10 -0
  27. package/docs/functions/patch.html +8 -0
  28. package/docs/functions/patchDangerously.html +9 -0
  29. package/docs/functions/put.html +1 -0
  30. package/docs/functions/query.html +15 -0
  31. package/docs/functions/queryStream.html +6 -0
  32. package/docs/functions/remove.html +1 -0
  33. package/docs/functions/removeLock.html +6 -0
  34. package/docs/functions/watchDocs.html +9 -0
  35. package/docs/functions/withRetry.html +6 -0
  36. package/docs/hierarchy.html +1 -0
  37. package/docs/index.html +483 -0
  38. package/docs/interfaces/NetworkError.html +6 -0
  39. package/docs/interfaces/NotFoundError.html +10 -0
  40. package/docs/interfaces/RetryOptions.html +10 -0
  41. package/docs/interfaces/RetryableError.html +10 -0
  42. package/docs/interfaces/StandardSchemaV1.FailureResult.html +4 -0
  43. package/docs/interfaces/StandardSchemaV1.Issue.html +6 -0
  44. package/docs/interfaces/StandardSchemaV1.Options.html +3 -0
  45. package/docs/interfaces/StandardSchemaV1.PathSegment.html +4 -0
  46. package/docs/interfaces/StandardSchemaV1.Props.html +10 -0
  47. package/docs/interfaces/StandardSchemaV1.SuccessResult.html +6 -0
  48. package/docs/interfaces/StandardSchemaV1.Types.html +6 -0
  49. package/docs/interfaces/StandardSchemaV1.html +4 -0
  50. package/docs/modules/StandardSchemaV1.html +1 -0
  51. package/docs/modules.html +1 -0
  52. package/docs/types/BoundInstance.html +1 -0
  53. package/docs/types/BulkGetBound.html +2 -0
  54. package/docs/types/BulkGetDictionaryBound.html +1 -0
  55. package/docs/types/BulkGetDictionaryOptions.html +2 -0
  56. package/docs/types/BulkGetDictionaryResult.html +3 -0
  57. package/docs/types/BulkGetOptions.html +3 -0
  58. package/docs/types/BulkGetResponse.html +1 -0
  59. package/docs/types/CouchConfig-1.html +1 -0
  60. package/docs/types/CouchConfig.html +1 -0
  61. package/docs/types/CouchConfigInput.html +1 -0
  62. package/docs/types/CouchDoc-1.html +1 -0
  63. package/docs/types/CouchDoc.html +2 -0
  64. package/docs/types/CouchDocInput.html +2 -0
  65. package/docs/types/GetAtRevBound.html +1 -0
  66. package/docs/types/GetBound.html +1 -0
  67. package/docs/types/GetOptions.html +2 -0
  68. package/docs/types/LockDoc-1.html +1 -0
  69. package/docs/types/LockDoc.html +1 -0
  70. package/docs/types/LockOptions-1.html +1 -0
  71. package/docs/types/LockOptions.html +1 -0
  72. package/docs/types/LockOptionsInput.html +1 -0
  73. package/docs/types/OnInvalidDocAction.html +1 -0
  74. package/docs/types/OnRow.html +1 -0
  75. package/docs/types/QueryBound.html +1 -0
  76. package/docs/types/SimpleViewOptions-1.html +1 -0
  77. package/docs/types/SimpleViewOptions.html +1 -0
  78. package/docs/types/StandardSchemaV1.InferInput.html +2 -0
  79. package/docs/types/StandardSchemaV1.InferOutput.html +2 -0
  80. package/docs/types/StandardSchemaV1.Result.html +2 -0
  81. package/docs/types/ViewQueryResponse-1.html +1 -0
  82. package/docs/types/ViewQueryResponse.html +2 -0
  83. package/docs/types/ViewQueryResponseValidated.html +2 -0
  84. package/docs/types/ViewRow-1.html +1 -0
  85. package/docs/types/ViewRow.html +2 -0
  86. package/docs/types/ViewRowValidated.html +7 -0
  87. package/docs/types/ViewString.html +1 -0
  88. package/docs/types/WatchOptionsInput.html +1 -0
  89. package/docs/types/WatchOptionsSchema-1.html +1 -0
  90. package/docs/types/WatchOptionsSchema.html +1 -0
  91. package/eslint.config.js +15 -0
  92. package/impl/bindConfig.mts +140 -0
  93. package/impl/bulkGet.mts +256 -0
  94. package/impl/bulkGet.test.mts +159 -0
  95. package/impl/bulkRemove.mts +98 -0
  96. package/impl/bulkRemove.test.mts +102 -0
  97. package/impl/bulkSave.mts +286 -0
  98. package/impl/bulkSave.test.mts +319 -0
  99. package/impl/get.mts +137 -0
  100. package/impl/get.test.mts +114 -0
  101. package/impl/getDBInfo.mts +67 -0
  102. package/impl/getDBInfo.test.mts +62 -0
  103. package/impl/patch.mts +134 -0
  104. package/impl/patch.test.mts +142 -0
  105. package/impl/put.mts +56 -0
  106. package/impl/put.test.mts +114 -0
  107. package/impl/query.mts +224 -0
  108. package/impl/query.test.mts +280 -0
  109. package/impl/remove.mts +65 -0
  110. package/impl/remove.test.mts +82 -0
  111. package/impl/retry.mts +66 -0
  112. package/impl/retry.test.mts +77 -0
  113. package/impl/stream.mts +143 -0
  114. package/impl/stream.test.mts +205 -0
  115. package/impl/sugar/lock.mts +103 -0
  116. package/impl/sugar/lock.test.mts +113 -0
  117. package/impl/sugar/{watch.mjs → watch.mts} +56 -22
  118. package/impl/sugar/watch.test.mts +155 -0
  119. package/impl/utils/errors.mts +130 -0
  120. package/impl/utils/errors.test.mts +58 -0
  121. package/impl/utils/logger.mts +62 -0
  122. package/impl/utils/logger.test.mts +129 -0
  123. package/impl/utils/mergeNeedleOpts.mts +16 -0
  124. package/impl/utils/parseRows.mts +117 -0
  125. package/impl/utils/parseRows.test.mts +183 -0
  126. package/impl/utils/queryBuilder.mts +173 -0
  127. package/impl/utils/queryBuilder.test.mts +83 -0
  128. package/impl/utils/queryString.mts +44 -0
  129. package/impl/utils/queryString.test.mts +53 -0
  130. package/impl/{trackedEmitter.mjs → utils/trackedEmitter.mts} +9 -7
  131. package/impl/utils/transactionErrors.mts +71 -0
  132. package/index.mts +82 -0
  133. package/index.test.mts +415 -0
  134. package/package.json +45 -31
  135. package/schema/config.mts +81 -0
  136. package/schema/couch/couch.input.schema.ts +43 -0
  137. package/schema/couch/couch.output.schema.ts +169 -0
  138. package/schema/sugar/lock.mts +18 -0
  139. package/schema/sugar/watch.mts +14 -0
  140. package/schema/util.mts +8 -0
  141. package/tsconfig.json +10 -4
  142. package/tsdown.config.ts +16 -0
  143. package/typedoc.json +4 -0
  144. package/types/output/eslint.config.d.ts +3 -0
  145. package/types/output/eslint.config.d.ts.map +1 -0
  146. package/types/output/impl/bindConfig.d.mts +174 -0
  147. package/types/output/impl/bindConfig.d.mts.map +1 -0
  148. package/types/output/impl/bulkGet.d.mts +75 -0
  149. package/types/output/impl/bulkGet.d.mts.map +1 -0
  150. package/types/output/impl/bulkGet.test.d.mts +2 -0
  151. package/types/output/impl/bulkGet.test.d.mts.map +1 -0
  152. package/types/output/impl/bulkRemove.d.mts +63 -0
  153. package/types/output/impl/bulkRemove.d.mts.map +1 -0
  154. package/types/output/impl/bulkRemove.test.d.mts +2 -0
  155. package/types/output/impl/bulkRemove.test.d.mts.map +1 -0
  156. package/types/output/impl/bulkSave.d.mts +64 -0
  157. package/types/output/impl/bulkSave.d.mts.map +1 -0
  158. package/types/output/impl/bulkSave.test.d.mts +2 -0
  159. package/types/output/impl/bulkSave.test.d.mts.map +1 -0
  160. package/types/output/impl/get.d.mts +20 -0
  161. package/types/output/impl/get.d.mts.map +1 -0
  162. package/types/output/impl/get.test.d.mts +2 -0
  163. package/types/output/impl/get.test.d.mts.map +1 -0
  164. package/types/output/impl/getDBInfo.d.mts +52 -0
  165. package/types/output/impl/getDBInfo.d.mts.map +1 -0
  166. package/types/output/impl/getDBInfo.test.d.mts +2 -0
  167. package/types/output/impl/getDBInfo.test.d.mts.map +1 -0
  168. package/types/output/impl/patch.d.mts +45 -0
  169. package/types/output/impl/patch.d.mts.map +1 -0
  170. package/types/output/impl/patch.test.d.mts +2 -0
  171. package/types/output/impl/patch.test.d.mts.map +1 -0
  172. package/types/output/impl/put.d.mts +5 -0
  173. package/types/output/impl/put.d.mts.map +1 -0
  174. package/types/output/impl/put.test.d.mts +2 -0
  175. package/types/output/impl/put.test.d.mts.map +1 -0
  176. package/types/output/impl/query.d.mts +47 -0
  177. package/types/output/impl/query.d.mts.map +1 -0
  178. package/types/output/impl/query.test.d.mts +2 -0
  179. package/types/output/impl/query.test.d.mts.map +1 -0
  180. package/types/output/impl/remove.d.mts +9 -0
  181. package/types/output/impl/remove.d.mts.map +1 -0
  182. package/types/output/impl/remove.test.d.mts +2 -0
  183. package/types/output/impl/remove.test.d.mts.map +1 -0
  184. package/types/output/impl/retry.d.mts +32 -0
  185. package/types/output/impl/retry.d.mts.map +1 -0
  186. package/types/output/impl/retry.test.d.mts +2 -0
  187. package/types/output/impl/retry.test.d.mts.map +1 -0
  188. package/types/output/impl/stream.d.mts +13 -0
  189. package/types/output/impl/stream.d.mts.map +1 -0
  190. package/types/output/impl/stream.test.d.mts +2 -0
  191. package/types/output/impl/stream.test.d.mts.map +1 -0
  192. package/types/output/impl/sugar/lock.d.mts +24 -0
  193. package/types/output/impl/sugar/lock.d.mts.map +1 -0
  194. package/types/output/impl/sugar/lock.test.d.mts +2 -0
  195. package/types/output/impl/sugar/lock.test.d.mts.map +1 -0
  196. package/types/output/impl/sugar/watch.d.mts +21 -0
  197. package/types/output/impl/sugar/watch.d.mts.map +1 -0
  198. package/types/output/impl/sugar/watch.test.d.mts +2 -0
  199. package/types/output/impl/sugar/watch.test.d.mts.map +1 -0
  200. package/types/output/impl/utils/errors.d.mts +78 -0
  201. package/types/output/impl/utils/errors.d.mts.map +1 -0
  202. package/types/output/impl/utils/errors.test.d.mts +2 -0
  203. package/types/output/impl/utils/errors.test.d.mts.map +1 -0
  204. package/types/output/impl/utils/logger.d.mts +11 -0
  205. package/types/output/impl/utils/logger.d.mts.map +1 -0
  206. package/types/output/impl/utils/logger.test.d.mts +2 -0
  207. package/types/output/impl/utils/logger.test.d.mts.map +1 -0
  208. package/types/output/impl/utils/mergeNeedleOpts.d.mts +53 -0
  209. package/types/output/impl/utils/mergeNeedleOpts.d.mts.map +1 -0
  210. package/types/output/impl/utils/parseRows.d.mts +15 -0
  211. package/types/output/impl/utils/parseRows.d.mts.map +1 -0
  212. package/types/output/impl/utils/parseRows.test.d.mts +2 -0
  213. package/types/output/impl/utils/parseRows.test.d.mts.map +1 -0
  214. package/types/output/impl/utils/queryBuilder.d.mts +68 -0
  215. package/types/output/impl/utils/queryBuilder.d.mts.map +1 -0
  216. package/types/output/impl/utils/queryBuilder.test.d.mts +2 -0
  217. package/types/output/impl/utils/queryBuilder.test.d.mts.map +1 -0
  218. package/types/output/impl/utils/queryString.d.mts +9 -0
  219. package/types/output/impl/utils/queryString.d.mts.map +1 -0
  220. package/types/output/impl/utils/queryString.test.d.mts +2 -0
  221. package/types/output/impl/utils/queryString.test.d.mts.map +1 -0
  222. package/types/output/impl/utils/trackedEmitter.d.mts +7 -0
  223. package/types/output/impl/utils/trackedEmitter.d.mts.map +1 -0
  224. package/{impl → types/output/impl/utils}/transactionErrors.d.mts +16 -31
  225. package/types/output/impl/utils/transactionErrors.d.mts.map +1 -0
  226. package/types/output/index.d.mts +32 -0
  227. package/types/output/index.d.mts.map +1 -0
  228. package/types/output/index.test.d.mts +2 -0
  229. package/types/output/index.test.d.mts.map +1 -0
  230. package/types/output/schema/config.d.mts +90 -0
  231. package/types/output/schema/config.d.mts.map +1 -0
  232. package/types/output/schema/couch/couch.input.schema.d.ts +29 -0
  233. package/types/output/schema/couch/couch.input.schema.d.ts.map +1 -0
  234. package/types/output/schema/couch/couch.output.schema.d.ts +113 -0
  235. package/types/output/schema/couch/couch.output.schema.d.ts.map +1 -0
  236. package/types/output/schema/sugar/lock.d.mts +19 -0
  237. package/types/output/schema/sugar/lock.d.mts.map +1 -0
  238. package/types/output/schema/sugar/watch.d.mts +11 -0
  239. package/types/output/schema/sugar/watch.d.mts.map +1 -0
  240. package/types/output/schema/util.d.mts +85 -0
  241. package/types/output/schema/util.d.mts.map +1 -0
  242. package/types/output/tsdown.config.d.ts +3 -0
  243. package/types/output/tsdown.config.d.ts.map +1 -0
  244. package/types/output/types/standard-schema.d.ts +60 -0
  245. package/types/output/types/standard-schema.d.ts.map +1 -0
  246. package/types/standard-schema.ts +76 -0
  247. package/types/utils.d.ts +1 -0
  248. package/cjs/impl/bulk.cjs +0 -275
  249. package/cjs/impl/changes.cjs +0 -67
  250. package/cjs/impl/crud.cjs +0 -127
  251. package/cjs/impl/errors.cjs +0 -75
  252. package/cjs/impl/logger.cjs +0 -70
  253. package/cjs/impl/patch.cjs +0 -95
  254. package/cjs/impl/query.cjs +0 -116
  255. package/cjs/impl/queryBuilder.cjs +0 -163
  256. package/cjs/impl/retry.cjs +0 -55
  257. package/cjs/impl/stream.cjs +0 -121
  258. package/cjs/impl/sugar/lock.cjs +0 -81
  259. package/cjs/impl/sugar/watch.cjs +0 -159
  260. package/cjs/impl/trackedEmitter.cjs +0 -54
  261. package/cjs/impl/transactionErrors.cjs +0 -70
  262. package/cjs/impl/util.cjs +0 -64
  263. package/cjs/index.cjs +0 -132
  264. package/cjs/integration/changes.cjs +0 -76
  265. package/cjs/integration/disconnect-watch.cjs +0 -52
  266. package/cjs/integration/watch.cjs +0 -59
  267. package/cjs/schema/bind.cjs +0 -59
  268. package/cjs/schema/bulk.cjs +0 -92
  269. package/cjs/schema/changes.cjs +0 -68
  270. package/cjs/schema/config.cjs +0 -48
  271. package/cjs/schema/crud.cjs +0 -77
  272. package/cjs/schema/patch.cjs +0 -53
  273. package/cjs/schema/query.cjs +0 -62
  274. package/cjs/schema/stream.cjs +0 -42
  275. package/cjs/schema/sugar/lock.cjs +0 -59
  276. package/cjs/schema/sugar/watch.cjs +0 -42
  277. package/cjs/schema/util.cjs +0 -39
  278. package/config.json +0 -5
  279. package/docs/compiler.png +0 -0
  280. package/dualmode.config.json +0 -11
  281. package/impl/bulk.d.mts +0 -11
  282. package/impl/bulk.d.mts.map +0 -1
  283. package/impl/bulk.mjs +0 -291
  284. package/impl/changes.d.mts +0 -12
  285. package/impl/changes.d.mts.map +0 -1
  286. package/impl/changes.mjs +0 -53
  287. package/impl/crud.d.mts +0 -7
  288. package/impl/crud.d.mts.map +0 -1
  289. package/impl/crud.mjs +0 -108
  290. package/impl/errors.d.mts +0 -43
  291. package/impl/errors.d.mts.map +0 -1
  292. package/impl/errors.mjs +0 -65
  293. package/impl/logger.d.mts +0 -32
  294. package/impl/logger.d.mts.map +0 -1
  295. package/impl/logger.mjs +0 -59
  296. package/impl/patch.d.mts +0 -6
  297. package/impl/patch.d.mts.map +0 -1
  298. package/impl/patch.mjs +0 -88
  299. package/impl/query.d.mts +0 -195
  300. package/impl/query.d.mts.map +0 -1
  301. package/impl/query.mjs +0 -122
  302. package/impl/queryBuilder.d.mts +0 -154
  303. package/impl/queryBuilder.d.mts.map +0 -1
  304. package/impl/queryBuilder.mjs +0 -175
  305. package/impl/retry.d.mts +0 -2
  306. package/impl/retry.d.mts.map +0 -1
  307. package/impl/retry.mjs +0 -39
  308. package/impl/stream.d.mts +0 -3
  309. package/impl/stream.d.mts.map +0 -1
  310. package/impl/stream.mjs +0 -98
  311. package/impl/sugar/lock.d.mts +0 -5
  312. package/impl/sugar/lock.d.mts.map +0 -1
  313. package/impl/sugar/lock.mjs +0 -70
  314. package/impl/sugar/watch.d.mts +0 -34
  315. package/impl/sugar/watch.d.mts.map +0 -1
  316. package/impl/trackedEmitter.d.mts +0 -8
  317. package/impl/trackedEmitter.d.mts.map +0 -1
  318. package/impl/transactionErrors.d.mts.map +0 -1
  319. package/impl/transactionErrors.mjs +0 -47
  320. package/impl/util.d.mts +0 -3
  321. package/impl/util.d.mts.map +0 -1
  322. package/impl/util.mjs +0 -35
  323. package/index.d.mts +0 -80
  324. package/index.d.mts.map +0 -1
  325. package/index.mjs +0 -141
  326. package/integration/changes.mjs +0 -60
  327. package/integration/disconnect-watch.mjs +0 -36
  328. package/integration/watch.mjs +0 -40
  329. package/log.txt +0 -580
  330. package/schema/bind.d.mts +0 -5461
  331. package/schema/bind.d.mts.map +0 -1
  332. package/schema/bind.mjs +0 -43
  333. package/schema/bulk.d.mts +0 -923
  334. package/schema/bulk.d.mts.map +0 -1
  335. package/schema/bulk.mjs +0 -83
  336. package/schema/changes.d.mts +0 -191
  337. package/schema/changes.d.mts.map +0 -1
  338. package/schema/changes.mjs +0 -59
  339. package/schema/config.d.mts +0 -79
  340. package/schema/config.d.mts.map +0 -1
  341. package/schema/config.mjs +0 -26
  342. package/schema/crud.d.mts +0 -491
  343. package/schema/crud.d.mts.map +0 -1
  344. package/schema/crud.mjs +0 -64
  345. package/schema/patch.d.mts +0 -255
  346. package/schema/patch.d.mts.map +0 -1
  347. package/schema/patch.mjs +0 -42
  348. package/schema/query.d.mts +0 -406
  349. package/schema/query.d.mts.map +0 -1
  350. package/schema/query.mjs +0 -45
  351. package/schema/stream.d.mts +0 -211
  352. package/schema/stream.d.mts.map +0 -1
  353. package/schema/stream.mjs +0 -23
  354. package/schema/sugar/lock.d.mts +0 -238
  355. package/schema/sugar/lock.d.mts.map +0 -1
  356. package/schema/sugar/lock.mjs +0 -50
  357. package/schema/sugar/watch.d.mts +0 -127
  358. package/schema/sugar/watch.d.mts.map +0 -1
  359. package/schema/sugar/watch.mjs +0 -29
  360. package/schema/util.d.mts +0 -160
  361. package/schema/util.d.mts.map +0 -1
  362. package/schema/util.mjs +0 -35
  363. package/types/changes-stream.d.ts +0 -11
@@ -0,0 +1,159 @@
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 { z } from 'zod'
6
+ import { RetryableError } from './utils/errors.mts'
7
+ import { bulkGet, bulkGetDictionary } from './bulkGet.mts'
8
+ import { TEST_DB_URL } from '../test/setup-db.mts'
9
+
10
+ const config: CouchConfigInput = {
11
+ couch: TEST_DB_URL
12
+ }
13
+
14
+ async function ensureDoc(id: string, body: Record<string, unknown>) {
15
+ await needle(
16
+ 'put',
17
+ `${TEST_DB_URL}/${id}`,
18
+ {
19
+ _id: id,
20
+ ...body
21
+ },
22
+ { json: true }
23
+ )
24
+ }
25
+
26
+ suite('bulkGet', () => {
27
+ test('integration with pouchdb-server', async t => {
28
+ await ensureDoc('doc-1', { value: 42 })
29
+ await ensureDoc('doc-valid', { count: 7 })
30
+ await ensureDoc('doc-invalid', { count: 'nope' })
31
+
32
+ await t.test('fetches docs and not-found rows', async () => {
33
+ const response = await bulkGet(config, ['doc-1', 'doc-missing'])
34
+ assert.strictEqual(response.rows.length, 2)
35
+ const [first, second] = response.rows
36
+ assert.strictEqual(first?.id, 'doc-1')
37
+ assert.strictEqual(first?.doc?._id, 'doc-1')
38
+ assert.strictEqual(first?.doc?.value, 42)
39
+ assert.strictEqual(second?.error, 'not_found')
40
+ assert.strictEqual(second?.key, 'doc-missing')
41
+ })
42
+
43
+ await t.test('supports includeDocs=false via _bulkGetWithOptions', async () => {
44
+ const response = await bulkGet(config, ['doc-1'], {
45
+ includeDocs: false
46
+ })
47
+ assert.strictEqual(response.rows.length, 1)
48
+ const [row] = response.rows
49
+ assert.strictEqual(row?.id, 'doc-1')
50
+ assert.ok(row?.value?.rev)
51
+ assert.ok(!('doc' in (row as Record<string, unknown>)))
52
+ })
53
+
54
+ await t.test('validates documents when schema provided', async () => {
55
+ const schema = z.looseObject({
56
+ _id: z.string(),
57
+ _rev: z.string().optional(),
58
+ count: z.number()
59
+ })
60
+
61
+ const valid = await bulkGet(config, ['doc-valid'], {
62
+ validate: {
63
+ docSchema: schema
64
+ }
65
+ })
66
+ assert.strictEqual(valid.rows[0]?.doc?.count, 7)
67
+
68
+ await assert.rejects(
69
+ () =>
70
+ bulkGet(config, ['doc-invalid'], {
71
+ validate: {
72
+ docSchema: schema
73
+ }
74
+ }),
75
+ (err: unknown) => {
76
+ assert.ok(Array.isArray(err))
77
+ assert.match(err[0]?.message, /Invalid input:/)
78
+ return true
79
+ }
80
+ )
81
+ })
82
+
83
+ await t.test('skips invalid documents when onInvalidDoc=skip', async () => {
84
+ const schema = z.looseObject({
85
+ _id: z.string(),
86
+ _rev: z.string().optional(),
87
+ count: z.number()
88
+ })
89
+
90
+ const response = await bulkGet(config, ['doc-valid', 'doc-invalid'], {
91
+ validate: {
92
+ docSchema: schema,
93
+ onInvalidDoc: 'skip'
94
+ }
95
+ })
96
+
97
+ assert.strictEqual(response.rows.length, 1)
98
+ assert.strictEqual(response.rows[0]?.doc?._id, 'doc-valid')
99
+ assert.strictEqual(response.rows[0]?.doc?.count, 7)
100
+ })
101
+
102
+ await t.test('throws RetryableError for retryable status codes', async () => {
103
+ const offlineConfig: CouchConfigInput = {
104
+ couch: 'http://localhost:6553/offline-db'
105
+ }
106
+
107
+ await assert.rejects(
108
+ () => bulkGet(offlineConfig, ['doc-1']),
109
+ (err: unknown) => err instanceof RetryableError && err.statusCode === 503
110
+ )
111
+ })
112
+
113
+ await t.test('bulkGetDictionary groups results', async () => {
114
+ const result = await bulkGetDictionary(config, ['doc-valid', 'doc-missing'])
115
+ assert.deepStrictEqual(Object.keys(result.found), ['doc-valid'])
116
+ assert.strictEqual(result.found['doc-valid'].count, 7)
117
+ assert.deepStrictEqual(Object.keys(result.notFound), ['doc-missing'])
118
+ assert.strictEqual(result.notFound['doc-missing'].error, 'not_found')
119
+ })
120
+
121
+ await t.test('bulkGetDictionary with validation schema', async () => {
122
+ const schema = z.looseObject({
123
+ _id: z.string(),
124
+ _rev: z.string().optional(),
125
+ count: z.number()
126
+ })
127
+
128
+ const result = await bulkGetDictionary(config, ['doc-valid', 'doc-not-there'], {
129
+ validate: {
130
+ docSchema: schema
131
+ }
132
+ })
133
+
134
+ assert.deepStrictEqual(Object.keys(result.found), ['doc-valid'])
135
+ assert.strictEqual(result.found['doc-valid'].count, 7)
136
+ assert.deepStrictEqual(Object.keys(result.notFound), ['doc-not-there'])
137
+ assert.strictEqual(result.notFound['doc-not-there'].error, 'not_found')
138
+ })
139
+
140
+ await t.test('bulkGetDictionary skips invalid docs when requested', async () => {
141
+ const schema = z.looseObject({
142
+ _id: z.string(),
143
+ _rev: z.string().optional(),
144
+ count: z.number()
145
+ })
146
+
147
+ const result = await bulkGetDictionary(config, ['doc-valid', 'doc-invalid'], {
148
+ validate: {
149
+ docSchema: schema,
150
+ onInvalidDoc: 'skip'
151
+ }
152
+ })
153
+
154
+ assert.deepStrictEqual(Object.keys(result.found), ['doc-valid'])
155
+ assert.strictEqual(result.found['doc-valid'].count, 7)
156
+ assert.deepStrictEqual(Object.keys(result.notFound), [])
157
+ })
158
+ })
159
+ })
@@ -0,0 +1,98 @@
1
+ import { bulkGet } from './bulkGet.mts'
2
+ import { bulkSave } from './bulkSave.mts'
3
+ import { createLogger } from './utils/logger.mts'
4
+ import { remove } from './remove.mts'
5
+ import { CouchDoc } from '../schema/couch/couch.output.schema.ts'
6
+ import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
7
+
8
+ /**
9
+ * Removes multiple documents from a CouchDB database using the _bulk_docs endpoint.
10
+ * It first retrieves the documents by their IDs, marks them as deleted, and then
11
+ * sends them back to the database for deletion.
12
+ *
13
+ * See https://docs.couchdb.org/en/stable/api/database/bulk-api.html#post--db-_bulk_docs
14
+ *
15
+ * @param configInput - The CouchDB configuration input.
16
+ * @param ids - An array of document IDs to be removed.
17
+ * @returns A promise that resolves to an array of results from the bulk delete operation.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const config: CouchConfigInput = {
22
+ * couch: 'http://localhost:5984/mydb',
23
+ * useConsoleLogger: true
24
+ * };
25
+ * const idsToRemove = ['doc1', 'doc2', 'doc3'];
26
+ * const results = await bulkRemove(config, idsToRemove);
27
+ * console.log(results);
28
+ * ```
29
+ *
30
+ * @throws Will throw an error if the provided configuration is invalid or if the bulk delete operation fails.
31
+ */
32
+ export const bulkRemove = async (configInput: CouchConfigInput, ids: string[]) => {
33
+ const config = CouchConfig.parse(configInput)
34
+ const logger = createLogger(config)
35
+ logger.info(`Starting bulk remove for ${ids.length} documents`)
36
+ const resp = await bulkGet(config, ids)
37
+ const toRemove: Array<CouchDoc> = []
38
+ resp.rows?.forEach(row => {
39
+ if (!row.doc) return
40
+ try {
41
+ const d = CouchDoc.parse(row.doc)
42
+ d._deleted = true
43
+ toRemove.push(d)
44
+ } catch (e) {
45
+ logger.warn(`Invalid document structure in bulk remove: ${row.id}`, e)
46
+ }
47
+ })
48
+ if (!toRemove.length) return []
49
+ const result = await bulkSave(config, toRemove)
50
+ return result
51
+ }
52
+
53
+ /**
54
+ * Removes multiple documents from a CouchDB database by their IDs using individual delete operations.
55
+ * It first retrieves the documents to get their revision IDs, then deletes each document one by one.
56
+ *
57
+ * See https://docs.couchdb.org/en/stable/api/document/common.html#delete--db-docid
58
+ *
59
+ * @param configInput - The CouchDB configuration input.
60
+ * @param ids - An array of document IDs to be removed.
61
+ * @returns A promise that resolves to an array of results from the individual delete operations.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const config: CouchConfigInput = {
66
+ * couch: 'http://localhost:5984/mydb',
67
+ * useConsoleLogger: true
68
+ * };
69
+ * const idsToRemove = ['doc1', 'doc2', 'doc3'];
70
+ * const results = await bulkRemoveMap(config, idsToRemove);
71
+ * console.log(results);
72
+ * ```
73
+ *
74
+ * @throws Will throw an error if the provided configuration is invalid or if any delete operation fails.
75
+ */
76
+ export const bulkRemoveMap = async (configInput: CouchConfigInput, ids: string[]) => {
77
+ const config = CouchConfig.parse(configInput)
78
+ const logger = createLogger(config)
79
+ logger.info(`Starting bulk remove map for ${ids.length} documents`)
80
+
81
+ const { rows } = await bulkGet(config, ids, { includeDocs: false })
82
+
83
+ const results = []
84
+ for (const row of rows || []) {
85
+ try {
86
+ if (!row.value?.rev) throw new Error(`no rev found for doc ${row.id}`)
87
+ if (!row.id) {
88
+ throw new Error(`no id found for doc ${row}`)
89
+ }
90
+
91
+ const result = await remove(config, row.id, row.value.rev)
92
+ results.push(result)
93
+ } catch (e) {
94
+ logger.warn(`Error removing a doc in bulk remove map: ${row.id}`, e)
95
+ }
96
+ }
97
+ return results
98
+ }
@@ -0,0 +1,102 @@
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 { bulkRemove, bulkRemoveMap } from './bulkRemove.mts'
6
+ import { TEST_DB_URL } from '../test/setup-db.mts'
7
+
8
+ const config: CouchConfigInput = {
9
+ couch: TEST_DB_URL
10
+ }
11
+
12
+ type DocBody = Record<string, unknown>
13
+
14
+ async function saveDoc(id: string, body: DocBody) {
15
+ const response = await needle(
16
+ 'put',
17
+ `${TEST_DB_URL}/${id}`,
18
+ {
19
+ _id: id,
20
+ ...body
21
+ },
22
+ { json: true }
23
+ )
24
+
25
+ if (response.statusCode !== 201 && response.statusCode !== 200) {
26
+ throw new Error(`Failed to save document ${id}: ${response.statusCode}`)
27
+ }
28
+
29
+ return response.body as { rev: string }
30
+ }
31
+
32
+ async function getDoc(id: string) {
33
+ return needle('get', `${TEST_DB_URL}/${id}`, null, { json: true })
34
+ }
35
+
36
+ suite('bulkRemove', () => {
37
+ test('it should throw if provided config is invalid', async () => {
38
+ await assert.rejects(async () => {
39
+ // @ts-expect-error testing invalid config
40
+ await bulkRemove({ notAnOption: true, couch: DB_URL, useConsoleLogger: true }, ['doc1'])
41
+ })
42
+
43
+ await assert.rejects(async () => {
44
+ // @ts-expect-error testing invalid config
45
+ await bulkRemoveMap({ anotherBadOption: 123, couch: DB_URL, useConsoleLogger: true }, [
46
+ 'doc1'
47
+ ])
48
+ })
49
+ })
50
+
51
+ test('integration with pouchdb-server', async t => {
52
+ await t.test('removes documents via _bulk_docs', async () => {
53
+ await saveDoc('bulk-remove-doc-1', { kind: 'test', count: 1 })
54
+
55
+ const results = await bulkRemove(config, ['bulk-remove-doc-1'])
56
+ assert.strictEqual(results.length, 1)
57
+ const [first] = results
58
+ assert.strictEqual(first?.id, 'bulk-remove-doc-1')
59
+ assert.strictEqual(first?.ok, true)
60
+ assert.ok(typeof first?.rev === 'string')
61
+
62
+ const { statusCode, body } = await getDoc('bulk-remove-doc-1')
63
+ assert.strictEqual(statusCode, 404)
64
+ assert.strictEqual(body?.error, 'not_found')
65
+ })
66
+
67
+ await t.test('returns empty array when docs are missing', async () => {
68
+ const results = await bulkRemove(config, ['bulk-remove-missing'])
69
+ assert.deepStrictEqual(results, [])
70
+ })
71
+
72
+ await t.test('bulkRemoveMap removes each document individually', async () => {
73
+ await saveDoc('bulk-remove-map-doc-1', { kind: 'map', count: 1 })
74
+
75
+ const results = await bulkRemoveMap(config, ['bulk-remove-map-doc-1'])
76
+ assert.strictEqual(results.length, 1)
77
+ const [first] = results
78
+ assert.ok(first)
79
+ assert.strictEqual(first.id, 'bulk-remove-map-doc-1')
80
+ assert.strictEqual(first.ok, true)
81
+ assert.strictEqual(first.statusCode, 200)
82
+
83
+ const { statusCode, body } = await getDoc('bulk-remove-map-doc-1')
84
+ assert.strictEqual(statusCode, 404)
85
+ assert.strictEqual(body?.error, 'not_found')
86
+ })
87
+
88
+ await t.test('bulkRemoveMap skips docs without revs', async () => {
89
+ await saveDoc('bulk-remove-map-doc-2', { kind: 'map', count: 2 })
90
+
91
+ const results = await bulkRemoveMap(config, [
92
+ 'bulk-remove-map-doc-2',
93
+ 'bulk-remove-map-missing'
94
+ ])
95
+ assert.strictEqual(results.length, 1)
96
+ const [first] = results
97
+ assert.ok(first)
98
+ assert.strictEqual(first.id, 'bulk-remove-map-doc-2')
99
+ assert.strictEqual(first.ok, true)
100
+ })
101
+ })
102
+ })
@@ -0,0 +1,286 @@
1
+ import needle from 'needle'
2
+ import { createLogger } from './utils/logger.mts'
3
+ import { mergeNeedleOpts } from './utils/mergeNeedleOpts.mts'
4
+ import { bulkGetDictionary } from './bulkGet.mts'
5
+ import { setupEmitter } from './utils/trackedEmitter.mts'
6
+ import {
7
+ TransactionSetupError,
8
+ TransactionVersionConflictError,
9
+ TransactionBulkOperationError,
10
+ TransactionRollbackError
11
+ } from './utils/transactionErrors.mts'
12
+ import {
13
+ BulkSaveResponse,
14
+ CouchDoc,
15
+ type CouchDocInput
16
+ } from '../schema/couch/couch.output.schema.ts'
17
+ import type { CouchConfigInput } from '../schema/config.mts'
18
+ import { RetryableError } from './utils/errors.mts'
19
+ import { withRetry } from './retry.mts'
20
+ import { put } from './put.mts'
21
+
22
+ /**
23
+ * Bulk saves documents to CouchDB using the _bulk_docs endpoint.
24
+ *
25
+ * @see
26
+ * https://docs.couchdb.org/en/stable/api/database/bulk-api.html#db-bulk-docs
27
+ *
28
+ * @param {CouchConfigInput} config - The CouchDB configuration.
29
+ * @param {CouchDocInput[]} docs - An array of documents to save.
30
+ * @returns {Promise<BulkSaveResponse>} - The response from CouchDB after the bulk save operation.
31
+ *
32
+ * @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.
34
+ */
35
+ export const bulkSave = async (config: CouchConfigInput, docs: CouchDocInput[]) => {
36
+ const logger = createLogger(config)
37
+
38
+ if (docs == null || !docs.length) {
39
+ logger.error('bulkSave called with no docs')
40
+ throw new Error('no docs provided')
41
+ }
42
+
43
+ logger.info(`Starting bulk save of ${docs.length} documents`)
44
+ const url = `${config.couch}/_bulk_docs`
45
+ 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
+ let resp
54
+ try {
55
+ resp = await needle('post', url, body, mergedOpts)
56
+ } catch (err) {
57
+ logger.error('Network error during bulk save:', err)
58
+ RetryableError.handleNetworkError(err)
59
+ }
60
+ if (!resp) {
61
+ logger.error('No response received from bulk save request')
62
+ throw new RetryableError('no response', 503)
63
+ }
64
+ if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
65
+ logger.warn(`Retryable status code received: ${resp.statusCode}`)
66
+ throw new RetryableError('retryable error during bulk save', resp.statusCode)
67
+ }
68
+ if (resp.statusCode !== 201) {
69
+ logger.error(`Unexpected status code: ${resp.statusCode}`)
70
+ throw new Error('could not save')
71
+ }
72
+ const results = resp?.body || []
73
+ return BulkSaveResponse.parse(results)
74
+ }
75
+
76
+ type TransactionStatus = 'pending' | 'completed' | 'rolled_back' | 'rollback_failed'
77
+ type TransactionDoc = {
78
+ _id: string
79
+ _rev: string | null | undefined
80
+ type: 'transaction'
81
+ status: TransactionStatus
82
+ changes: CouchDocInput[]
83
+ timestamp: string
84
+ }
85
+
86
+ /**
87
+ * Performs a bulk save of documents within a transaction context.
88
+ *
89
+ * @remarks
90
+ * This operation ensures that either all documents are saved successfully, or none are, maintaining data consistency.
91
+ * If any document fails to save, the operation will attempt to roll back all changes.
92
+ *
93
+ * The transactionId has to be unique for the lifetime of the app. It is used to prevent two processes from executing the same transaction. It is up to you to craft a transactionId that uniquely represents this transaction, and that also is the same if another process tries to generate it.
94
+ *
95
+ * Exceptions to handle:
96
+ *
97
+ * `TransactionSetupError` Thrown if the transaction document cannot be created. Usually because it already exists
98
+ * `TransactionVersionConflictError` Thrown if there are version conflicts with existing documents.
99
+ * `TransactionBulkOperationError` Thrown if the bulk save operation fails for some documents.
100
+ * `TransactionRollbackError` Thrown if the rollback operation fails after a transaction failure.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const docsToSave = [
105
+ * { _id: 'doc1', foo: 'bar' },
106
+ * { _id: 'doc2', foo: 'baz' }
107
+ * ];
108
+ *
109
+ * try {
110
+ * const results = await bulkSaveTransaction(config, 'unique-transaction-id', docsToSave);
111
+ * console.log('Bulk save successful:', results);
112
+ * } catch (error) {
113
+ * console.error('Bulk save transaction failed:', error);
114
+ * }
115
+ * ```
116
+ *
117
+ * @param {CouchConfigInput} config - The CouchDB configuration.
118
+ * @param {string} transactionId - A unique identifier for the transaction.
119
+ * @param {CouchDocInput[]} docs - An array of documents to save.
120
+ * @returns {Promise<BulkSaveResponse>} - The transaction save results.
121
+ * @throws {TransactionSetupError} When the transaction document cannot be created.
122
+ * @throws {TransactionVersionConflictError} When there are version conflicts with existing documents.
123
+ * @throws {TransactionBulkOperationError} When the bulk save operation fails for some documents.
124
+ * @throws {TransactionRollbackError} When the rollback operation fails after a transaction failure.
125
+ */
126
+ export const bulkSaveTransaction = async (
127
+ config: CouchConfigInput,
128
+ transactionId: string,
129
+ docs: CouchDocInput[]
130
+ ): Promise<BulkSaveResponse> => {
131
+ const emitter = setupEmitter(config)
132
+ const logger = createLogger(config)
133
+ const retryOptions = {
134
+ maxRetries: config.maxRetries ?? 10,
135
+ initialDelay: config.initialDelay ?? 1000,
136
+ backoffFactor: config.backoffFactor ?? 2
137
+ }
138
+ const _put = config.bindWithRetry
139
+ ? withRetry(put.bind(null, config), retryOptions)
140
+ : put.bind(null, config)
141
+ logger.info(`Starting bulk save transaction ${transactionId} for ${docs.length} documents`)
142
+
143
+ // Create transaction document
144
+ const transactionDoc: TransactionDoc = {
145
+ _id: `txn:${transactionId}`,
146
+ _rev: null,
147
+ type: 'transaction',
148
+ status: 'pending',
149
+ changes: docs,
150
+ timestamp: new Date().toISOString()
151
+ }
152
+
153
+ // Save transaction document
154
+ let transactionResponse = await _put(transactionDoc)
155
+ logger.debug('Transaction document created:', transactionDoc, transactionResponse)
156
+ await emitter.emit('transaction-created', {
157
+ transactionResponse,
158
+ txnDoc: transactionDoc
159
+ })
160
+ if (transactionResponse.error) {
161
+ throw new TransactionSetupError('Failed to create transaction document', {
162
+ error: transactionResponse.error,
163
+ response: transactionResponse
164
+ })
165
+ }
166
+
167
+ // Get current revisions of all documents
168
+ const existingDocs = await bulkGetDictionary(
169
+ config,
170
+ docs.map(d => d._id)
171
+ )
172
+ logger.debug('Fetched current revisions of documents:', existingDocs)
173
+ await emitter.emit('transaction-revs-fetched', existingDocs)
174
+
175
+ /** @type {string[]} */
176
+ const revErrors: string[] = []
177
+ // if any of the existingDocs, and the docs provided do not match on rev, then throw an error
178
+ docs.forEach(d => {
179
+ if (!d._id) return
180
+ if (existingDocs.found[d._id] && existingDocs.found[d._id]._rev !== d._rev)
181
+ revErrors.push(d._id)
182
+ if (existingDocs.notFound[d._id] && d._rev) revErrors.push(d._id)
183
+ })
184
+
185
+ if (revErrors.length > 0) {
186
+ throw new TransactionVersionConflictError(revErrors)
187
+ }
188
+ logger.debug('Checked document revisions:', existingDocs)
189
+ await emitter.emit('transaction-revs-checked', existingDocs)
190
+
191
+ const providedDocsById: Record<string, CouchDocInput> = {}
192
+ docs.forEach(d => {
193
+ if (!d._id) return
194
+ providedDocsById[d._id] = d
195
+ })
196
+
197
+ const newDocsToRollback: BulkSaveResponse = []
198
+ const potentialExistingDocsToRollback: BulkSaveResponse = []
199
+ const failedDocs: BulkSaveResponse = []
200
+
201
+ try {
202
+ logger.info('Transaction started:', transactionDoc)
203
+ await emitter.emit('transaction-started', transactionDoc)
204
+ // Apply updates
205
+ const results = await bulkSave(config, docs)
206
+ logger.info('Transaction updates applied:', results)
207
+ await emitter.emit('transaction-updates-applied', results)
208
+
209
+ // Check for failures
210
+ results.forEach(r => {
211
+ if (!r.id) return // not enough info
212
+ if (!r.error) {
213
+ if (existingDocs.notFound[r.id]) newDocsToRollback.push(r)
214
+ if (existingDocs.found[r.id]) potentialExistingDocsToRollback.push(r)
215
+ } else {
216
+ failedDocs.push(r)
217
+ }
218
+ })
219
+ if (failedDocs.length > 0) {
220
+ throw new TransactionBulkOperationError(failedDocs)
221
+ }
222
+
223
+ // Update transaction status to completed
224
+ transactionDoc.status = 'completed'
225
+ transactionDoc._rev = transactionResponse.rev
226
+ transactionResponse = await _put(transactionDoc)
227
+ logger.info('Transaction completed:', transactionDoc)
228
+ await emitter.emit('transaction-completed', {
229
+ transactionResponse,
230
+ transactionDoc
231
+ })
232
+ if (transactionResponse.statusCode !== 201) {
233
+ logger.error('Failed to update transaction status to completed')
234
+ }
235
+
236
+ return results
237
+ } catch (error) {
238
+ logger.error('Transaction failed, attempting rollback:', error)
239
+
240
+ // Rollback changes
241
+ const toRollback: CouchDoc[] = []
242
+ potentialExistingDocsToRollback.forEach(row => {
243
+ if (!row.id || !row.rev) return
244
+ const doc = existingDocs.found[row.id]
245
+ doc._rev = row.rev
246
+ toRollback.push(doc)
247
+ })
248
+ newDocsToRollback.forEach(d => {
249
+ if (!d.id || !d.rev) return
250
+ const before = JSON.parse(JSON.stringify(providedDocsById[d.id]))
251
+ before._rev = d.rev
252
+ before._deleted = true
253
+ toRollback.push(before)
254
+ })
255
+
256
+ // rollback all the changes
257
+ const bulkRollbackResult = await bulkSave(config, toRollback)
258
+ let status: TransactionStatus = 'rolled_back'
259
+ bulkRollbackResult.forEach(r => {
260
+ if (r.error) status = 'rollback_failed'
261
+ })
262
+ logger.warn('Transaction rolled back:', { bulkRollbackResult, status })
263
+ await emitter.emit('transaction-rolled-back', {
264
+ bulkRollbackResult,
265
+ status
266
+ })
267
+
268
+ // Update transaction status to rolled back
269
+ transactionDoc.status = status
270
+ transactionDoc._rev = transactionResponse.rev || null
271
+ transactionResponse = await _put(transactionDoc)
272
+ logger.warn('Transaction rollback status updated:', transactionDoc)
273
+ await emitter.emit('transaction-rolled-back-status', {
274
+ transactionResponse,
275
+ transactionDoc
276
+ })
277
+ if (transactionResponse.statusCode !== 201) {
278
+ logger.error('Failed to update transaction status to rolled_back')
279
+ }
280
+ throw new TransactionRollbackError(
281
+ 'Transaction failed and rollback was unsuccessful',
282
+ error as Error,
283
+ bulkRollbackResult
284
+ )
285
+ }
286
+ }