@rsktash/beads-ui 0.1.51 → 0.10.3

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 (336) hide show
  1. package/README.md +85 -144
  2. package/bin/bd-web +94 -0
  3. package/dist/assets/index-BBVIMXaM.js +73 -0
  4. package/dist/assets/index-SUXI6Mzt.css +1 -0
  5. package/dist/bd-favicon-16.svg +6 -0
  6. package/dist/bd-favicon-32.svg +6 -0
  7. package/dist/bd-logo-512.svg +6 -0
  8. package/dist/favicon.ico +0 -0
  9. package/dist/icon-180.png +0 -0
  10. package/dist/icon-192.png +0 -0
  11. package/dist/icon-512.png +0 -0
  12. package/dist/index.html +8 -5
  13. package/package.json +39 -29
  14. package/server/auth.js +68 -87
  15. package/server/db.js +91 -134
  16. package/server/dsn.js +130 -0
  17. package/server/index.js +151 -87
  18. package/server/queries.js +247 -0
  19. package/server/routes/auth.js +27 -0
  20. package/server/routes/issues.js +120 -0
  21. package/server/routes/stream.js +111 -0
  22. package/server/types.js +83 -0
  23. package/app/protocol.js +0 -216
  24. package/bin/bd-grep +0 -121
  25. package/bin/bd-ui +0 -19
  26. package/dist/assets/abap-DsBKuouk.js +0 -1
  27. package/dist/assets/actionscript-3-D_z4Izcz.js +0 -1
  28. package/dist/assets/ada-727ZlQH0.js +0 -1
  29. package/dist/assets/andromeeda-C3khCPGq.js +0 -1
  30. package/dist/assets/angular-html-LfdN0zeE.js +0 -1
  31. package/dist/assets/angular-ts-CKsD7JZE.js +0 -1
  32. package/dist/assets/apache-Dn00JSTd.js +0 -1
  33. package/dist/assets/apex-COJ4H7py.js +0 -1
  34. package/dist/assets/apl-BBq3IX1j.js +0 -1
  35. package/dist/assets/applescript-Bu5BbsvL.js +0 -1
  36. package/dist/assets/ara-7O62HKoU.js +0 -1
  37. package/dist/assets/asciidoc-BPT9niGB.js +0 -1
  38. package/dist/assets/asm-Dhn9LcZ4.js +0 -1
  39. package/dist/assets/astro-CqkE3fuf.js +0 -1
  40. package/dist/assets/aurora-x-D-2ljcwZ.js +0 -1
  41. package/dist/assets/awk-eg146-Ew.js +0 -1
  42. package/dist/assets/ayu-dark-Cv9koXgw.js +0 -1
  43. package/dist/assets/ballerina-Du268qiB.js +0 -1
  44. package/dist/assets/bat-fje9CFhw.js +0 -1
  45. package/dist/assets/beancount-BwXTMy5W.js +0 -1
  46. package/dist/assets/berry-3xVqZejG.js +0 -1
  47. package/dist/assets/bibtex-xW4inM5L.js +0 -1
  48. package/dist/assets/bicep-DHo0CJ0O.js +0 -1
  49. package/dist/assets/blade-a8OxSdnT.js +0 -1
  50. package/dist/assets/bsl-Dgyn0ogV.js +0 -1
  51. package/dist/assets/c-C3t2pwGQ.js +0 -1
  52. package/dist/assets/cadence-DNquZEk8.js +0 -1
  53. package/dist/assets/cairo--RitsXJZ.js +0 -1
  54. package/dist/assets/catppuccin-frappe-CD_QflpE.js +0 -1
  55. package/dist/assets/catppuccin-latte-DRW-0cLl.js +0 -1
  56. package/dist/assets/catppuccin-macchiato-C-_shW-Y.js +0 -1
  57. package/dist/assets/catppuccin-mocha-LGGdnPYs.js +0 -1
  58. package/dist/assets/clarity-BHOwM8T6.js +0 -1
  59. package/dist/assets/clojure-DxSadP1t.js +0 -1
  60. package/dist/assets/cmake-DbXoA79R.js +0 -1
  61. package/dist/assets/cobol-PTqiYgYu.js +0 -1
  62. package/dist/assets/codeowners-Bp6g37R7.js +0 -1
  63. package/dist/assets/codeql-sacFqUAJ.js +0 -1
  64. package/dist/assets/coffee-dyiR41kL.js +0 -1
  65. package/dist/assets/common-lisp-C7gG9l05.js +0 -1
  66. package/dist/assets/coq-Dsg_Bt_b.js +0 -1
  67. package/dist/assets/cpp-BksuvNSY.js +0 -1
  68. package/dist/assets/crystal-DtDmRg-F.js +0 -1
  69. package/dist/assets/csharp-D9R-vmeu.js +0 -1
  70. package/dist/assets/css-BPhBrDlE.js +0 -1
  71. package/dist/assets/csv-B0qRVHPH.js +0 -1
  72. package/dist/assets/cue-DtFQj3wx.js +0 -1
  73. package/dist/assets/cypher-m2LEI-9-.js +0 -1
  74. package/dist/assets/d-BoXegm-a.js +0 -1
  75. package/dist/assets/dark-plus-C3mMm8J8.js +0 -1
  76. package/dist/assets/dart-B9wLZaAG.js +0 -1
  77. package/dist/assets/dax-ClGRhx96.js +0 -1
  78. package/dist/assets/desktop-DEIpsLCJ.js +0 -1
  79. package/dist/assets/diff-BgYniUM_.js +0 -1
  80. package/dist/assets/docker-COcR7UxN.js +0 -1
  81. package/dist/assets/dotenv-BjQB5zDj.js +0 -1
  82. package/dist/assets/dracula-BzJJZx-M.js +0 -1
  83. package/dist/assets/dracula-soft-BXkSAIEj.js +0 -1
  84. package/dist/assets/dream-maker-C-nORZOA.js +0 -1
  85. package/dist/assets/edge-D5gP-w-T.js +0 -1
  86. package/dist/assets/elixir-CLiX3zqd.js +0 -1
  87. package/dist/assets/elm-CmHSxxaM.js +0 -1
  88. package/dist/assets/emacs-lisp-BX77sIaO.js +0 -1
  89. package/dist/assets/erb-BYTLMnw6.js +0 -1
  90. package/dist/assets/erlang-B-DoSBHF.js +0 -1
  91. package/dist/assets/everforest-dark-BgDCqdQA.js +0 -1
  92. package/dist/assets/everforest-light-C8M2exoo.js +0 -1
  93. package/dist/assets/fennel-bCA53EVm.js +0 -1
  94. package/dist/assets/fish-w-ucz2PV.js +0 -1
  95. package/dist/assets/fluent-Dayu4EKP.js +0 -1
  96. package/dist/assets/fortran-fixed-form-TqA4NnZg.js +0 -1
  97. package/dist/assets/fortran-free-form-DKXYxT9g.js +0 -1
  98. package/dist/assets/fsharp-XplgxFYe.js +0 -1
  99. package/dist/assets/gdresource-BHYsBjWJ.js +0 -1
  100. package/dist/assets/gdscript-DfxzS6Rs.js +0 -1
  101. package/dist/assets/gdshader-SKMF96pI.js +0 -1
  102. package/dist/assets/genie-ajMbGru0.js +0 -1
  103. package/dist/assets/gherkin--30QC5Em.js +0 -1
  104. package/dist/assets/git-commit-i4q6IMui.js +0 -1
  105. package/dist/assets/git-rebase-B-v9cOL2.js +0 -1
  106. package/dist/assets/github-dark-DHJKELXO.js +0 -1
  107. package/dist/assets/github-dark-default-Cuk6v7N8.js +0 -1
  108. package/dist/assets/github-dark-dimmed-DH5Ifo-i.js +0 -1
  109. package/dist/assets/github-dark-high-contrast-E3gJ1_iC.js +0 -1
  110. package/dist/assets/github-light-DAi9KRSo.js +0 -1
  111. package/dist/assets/github-light-default-D7oLnXFd.js +0 -1
  112. package/dist/assets/github-light-high-contrast-BfjtVDDH.js +0 -1
  113. package/dist/assets/gleam-B430Bg39.js +0 -1
  114. package/dist/assets/glimmer-js-D-cwc0-E.js +0 -1
  115. package/dist/assets/glimmer-ts-pgjy16dm.js +0 -1
  116. package/dist/assets/glsl-DBO2IWDn.js +0 -1
  117. package/dist/assets/gnuplot-CM8KxXT1.js +0 -1
  118. package/dist/assets/go-B1SYOhNW.js +0 -1
  119. package/dist/assets/graphql-cDcHW_If.js +0 -1
  120. package/dist/assets/groovy-DkBy-JyN.js +0 -1
  121. package/dist/assets/hack-D1yCygmZ.js +0 -1
  122. package/dist/assets/haml-B2EZWmdv.js +0 -1
  123. package/dist/assets/handlebars-BQGss363.js +0 -1
  124. package/dist/assets/haskell-BILxekzW.js +0 -1
  125. package/dist/assets/haxe-C5wWYbrZ.js +0 -1
  126. package/dist/assets/hcl-HzYwdGDm.js +0 -1
  127. package/dist/assets/hjson-T-Tgc4AT.js +0 -1
  128. package/dist/assets/hlsl-ifBTmRxC.js +0 -1
  129. package/dist/assets/houston-DnULxvSX.js +0 -1
  130. package/dist/assets/html-C2L_23MC.js +0 -1
  131. package/dist/assets/html-derivative-CSfWNPLT.js +0 -1
  132. package/dist/assets/http-FRrOvY1W.js +0 -1
  133. package/dist/assets/hxml-TIA70rKU.js +0 -1
  134. package/dist/assets/hy-BMj5Y0dO.js +0 -1
  135. package/dist/assets/imba-bv_oIlVt.js +0 -1
  136. package/dist/assets/index-CNDeKQGk.js +0 -125
  137. package/dist/assets/index-oO5WB2l2.css +0 -1
  138. package/dist/assets/ini-BjABl1g7.js +0 -1
  139. package/dist/assets/java-xI-RfyKK.js +0 -1
  140. package/dist/assets/javascript-ySlJ1b_l.js +0 -1
  141. package/dist/assets/jinja-DGy0s7-h.js +0 -1
  142. package/dist/assets/jison-BqZprYcd.js +0 -1
  143. package/dist/assets/json-BQoSv7ci.js +0 -1
  144. package/dist/assets/json5-w8dY5SsB.js +0 -1
  145. package/dist/assets/jsonc-TU54ms6u.js +0 -1
  146. package/dist/assets/jsonl-DREVFZK8.js +0 -1
  147. package/dist/assets/jsonnet-BfivnA6A.js +0 -1
  148. package/dist/assets/jssm-P4WzXJd0.js +0 -1
  149. package/dist/assets/jsx-BAng5TT0.js +0 -1
  150. package/dist/assets/julia-BBuGR-5E.js +0 -1
  151. package/dist/assets/kanagawa-dragon-CkXjmgJE.js +0 -1
  152. package/dist/assets/kanagawa-lotus-CfQXZHmo.js +0 -1
  153. package/dist/assets/kanagawa-wave-DWedfzmr.js +0 -1
  154. package/dist/assets/kotlin-B5lbUyaz.js +0 -1
  155. package/dist/assets/kusto-mebxcVVE.js +0 -1
  156. package/dist/assets/laserwave-DUszq2jm.js +0 -1
  157. package/dist/assets/latex-C-cWTeAZ.js +0 -1
  158. package/dist/assets/lean-XBlWyCtg.js +0 -1
  159. package/dist/assets/less-BfCpw3nA.js +0 -1
  160. package/dist/assets/light-plus-B7mTdjB0.js +0 -1
  161. package/dist/assets/liquid-D3W5UaiH.js +0 -1
  162. package/dist/assets/log-Cc5clBb7.js +0 -1
  163. package/dist/assets/logo-IuBKFhSY.js +0 -1
  164. package/dist/assets/lua-CvWAzNxB.js +0 -1
  165. package/dist/assets/luau-Du5NY7AG.js +0 -1
  166. package/dist/assets/make-Bvotw-X0.js +0 -1
  167. package/dist/assets/markdown-UIAJJxZW.js +0 -1
  168. package/dist/assets/marko-z0MBrx5-.js +0 -1
  169. package/dist/assets/material-theme-D5KoaKCx.js +0 -1
  170. package/dist/assets/material-theme-darker-BfHTSMKl.js +0 -1
  171. package/dist/assets/material-theme-lighter-B0m2ddpp.js +0 -1
  172. package/dist/assets/material-theme-ocean-CyktbL80.js +0 -1
  173. package/dist/assets/material-theme-palenight-Csfq5Kiy.js +0 -1
  174. package/dist/assets/matlab-D9-PGadD.js +0 -1
  175. package/dist/assets/mdc-DB_EDNY_.js +0 -1
  176. package/dist/assets/mdx-sdHcTMYB.js +0 -1
  177. package/dist/assets/mermaid-Ci6OQyBP.js +0 -1
  178. package/dist/assets/min-dark-CafNBF8u.js +0 -1
  179. package/dist/assets/min-light-CTRr51gU.js +0 -1
  180. package/dist/assets/mipsasm-BC5c_5Pe.js +0 -1
  181. package/dist/assets/mojo-Tz6hzZYG.js +0 -1
  182. package/dist/assets/monokai-D4h5O-jR.js +0 -1
  183. package/dist/assets/move-DB_GagMm.js +0 -1
  184. package/dist/assets/narrat-DLbgOhZU.js +0 -1
  185. package/dist/assets/nextflow-B0XVJmRM.js +0 -1
  186. package/dist/assets/nginx-D_VnBJ67.js +0 -1
  187. package/dist/assets/night-owl-C39BiMTA.js +0 -1
  188. package/dist/assets/nim-ZlGxZxc3.js +0 -1
  189. package/dist/assets/nix-shcSOmrb.js +0 -1
  190. package/dist/assets/nord-Ddv68eIx.js +0 -1
  191. package/dist/assets/nushell-D4Tzg5kh.js +0 -1
  192. package/dist/assets/objective-c-Deuh7S70.js +0 -1
  193. package/dist/assets/objective-cpp-BUEGK8hf.js +0 -1
  194. package/dist/assets/ocaml-BNioltXt.js +0 -1
  195. package/dist/assets/one-dark-pro-GBQ2dnAY.js +0 -1
  196. package/dist/assets/one-light-PoHY5YXO.js +0 -1
  197. package/dist/assets/pascal-JqZropPD.js +0 -1
  198. package/dist/assets/perl-CHQXSrWU.js +0 -1
  199. package/dist/assets/php-B5ebYQev.js +0 -1
  200. package/dist/assets/plastic-3e1v2bzS.js +0 -1
  201. package/dist/assets/plsql-LKU2TuZ1.js +0 -1
  202. package/dist/assets/po-BFLt1xDp.js +0 -1
  203. package/dist/assets/poimandres-CS3Unz2-.js +0 -1
  204. package/dist/assets/polar-DKykz6zU.js +0 -1
  205. package/dist/assets/postcss-B3ZDOciz.js +0 -1
  206. package/dist/assets/powerquery-CSHBycmS.js +0 -1
  207. package/dist/assets/powershell-BIEUsx6d.js +0 -1
  208. package/dist/assets/prisma-B48N-Iqd.js +0 -1
  209. package/dist/assets/prolog-BY-TUvya.js +0 -1
  210. package/dist/assets/proto-zocC4JxJ.js +0 -1
  211. package/dist/assets/pug-CM9l7STV.js +0 -1
  212. package/dist/assets/puppet-Cza_XSSt.js +0 -1
  213. package/dist/assets/purescript-Bg-kzb6g.js +0 -1
  214. package/dist/assets/python-DhUJRlN_.js +0 -1
  215. package/dist/assets/qml-D8XfuvdV.js +0 -1
  216. package/dist/assets/qmldir-C8lEn-DE.js +0 -1
  217. package/dist/assets/qss-DhMKtDLN.js +0 -1
  218. package/dist/assets/r-CwjWoCRV.js +0 -1
  219. package/dist/assets/racket-CzouJOBO.js +0 -1
  220. package/dist/assets/raku-B1bQXN8T.js +0 -1
  221. package/dist/assets/razor-CNLDkMZG.js +0 -1
  222. package/dist/assets/red-bN70gL4F.js +0 -1
  223. package/dist/assets/reg-5LuOXUq_.js +0 -1
  224. package/dist/assets/regexp-DWJ3fJO_.js +0 -1
  225. package/dist/assets/rel-DJlmqQ1C.js +0 -1
  226. package/dist/assets/riscv-QhoSD0DR.js +0 -1
  227. package/dist/assets/rose-pine-CmCqftbK.js +0 -1
  228. package/dist/assets/rose-pine-dawn-Ds-gbosJ.js +0 -1
  229. package/dist/assets/rose-pine-moon-CjDtw9vr.js +0 -1
  230. package/dist/assets/rst-4NLicBqY.js +0 -1
  231. package/dist/assets/ruby-DeZ3UC14.js +0 -1
  232. package/dist/assets/rust-Be6lgOlo.js +0 -1
  233. package/dist/assets/sas-BmTFh92c.js +0 -1
  234. package/dist/assets/sass-BJ4Li9vH.js +0 -1
  235. package/dist/assets/scala-DQVVAn-B.js +0 -1
  236. package/dist/assets/scheme-BJGe-b2p.js +0 -1
  237. package/dist/assets/scss-C31hgJw-.js +0 -1
  238. package/dist/assets/sdbl-BLhTXw86.js +0 -1
  239. package/dist/assets/shaderlab-B7qAK45m.js +0 -1
  240. package/dist/assets/shellscript-atvbtKCR.js +0 -1
  241. package/dist/assets/shellsession-C_rIy8kc.js +0 -1
  242. package/dist/assets/slack-dark-BthQWCQV.js +0 -1
  243. package/dist/assets/slack-ochin-DqwNpetd.js +0 -1
  244. package/dist/assets/smalltalk-DkLiglaE.js +0 -1
  245. package/dist/assets/snazzy-light-Bw305WKR.js +0 -1
  246. package/dist/assets/solarized-dark-DXbdFlpD.js +0 -1
  247. package/dist/assets/solarized-light-L9t79GZl.js +0 -1
  248. package/dist/assets/solidity-C1w2a3ep.js +0 -1
  249. package/dist/assets/soy-C-lX7w71.js +0 -1
  250. package/dist/assets/sparql-bYkjHRlG.js +0 -1
  251. package/dist/assets/splunk-Cf8iN4DR.js +0 -1
  252. package/dist/assets/sql-COK4E0Yg.js +0 -1
  253. package/dist/assets/ssh-config-BknIz3MU.js +0 -1
  254. package/dist/assets/stata-DorPZHa4.js +0 -1
  255. package/dist/assets/stylus-BeQkCIfX.js +0 -1
  256. package/dist/assets/svelte-MSaWC3Je.js +0 -1
  257. package/dist/assets/swift-BSxZ-RaX.js +0 -1
  258. package/dist/assets/synthwave-84-CbfX1IO0.js +0 -1
  259. package/dist/assets/system-verilog-C7L56vO4.js +0 -1
  260. package/dist/assets/systemd-CUnW07Te.js +0 -1
  261. package/dist/assets/talonscript-C1XDQQGZ.js +0 -1
  262. package/dist/assets/tasl-CQjiPCtT.js +0 -1
  263. package/dist/assets/tcl-DQ1-QYvQ.js +0 -1
  264. package/dist/assets/templ-dwX3ZSMB.js +0 -1
  265. package/dist/assets/terraform-BbSNqyBO.js +0 -1
  266. package/dist/assets/tex-rYs2v40G.js +0 -1
  267. package/dist/assets/tokyo-night-DBQeEorK.js +0 -1
  268. package/dist/assets/toml-CB2ApiWb.js +0 -1
  269. package/dist/assets/ts-tags-CipyTH0X.js +0 -1
  270. package/dist/assets/tsv-B_m7g4N7.js +0 -1
  271. package/dist/assets/tsx-B6W0miNI.js +0 -1
  272. package/dist/assets/turtle-BMR_PYu6.js +0 -1
  273. package/dist/assets/twig-NC5TFiHP.js +0 -1
  274. package/dist/assets/typescript-Dj6nwHGl.js +0 -1
  275. package/dist/assets/typespec-BpWG_bgh.js +0 -1
  276. package/dist/assets/typst-BVUVsWT6.js +0 -1
  277. package/dist/assets/v-CAQ2eGtk.js +0 -1
  278. package/dist/assets/vala-BFOHcciG.js +0 -1
  279. package/dist/assets/vb-CdO5JTpU.js +0 -1
  280. package/dist/assets/verilog-CJaU5se_.js +0 -1
  281. package/dist/assets/vesper-BEBZ7ncR.js +0 -1
  282. package/dist/assets/vhdl-DYoNaHQp.js +0 -1
  283. package/dist/assets/viml-m4uW47V2.js +0 -1
  284. package/dist/assets/vitesse-black-Bkuqu6BP.js +0 -1
  285. package/dist/assets/vitesse-dark-D0r3Knsf.js +0 -1
  286. package/dist/assets/vitesse-light-CVO1_9PV.js +0 -1
  287. package/dist/assets/vue-BuYVFjOK.js +0 -1
  288. package/dist/assets/vue-html-xdeiXROB.js +0 -1
  289. package/dist/assets/vyper-nyqBNV6O.js +0 -1
  290. package/dist/assets/wasm-C6j12Q_x.js +0 -1
  291. package/dist/assets/wasm-CG6Dc4jp.js +0 -1
  292. package/dist/assets/wenyan-7A4Fjokl.js +0 -1
  293. package/dist/assets/wgsl-CB0Krxn9.js +0 -1
  294. package/dist/assets/wikitext-DCE3LsBG.js +0 -1
  295. package/dist/assets/wolfram-C3FkfJm5.js +0 -1
  296. package/dist/assets/xml-e3z08dGr.js +0 -1
  297. package/dist/assets/xsl-Dd0NUgwM.js +0 -1
  298. package/dist/assets/yaml-CVw76BM1.js +0 -1
  299. package/dist/assets/zenscript-HnGAYVZD.js +0 -1
  300. package/dist/assets/zig-BVz_zdnA.js +0 -1
  301. package/server/app.js +0 -165
  302. package/server/app.test.js +0 -30
  303. package/server/bd.js +0 -227
  304. package/server/bd.test.js +0 -194
  305. package/server/cli/cli.test.js +0 -207
  306. package/server/cli/commands.integration.test.js +0 -148
  307. package/server/cli/commands.js +0 -285
  308. package/server/cli/commands.unit.test.js +0 -408
  309. package/server/cli/daemon.js +0 -340
  310. package/server/cli/daemon.test.js +0 -31
  311. package/server/cli/index.js +0 -135
  312. package/server/cli/open.js +0 -178
  313. package/server/cli/open.test.js +0 -26
  314. package/server/cli/usage.js +0 -49
  315. package/server/config.js +0 -36
  316. package/server/db.test.js +0 -169
  317. package/server/dolt-pool.js +0 -313
  318. package/server/dolt-queries.js +0 -781
  319. package/server/list-adapters.js +0 -421
  320. package/server/list-adapters.test.js +0 -208
  321. package/server/logging.js +0 -23
  322. package/server/registry-watcher.js +0 -200
  323. package/server/subscriptions.js +0 -299
  324. package/server/subscriptions.test.js +0 -128
  325. package/server/validators.js +0 -124
  326. package/server/watcher.js +0 -139
  327. package/server/watcher.test.js +0 -120
  328. package/server/ws.comments.test.js +0 -262
  329. package/server/ws.delete.test.js +0 -119
  330. package/server/ws.js +0 -1329
  331. package/server/ws.labels.test.js +0 -95
  332. package/server/ws.list-refresh.coalesce.test.js +0 -95
  333. package/server/ws.list-subscriptions.test.js +0 -403
  334. package/server/ws.mutation-window.test.js +0 -147
  335. package/server/ws.mutations.test.js +0 -389
  336. package/server/ws.test.js +0 -52
@@ -1,781 +0,0 @@
1
- /**
2
- * Direct SQL queries against Dolt, replacing bd CLI subprocess calls.
3
- * Each query returns data in the same shape as the bd CLI JSON output
4
- * so callers don't need to change.
5
- */
6
- import { getPool } from './dolt-pool.js';
7
- import { debug } from './logging.js';
8
-
9
- const log = debug('dolt-queries');
10
-
11
- // Columns selected for list views (omit large text fields for performance).
12
- // Note: 'parent' is NOT a real column — it's derived from dependencies table.
13
- const LIST_COL_NAMES = [
14
- 'id', 'title', 'status', 'priority', 'issue_type', 'assignee',
15
- 'created_at', 'created_by', 'updated_at', 'closed_at', 'close_reason',
16
- 'description', 'owner', 'estimated_minutes', 'external_ref', 'spec_id',
17
- 'ephemeral', 'pinned', 'is_template', 'mol_type', 'work_type',
18
- 'source_system', 'source_repo'
19
- ];
20
-
21
- // Prefixed with table alias for JOINed queries
22
- const LIST_COLS_ALIASED = LIST_COL_NAMES.map(c => `i.${c}`).join(', ');
23
- const LIST_COLS = LIST_COL_NAMES.join(', ');
24
-
25
- // All columns for detail view (single-issue fetch includes text fields)
26
- const DETAIL_COLS = '*';
27
-
28
- /**
29
- * Check if the Dolt pool is available.
30
- *
31
- * @returns {boolean}
32
- */
33
- export function isDoltPoolReady() {
34
- return getPool() !== null;
35
- }
36
-
37
- /**
38
- * Normalize a Dolt datetime row to the format bd CLI returns.
39
- * bd returns ISO strings; Dolt with dateStrings returns 'YYYY-MM-DD HH:MM:SS'.
40
- *
41
- * @param {Record<string, unknown>} row
42
- * @returns {Record<string, unknown>}
43
- */
44
- function normalizeRow(row) {
45
- const out = { ...row };
46
- for (const key of ['created_at', 'updated_at', 'closed_at', 'last_activity', 'compacted_at', 'due_at', 'defer_until']) {
47
- if (out[key] && typeof out[key] === 'string') {
48
- // Convert '2026-04-05 01:50:19' → '2026-04-05T01:50:19Z'
49
- const v = /** @type {string} */ (out[key]);
50
- if (v.includes(' ') && !v.includes('T')) {
51
- out[key] = v.replace(' ', 'T') + 'Z';
52
- }
53
- }
54
- }
55
- // Ensure boolean fields are numbers (Dolt returns 0/1)
56
- for (const key of ['ephemeral', 'pinned', 'is_template', 'no_history']) {
57
- if (key in out) {
58
- out[key] = out[key] ? 1 : 0;
59
- }
60
- }
61
- return out;
62
- }
63
-
64
- /**
65
- * @typedef {{ limit?: number, offset?: number }} Pagination
66
- * @typedef {{ ok: true, items: Array<Record<string, unknown>>, total: number }} PaginatedResult
67
- * @typedef {{ ok: false, error: { code: string, message: string } }} QueryError
68
- */
69
-
70
- /**
71
- * Build SQL LIMIT/OFFSET clause from pagination params.
72
- *
73
- * @param {Pagination} [pagination]
74
- * @returns {{ limitClause: string }}
75
- */
76
- function buildPagination(pagination) {
77
- const limit = pagination?.limit || 0;
78
- const offset = pagination?.offset || 0;
79
- const limitClause = limit > 0 ? ` LIMIT ${Number(limit)} OFFSET ${Number(offset)}` : '';
80
- return { limitClause };
81
- }
82
-
83
- /**
84
- * Enrich list items with parent_title, children counts, and blocked_by.
85
- * Runs batch queries to avoid N+1.
86
- *
87
- * @param {Array<Record<string, unknown>>} items
88
- * @returns {Promise<Array<Record<string, unknown>>>}
89
- */
90
- export async function enrichListItems(items) {
91
- const pool = getPool();
92
- if (!pool || items.length === 0) {
93
- log('enrichListItems skip: pool=%s items=%d', !!pool, items.length);
94
- return items;
95
- }
96
-
97
- const ids = items.map(i => String(i.id));
98
- log('enrichListItems: enriching %d items', ids.length);
99
-
100
- try {
101
- // 1. Resolve parent IDs — use the `parent` field already present from the
102
- // LEFT JOIN in query functions, falling back to a dependencies lookup.
103
- /** @type {Map<string, string>} */
104
- const parentIdMap = new Map();
105
- for (const item of items) {
106
- const p = item.parent ?? item.parent_id;
107
- if (p && typeof p === 'string' && p.length > 0) {
108
- parentIdMap.set(String(item.id), p);
109
- }
110
- }
111
-
112
- // If items didn't carry a `parent` column, query dependencies table
113
- if (parentIdMap.size === 0 && ids.length > 0) {
114
- const [parentDepRows] = await pool.query(
115
- `SELECT issue_id, depends_on_id FROM dependencies
116
- WHERE type = 'parent-child' AND issue_id IN (${ids.map(() => '?').join(',')})`,
117
- ids
118
- );
119
- for (const r of /** @type {any[]} */ (parentDepRows)) {
120
- parentIdMap.set(r.issue_id, r.depends_on_id);
121
- }
122
- }
123
- log('enrichListItems: %d parent mappings found', parentIdMap.size);
124
-
125
- // 2. Batch parent titles
126
- const parentIds = [...new Set(parentIdMap.values())];
127
- /** @type {Map<string, string>} */
128
- const parentTitles = new Map();
129
- if (parentIds.length > 0) {
130
- const [rows] = await pool.query(
131
- `SELECT id, title FROM issues WHERE id IN (${parentIds.map(() => '?').join(',')})`,
132
- parentIds
133
- );
134
- for (const r of /** @type {any[]} */ (rows)) parentTitles.set(r.id, r.title);
135
- }
136
-
137
- // 3. Batch children counts
138
- /** @type {Map<string, { total: number, closed: number }>} */
139
- const childCounts = new Map();
140
- if (ids.length > 0) {
141
- const [childRows] = await pool.query(
142
- `SELECT depends_on_id AS pid, COUNT(*) AS total,
143
- SUM(CASE WHEN i.status = 'closed' THEN 1 ELSE 0 END) AS closed
144
- FROM dependencies d JOIN issues i ON i.id = d.issue_id
145
- WHERE d.type = 'parent-child' AND d.depends_on_id IN (${ids.map(() => '?').join(',')})
146
- GROUP BY d.depends_on_id`,
147
- ids
148
- );
149
- for (const r of /** @type {any[]} */ (childRows)) {
150
- childCounts.set(r.pid, { total: Number(r.total), closed: Number(r.closed) });
151
- }
152
- }
153
- log('enrichListItems: %d items have children', childCounts.size);
154
-
155
- // 4. Batch blocked-by (open issues that block each item)
156
- /** @type {Map<string, Array<{ id: string, title: string }>>} */
157
- const blockedBy = new Map();
158
- if (ids.length > 0) {
159
- const [blockRows] = await pool.query(
160
- `SELECT d.issue_id, d.depends_on_id, i.title AS blocker_title
161
- FROM dependencies d JOIN issues i ON i.id = d.depends_on_id
162
- WHERE d.type = 'blocks' AND d.issue_id IN (${ids.map(() => '?').join(',')})
163
- AND i.status != 'closed'`,
164
- ids
165
- );
166
- for (const r of /** @type {any[]} */ (blockRows)) {
167
- if (!blockedBy.has(r.issue_id)) blockedBy.set(r.issue_id, []);
168
- blockedBy.get(r.issue_id).push({ id: r.depends_on_id, title: r.blocker_title });
169
- }
170
- }
171
- log('enrichListItems: %d items have blockers', blockedBy.size);
172
-
173
- // 5. Batch comment counts
174
- /** @type {Map<string, number>} */
175
- const commentCounts = new Map();
176
- if (ids.length > 0) {
177
- const [commentRows] = await pool.query(
178
- `SELECT issue_id, COUNT(*) AS cnt FROM comments
179
- WHERE issue_id IN (${ids.map(() => '?').join(',')})
180
- GROUP BY issue_id`,
181
- ids
182
- );
183
- for (const r of /** @type {any[]} */ (commentRows)) {
184
- commentCounts.set(r.issue_id, Number(r.cnt));
185
- }
186
- }
187
- log('enrichListItems: %d items have comments', commentCounts.size);
188
-
189
- return items.map(item => {
190
- const id = String(item.id);
191
- const enriched = { ...item };
192
- const pid = parentIdMap.get(id);
193
- if (pid) {
194
- enriched.parent_id = pid;
195
- if (parentTitles.has(pid)) enriched.parent_title = parentTitles.get(pid);
196
- }
197
- const cc = childCounts.get(id);
198
- if (cc) {
199
- enriched.total_children = cc.total;
200
- enriched.closed_children = cc.closed;
201
- }
202
- const bb = blockedBy.get(id);
203
- if (bb && bb.length > 0) {
204
- enriched.blocked_by = bb;
205
- }
206
- enriched.comment_count = commentCounts.get(id) || 0;
207
- return enriched;
208
- });
209
- } catch (err) {
210
- log('enrichListItems error: %o', err);
211
- return items;
212
- }
213
- }
214
-
215
- /**
216
- * Fetch total count for a WHERE clause.
217
- *
218
- * @param {import('mysql2/promise').Pool} pool
219
- * @param {string} where - SQL WHERE clause (without WHERE keyword), or empty for all
220
- * @param {any[]} [params]
221
- * @returns {Promise<number>}
222
- */
223
- async function fetchTotal(pool, where, params = []) {
224
- const sql = where
225
- ? `SELECT COUNT(*) AS total FROM issues WHERE ${where}`
226
- : `SELECT COUNT(*) AS total FROM issues`;
227
- const [rows] = await pool.query(sql, params);
228
- return /** @type {any[]} */ (rows)[0]?.total || 0;
229
- }
230
-
231
- /**
232
- * Fetch all issues with pagination (for 'all-issues' subscription).
233
- *
234
- * @param {Pagination} [pagination]
235
- * @returns {Promise<PaginatedResult | QueryError>}
236
- */
237
- export async function queryAllIssues(pagination) {
238
- const pool = getPool();
239
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
240
- try {
241
- const total = await fetchTotal(pool, '');
242
- const { limitClause } = buildPagination(pagination);
243
- const [rows] = await pool.query(
244
- `SELECT ${LIST_COLS_ALIASED}, d.depends_on_id AS parent
245
- FROM issues i
246
- LEFT JOIN dependencies d ON d.issue_id = i.id AND d.type = 'parent-child'
247
- ORDER BY i.updated_at DESC${limitClause}`
248
- );
249
- return { ok: true, items: /** @type {any[]} */ (rows).map(normalizeRow), total };
250
- } catch (err) {
251
- log('queryAllIssues error: %o', err);
252
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
253
- }
254
- }
255
-
256
- /**
257
- * Fetch epics (for 'epics' subscription).
258
- *
259
- * @returns {Promise<{ ok: true, items: Array<Record<string, unknown>> } | { ok: false, error: { code: string, message: string } }>}
260
- */
261
- /**
262
- * @param {{ limit?: number, offset?: number }} [pagination]
263
- * @returns {Promise<PaginatedResult | QueryError>}
264
- */
265
- export async function queryEpics(pagination) {
266
- const pool = getPool();
267
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
268
- try {
269
- const [countRows] = await pool.query(`SELECT COUNT(*) AS total FROM issues WHERE issue_type = 'epic'`);
270
- const total = /** @type {any[]} */ (countRows)[0]?.total || 0;
271
- const { limitClause } = buildPagination(pagination);
272
- const [rows] = await pool.query(
273
- `SELECT ${LIST_COLS_ALIASED}, d.depends_on_id AS parent
274
- FROM issues i
275
- LEFT JOIN dependencies d ON d.issue_id = i.id AND d.type = 'parent-child'
276
- WHERE i.issue_type = 'epic'
277
- ORDER BY i.updated_at DESC${limitClause}`
278
- );
279
- return { ok: true, items: /** @type {any[]} */ (rows).map(normalizeRow), total };
280
- } catch (err) {
281
- log('queryEpics error: %o', err);
282
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
283
- }
284
- }
285
-
286
- /**
287
- * Fetch blocked issues (for 'blocked-issues' subscription).
288
- *
289
- * @returns {Promise<{ ok: true, items: Array<Record<string, unknown>> } | { ok: false, error: { code: string, message: string } }>}
290
- */
291
- /**
292
- * @param {Pagination} [pagination]
293
- * @returns {Promise<PaginatedResult | QueryError>}
294
- */
295
- export async function queryBlockedIssues(pagination) {
296
- const pool = getPool();
297
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
298
- try {
299
- const blockedWhere = `status = 'open' AND id IN (
300
- SELECT bl.issue_id FROM dependencies bl
301
- JOIN issues blocker ON bl.depends_on_id = blocker.id
302
- WHERE bl.type = 'blocks' AND blocker.status != 'closed')`;
303
- const total = await fetchTotal(pool, blockedWhere);
304
- const { limitClause } = buildPagination(pagination);
305
- const [rows] = await pool.query(
306
- `SELECT ${LIST_COLS_ALIASED}, pc.depends_on_id AS parent
307
- FROM issues i
308
- LEFT JOIN dependencies pc ON pc.issue_id = i.id AND pc.type = 'parent-child'
309
- WHERE i.status = 'open'
310
- AND i.id IN (
311
- SELECT bl.issue_id FROM dependencies bl
312
- JOIN issues blocker ON bl.depends_on_id = blocker.id
313
- WHERE bl.type = 'blocks' AND blocker.status != 'closed'
314
- )
315
- ORDER BY i.updated_at DESC${limitClause}`
316
- );
317
- return { ok: true, items: /** @type {any[]} */ (rows).map(normalizeRow), total };
318
- } catch (err) {
319
- log('queryBlockedIssues error: %o', err);
320
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
321
- }
322
- }
323
-
324
- /**
325
- * Fetch ready issues (for 'ready-issues' subscription).
326
- *
327
- * @returns {Promise<{ ok: true, items: Array<Record<string, unknown>> } | { ok: false, error: { code: string, message: string } }>}
328
- */
329
- /**
330
- * @param {Pagination} [pagination]
331
- * @returns {Promise<PaginatedResult | QueryError>}
332
- */
333
- export async function queryReadyIssues(pagination) {
334
- const pool = getPool();
335
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
336
- try {
337
- const readyWhere = `status = 'open' AND id NOT IN (
338
- SELECT bl.issue_id FROM dependencies bl
339
- JOIN issues blocker ON bl.depends_on_id = blocker.id
340
- WHERE bl.type = 'blocks' AND blocker.status != 'closed')`;
341
- const total = await fetchTotal(pool, readyWhere);
342
- const { limitClause } = buildPagination(pagination);
343
- const [rows] = await pool.query(
344
- `SELECT ${LIST_COLS_ALIASED}, pc.depends_on_id AS parent
345
- FROM issues i
346
- LEFT JOIN dependencies pc ON pc.issue_id = i.id AND pc.type = 'parent-child'
347
- WHERE i.status = 'open'
348
- AND i.id NOT IN (
349
- SELECT bl.issue_id FROM dependencies bl
350
- JOIN issues blocker ON bl.depends_on_id = blocker.id
351
- WHERE bl.type = 'blocks' AND blocker.status != 'closed'
352
- )
353
- ORDER BY i.updated_at DESC${limitClause}`
354
- );
355
- return { ok: true, items: /** @type {any[]} */ (rows).map(normalizeRow), total };
356
- } catch (err) {
357
- log('queryReadyIssues error: %o', err);
358
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
359
- }
360
- }
361
-
362
- /**
363
- * Fetch issues by status (for 'in-progress-issues', 'closed-issues' subscriptions).
364
- *
365
- * @param {string} status
366
- * @param {number} [limit]
367
- * @returns {Promise<{ ok: true, items: Array<Record<string, unknown>> } | { ok: false, error: { code: string, message: string } }>}
368
- */
369
- /**
370
- * @param {string} status
371
- * @param {Pagination} [pagination]
372
- * @returns {Promise<PaginatedResult | QueryError>}
373
- */
374
- export async function queryIssuesByStatus(status, pagination) {
375
- const pool = getPool();
376
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
377
- try {
378
- const total = await fetchTotal(pool, 'status = ?', [status]);
379
- const { limitClause } = buildPagination(pagination);
380
- const [rows] = await pool.query(
381
- `SELECT ${LIST_COLS_ALIASED}, pc.depends_on_id AS parent
382
- FROM issues i
383
- LEFT JOIN dependencies pc ON pc.issue_id = i.id AND pc.type = 'parent-child'
384
- WHERE i.status = ?
385
- ORDER BY i.updated_at DESC${limitClause}`,
386
- [status]
387
- );
388
- return { ok: true, items: /** @type {any[]} */ (rows).map(normalizeRow), total };
389
- } catch (err) {
390
- log('queryIssuesByStatus error: %o', err);
391
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
392
- }
393
- }
394
-
395
- /**
396
- * Search issues with optional FULLTEXT query and status/type filters.
397
- *
398
- * When `query` is non-empty, uses Dolt FULLTEXT MATCH...AGAINST for
399
- * relevance-ranked results. Falls back to LIKE on id for exact ID matches.
400
- * Status and type filters narrow results server-side.
401
- *
402
- * @param {string} query - Search terms (empty string = no text filter)
403
- * @param {Pagination & { status?: string, type?: string }} [options]
404
- * @returns {Promise<PaginatedResult | QueryError>}
405
- */
406
- export async function querySearchIssues(query, options) {
407
- const pool = getPool();
408
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
409
- try {
410
- const conditions = [];
411
- const params = [];
412
-
413
- // FULLTEXT search on title + description
414
- if (query.length > 0) {
415
- // Use MATCH...AGAINST for natural language search, plus LIKE on id for exact ID matches
416
- conditions.push('(MATCH(i.title, i.description) AGAINST (?) OR i.id LIKE ?)');
417
- params.push(query, `%${query}%`);
418
- }
419
-
420
- // Status filter
421
- if (options?.status && options.status !== 'all') {
422
- conditions.push('i.status = ?');
423
- params.push(options.status);
424
- }
425
-
426
- // Type filter
427
- if (options?.type && options.type !== 'all') {
428
- conditions.push('i.issue_type = ?');
429
- params.push(options.type);
430
- }
431
-
432
- const where = conditions.length > 0 ? conditions.join(' AND ') : '1=1';
433
-
434
- const [countRows] = await pool.query(
435
- `SELECT COUNT(*) AS total FROM issues i WHERE ${where}`, params
436
- );
437
- const total = /** @type {any[]} */ (countRows)[0]?.total || 0;
438
-
439
- const { limitClause } = buildPagination(options);
440
- const [rows] = await pool.query(
441
- `SELECT ${LIST_COLS_ALIASED}, pc.depends_on_id AS parent
442
- FROM issues i
443
- LEFT JOIN dependencies pc ON pc.issue_id = i.id AND pc.type = 'parent-child'
444
- WHERE ${where}
445
- ORDER BY i.updated_at DESC${limitClause}`,
446
- params
447
- );
448
- return { ok: true, items: /** @type {any[]} */ (rows).map(normalizeRow), total };
449
- } catch (err) {
450
- log('querySearchIssues error: %o', err);
451
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
452
- }
453
- }
454
-
455
- /**
456
- * Fetch a single issue with dependencies, labels and parent info
457
- * (for 'issue-detail' subscription and post-mutation show).
458
- *
459
- * @param {string} id
460
- * @returns {Promise<{ ok: true, item: Record<string, unknown> } | { ok: false, error: { code: string, message: string } }>}
461
- */
462
- export async function queryIssueDetail(id) {
463
- const pool = getPool();
464
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
465
- try {
466
- const [issueRows] = await pool.query(
467
- `SELECT ${DETAIL_COLS} FROM issues WHERE id = ?`, [id]
468
- );
469
- const issues = /** @type {any[]} */ (issueRows);
470
- if (issues.length === 0) {
471
- return { ok: false, error: { code: 'not_found', message: `Issue ${id} not found` } };
472
- }
473
-
474
- const issue = normalizeRow(issues[0]);
475
-
476
- // Fetch dependencies
477
- const [depRows] = await pool.query(
478
- `SELECT issue_id, depends_on_id, type, created_at, created_by, metadata
479
- FROM dependencies WHERE issue_id = ? OR depends_on_id = ?`, [id, id]
480
- );
481
- issue.dependencies = /** @type {any[]} */ (depRows).map(normalizeRow);
482
-
483
- // Derive parent from parent-child dependency (with context)
484
- const parentDep = /** @type {any[]} */ (depRows).find(
485
- (d) => d.issue_id === id && d.type === 'parent-child'
486
- );
487
- if (parentDep) {
488
- issue.parent_id = parentDep.depends_on_id;
489
- issue.parent = parentDep.depends_on_id;
490
- // Fetch parent title/status for sidebar context
491
- const [parentRows] = await pool.query(
492
- `SELECT id, title, status, issue_type FROM issues WHERE id = ?`,
493
- [parentDep.depends_on_id]
494
- );
495
- const parentIssues = /** @type {any[]} */ (parentRows);
496
- if (parentIssues.length > 0) {
497
- issue.parent_title = parentIssues[0].title;
498
- issue.parent_status = parentIssues[0].status;
499
- issue.parent_type = parentIssues[0].issue_type;
500
- }
501
- }
502
-
503
- // Derive dependency/dependent counts
504
- issue.dependency_count = /** @type {any[]} */ (depRows).filter(
505
- (d) => d.issue_id === id && d.type === 'blocks'
506
- ).length;
507
- issue.dependent_count = /** @type {any[]} */ (depRows).filter(
508
- (d) => d.depends_on_id === id && d.type === 'blocks'
509
- ).length;
510
-
511
- // Fetch labels
512
- const [labelRows] = await pool.query(
513
- `SELECT label FROM labels WHERE issue_id = ?`, [id]
514
- );
515
- issue.labels = /** @type {any[]} */ (labelRows).map((r) => r.label);
516
-
517
- // Fetch children (issues that have a parent-child dep pointing to this issue)
518
- const [childRows] = await pool.query(
519
- `SELECT i.id, i.title, i.status, i.priority, i.issue_type, i.assignee
520
- FROM dependencies d
521
- JOIN issues i ON i.id = d.issue_id
522
- WHERE d.depends_on_id = ? AND d.type = 'parent-child'
523
- ORDER BY i.created_at ASC`, [id]
524
- );
525
- const children = /** @type {any[]} */ (childRows).map(normalizeRow);
526
- if (children.length > 0) {
527
- issue.dependents = children;
528
- issue.total_children = children.length;
529
- issue.closed_children = children.filter((c) => c.status === 'closed').length;
530
- }
531
-
532
- // Fetch comments
533
- const [commentRows] = await pool.query(
534
- `SELECT id, issue_id, author, text, created_at FROM comments
535
- WHERE issue_id = ? ORDER BY created_at ASC`, [id]
536
- );
537
- const comments = /** @type {any[]} */ (commentRows).map(normalizeRow);
538
- issue.comments = comments;
539
- issue.comment_count = comments.length;
540
-
541
- return { ok: true, item: issue };
542
- } catch (err) {
543
- log('queryIssueDetail error: %o', err);
544
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
545
- }
546
- }
547
-
548
- /**
549
- * Fetch comments for an issue.
550
- *
551
- * @param {string} issueId
552
- * @returns {Promise<{ ok: true, items: Array<Record<string, unknown>> } | { ok: false, error: { code: string, message: string } }>}
553
- */
554
- export async function queryComments(issueId) {
555
- const pool = getPool();
556
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
557
- try {
558
- const [rows] = await pool.query(
559
- `SELECT id, issue_id, author, text, created_at FROM comments
560
- WHERE issue_id = ? ORDER BY created_at ASC`, [issueId]
561
- );
562
- return { ok: true, items: /** @type {any[]} */ (rows).map(normalizeRow) };
563
- } catch (err) {
564
- log('queryComments error: %o', err);
565
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
566
- }
567
- }
568
-
569
- /**
570
- * Update a single field on an issue.
571
- *
572
- * @param {string} id
573
- * @param {string} field - SQL column name
574
- * @param {string | number | null} value
575
- * @returns {Promise<{ ok: true } | { ok: false, error: { code: string, message: string } }>}
576
- */
577
- export async function updateIssueField(id, field, value) {
578
- const pool = getPool();
579
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
580
-
581
- // Whitelist allowed columns to prevent SQL injection
582
- const ALLOWED_FIELDS = new Set([
583
- 'title', 'description', 'design', 'acceptance_criteria', 'notes',
584
- 'status', 'priority', 'assignee', 'issue_type'
585
- ]);
586
- if (!ALLOWED_FIELDS.has(field)) {
587
- return { ok: false, error: { code: 'bad_request', message: `Field '${field}' not allowed` } };
588
- }
589
-
590
- try {
591
- const now = new Date().toISOString().replace('T', ' ').replace('Z', '');
592
- // Handle status=closed → set closed_at
593
- if (field === 'status' && value === 'closed') {
594
- await pool.query(
595
- `UPDATE issues SET status = 'closed', closed_at = ?, updated_at = ? WHERE id = ?`,
596
- [now, now, id]
597
- );
598
- } else if (field === 'status' && value !== 'closed') {
599
- // Reopening: clear closed_at
600
- await pool.query(
601
- `UPDATE issues SET status = ?, closed_at = NULL, updated_at = ? WHERE id = ?`,
602
- [value, now, id]
603
- );
604
- } else {
605
- await pool.query(
606
- `UPDATE issues SET \`${field}\` = ?, updated_at = ? WHERE id = ?`,
607
- [value, now, id]
608
- );
609
- }
610
- await doltCommit(pool, `update ${field} on ${id}`);
611
- return { ok: true };
612
- } catch (err) {
613
- log('updateIssueField error: %o', err);
614
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
615
- }
616
- }
617
-
618
- /**
619
- * Add a comment to an issue.
620
- *
621
- * @param {string} issueId
622
- * @param {string} text
623
- * @param {string} author
624
- * @returns {Promise<{ ok: true } | { ok: false, error: { code: string, message: string } }>}
625
- */
626
- export async function addComment(issueId, text, author) {
627
- const pool = getPool();
628
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
629
- try {
630
- await pool.query(
631
- `INSERT INTO comments (issue_id, author, text) VALUES (?, ?, ?)`,
632
- [issueId, author, text]
633
- );
634
- await doltCommit(pool, `add comment on ${issueId}`);
635
- return { ok: true };
636
- } catch (err) {
637
- log('addComment error: %o', err);
638
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
639
- }
640
- }
641
-
642
- /**
643
- * Add a dependency.
644
- *
645
- * @param {string} issueId
646
- * @param {string} dependsOnId
647
- * @param {string} [createdBy]
648
- * @returns {Promise<{ ok: true } | { ok: false, error: { code: string, message: string } }>}
649
- */
650
- export async function addDependency(issueId, dependsOnId, createdBy = '') {
651
- const pool = getPool();
652
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
653
- try {
654
- await pool.query(
655
- `INSERT IGNORE INTO dependencies (issue_id, depends_on_id, type, created_by)
656
- VALUES (?, ?, 'blocks', ?)`,
657
- [issueId, dependsOnId, createdBy]
658
- );
659
- await doltCommit(pool, `add dep ${issueId} → ${dependsOnId}`);
660
- return { ok: true };
661
- } catch (err) {
662
- log('addDependency error: %o', err);
663
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
664
- }
665
- }
666
-
667
- /**
668
- * Remove a dependency.
669
- *
670
- * @param {string} issueId
671
- * @param {string} dependsOnId
672
- * @returns {Promise<{ ok: true } | { ok: false, error: { code: string, message: string } }>}
673
- */
674
- export async function removeDependency(issueId, dependsOnId) {
675
- const pool = getPool();
676
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
677
- try {
678
- await pool.query(
679
- `DELETE FROM dependencies WHERE issue_id = ? AND depends_on_id = ? AND type = 'blocks'`,
680
- [issueId, dependsOnId]
681
- );
682
- await doltCommit(pool, `remove dep ${issueId} → ${dependsOnId}`);
683
- return { ok: true };
684
- } catch (err) {
685
- log('removeDependency error: %o', err);
686
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
687
- }
688
- }
689
-
690
- /**
691
- * Add a label to an issue.
692
- *
693
- * @param {string} issueId
694
- * @param {string} label
695
- * @returns {Promise<{ ok: true } | { ok: false, error: { code: string, message: string } }>}
696
- */
697
- export async function addLabel(issueId, label) {
698
- const pool = getPool();
699
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
700
- try {
701
- await pool.query(
702
- `INSERT IGNORE INTO labels (issue_id, label) VALUES (?, ?)`,
703
- [issueId, label]
704
- );
705
- await doltCommit(pool, `add label '${label}' on ${issueId}`);
706
- return { ok: true };
707
- } catch (err) {
708
- log('addLabel error: %o', err);
709
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
710
- }
711
- }
712
-
713
- /**
714
- * Remove a label from an issue.
715
- *
716
- * @param {string} issueId
717
- * @param {string} label
718
- * @returns {Promise<{ ok: true } | { ok: false, error: { code: string, message: string } }>}
719
- */
720
- export async function removeLabel(issueId, label) {
721
- const pool = getPool();
722
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
723
- try {
724
- await pool.query(
725
- `DELETE FROM labels WHERE issue_id = ? AND label = ?`,
726
- [issueId, label]
727
- );
728
- await doltCommit(pool, `remove label '${label}' from ${issueId}`);
729
- return { ok: true };
730
- } catch (err) {
731
- log('removeLabel error: %o', err);
732
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
733
- }
734
- }
735
-
736
- /**
737
- * Delete an issue.
738
- *
739
- * @param {string} id
740
- * @returns {Promise<{ ok: true } | { ok: false, error: { code: string, message: string } }>}
741
- */
742
- export async function deleteIssue(id) {
743
- const pool = getPool();
744
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
745
- const conn = await pool.getConnection();
746
- try {
747
- await conn.beginTransaction();
748
- await conn.query(`DELETE FROM comments WHERE issue_id = ?`, [id]);
749
- await conn.query(`DELETE FROM labels WHERE issue_id = ?`, [id]);
750
- await conn.query(`DELETE FROM dependencies WHERE issue_id = ? OR depends_on_id = ?`, [id, id]);
751
- await conn.query(`DELETE FROM issues WHERE id = ?`, [id]);
752
- await conn.commit();
753
- conn.release();
754
- await doltCommit(pool, `delete issue ${id}`);
755
- return { ok: true };
756
- } catch (err) {
757
- try { await conn.rollback(); } catch { /* ignore */ }
758
- conn.release();
759
- log('deleteIssue error: %o', err);
760
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
761
- }
762
- }
763
-
764
- /**
765
- * Dolt commit (auto-commit to working set).
766
- * Uses CALL dolt_commit() to persist changes to the Dolt commit graph.
767
- *
768
- * @param {import('mysql2/promise').Pool} pool
769
- * @param {string} message
770
- */
771
- async function doltCommit(pool, message) {
772
- try {
773
- await pool.query(`CALL dolt_commit('-Am', ?)`, [message]);
774
- } catch (err) {
775
- // If nothing to commit, that's fine
776
- const msg = /** @type {any} */ (err).message || '';
777
- if (!msg.includes('nothing to commit')) {
778
- log('dolt_commit warning: %s', msg);
779
- }
780
- }
781
- }