@rsktash/beads-ui 0.1.50 → 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-BuQ1xTdM.css +0 -1
  137. package/dist/assets/index-DclShbpd.js +0 -125
  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 -764
  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,764 +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
- return items.map(item => {
174
- const id = String(item.id);
175
- const enriched = { ...item };
176
- const pid = parentIdMap.get(id);
177
- if (pid) {
178
- enriched.parent_id = pid;
179
- if (parentTitles.has(pid)) enriched.parent_title = parentTitles.get(pid);
180
- }
181
- const cc = childCounts.get(id);
182
- if (cc) {
183
- enriched.total_children = cc.total;
184
- enriched.closed_children = cc.closed;
185
- }
186
- const bb = blockedBy.get(id);
187
- if (bb && bb.length > 0) {
188
- enriched.blocked_by = bb;
189
- }
190
- return enriched;
191
- });
192
- } catch (err) {
193
- log('enrichListItems error: %o', err);
194
- return items;
195
- }
196
- }
197
-
198
- /**
199
- * Fetch total count for a WHERE clause.
200
- *
201
- * @param {import('mysql2/promise').Pool} pool
202
- * @param {string} where - SQL WHERE clause (without WHERE keyword), or empty for all
203
- * @param {any[]} [params]
204
- * @returns {Promise<number>}
205
- */
206
- async function fetchTotal(pool, where, params = []) {
207
- const sql = where
208
- ? `SELECT COUNT(*) AS total FROM issues WHERE ${where}`
209
- : `SELECT COUNT(*) AS total FROM issues`;
210
- const [rows] = await pool.query(sql, params);
211
- return /** @type {any[]} */ (rows)[0]?.total || 0;
212
- }
213
-
214
- /**
215
- * Fetch all issues with pagination (for 'all-issues' subscription).
216
- *
217
- * @param {Pagination} [pagination]
218
- * @returns {Promise<PaginatedResult | QueryError>}
219
- */
220
- export async function queryAllIssues(pagination) {
221
- const pool = getPool();
222
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
223
- try {
224
- const total = await fetchTotal(pool, '');
225
- const { limitClause } = buildPagination(pagination);
226
- const [rows] = await pool.query(
227
- `SELECT ${LIST_COLS_ALIASED}, d.depends_on_id AS parent
228
- FROM issues i
229
- LEFT JOIN dependencies d ON d.issue_id = i.id AND d.type = 'parent-child'
230
- ORDER BY i.updated_at DESC${limitClause}`
231
- );
232
- return { ok: true, items: /** @type {any[]} */ (rows).map(normalizeRow), total };
233
- } catch (err) {
234
- log('queryAllIssues error: %o', err);
235
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
236
- }
237
- }
238
-
239
- /**
240
- * Fetch epics (for 'epics' subscription).
241
- *
242
- * @returns {Promise<{ ok: true, items: Array<Record<string, unknown>> } | { ok: false, error: { code: string, message: string } }>}
243
- */
244
- /**
245
- * @param {{ limit?: number, offset?: number }} [pagination]
246
- * @returns {Promise<PaginatedResult | QueryError>}
247
- */
248
- export async function queryEpics(pagination) {
249
- const pool = getPool();
250
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
251
- try {
252
- const [countRows] = await pool.query(`SELECT COUNT(*) AS total FROM issues WHERE issue_type = 'epic'`);
253
- const total = /** @type {any[]} */ (countRows)[0]?.total || 0;
254
- const { limitClause } = buildPagination(pagination);
255
- const [rows] = await pool.query(
256
- `SELECT ${LIST_COLS_ALIASED}, d.depends_on_id AS parent
257
- FROM issues i
258
- LEFT JOIN dependencies d ON d.issue_id = i.id AND d.type = 'parent-child'
259
- WHERE i.issue_type = 'epic'
260
- ORDER BY i.updated_at DESC${limitClause}`
261
- );
262
- return { ok: true, items: /** @type {any[]} */ (rows).map(normalizeRow), total };
263
- } catch (err) {
264
- log('queryEpics error: %o', err);
265
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
266
- }
267
- }
268
-
269
- /**
270
- * Fetch blocked issues (for 'blocked-issues' subscription).
271
- *
272
- * @returns {Promise<{ ok: true, items: Array<Record<string, unknown>> } | { ok: false, error: { code: string, message: string } }>}
273
- */
274
- /**
275
- * @param {Pagination} [pagination]
276
- * @returns {Promise<PaginatedResult | QueryError>}
277
- */
278
- export async function queryBlockedIssues(pagination) {
279
- const pool = getPool();
280
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
281
- try {
282
- const blockedWhere = `status = 'open' AND id IN (
283
- SELECT bl.issue_id FROM dependencies bl
284
- JOIN issues blocker ON bl.depends_on_id = blocker.id
285
- WHERE bl.type = 'blocks' AND blocker.status != 'closed')`;
286
- const total = await fetchTotal(pool, blockedWhere);
287
- const { limitClause } = buildPagination(pagination);
288
- const [rows] = await pool.query(
289
- `SELECT ${LIST_COLS_ALIASED}, pc.depends_on_id AS parent
290
- FROM issues i
291
- LEFT JOIN dependencies pc ON pc.issue_id = i.id AND pc.type = 'parent-child'
292
- WHERE i.status = 'open'
293
- AND i.id IN (
294
- SELECT bl.issue_id FROM dependencies bl
295
- JOIN issues blocker ON bl.depends_on_id = blocker.id
296
- WHERE bl.type = 'blocks' AND blocker.status != 'closed'
297
- )
298
- ORDER BY i.updated_at DESC${limitClause}`
299
- );
300
- return { ok: true, items: /** @type {any[]} */ (rows).map(normalizeRow), total };
301
- } catch (err) {
302
- log('queryBlockedIssues error: %o', err);
303
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
304
- }
305
- }
306
-
307
- /**
308
- * Fetch ready issues (for 'ready-issues' subscription).
309
- *
310
- * @returns {Promise<{ ok: true, items: Array<Record<string, unknown>> } | { ok: false, error: { code: string, message: string } }>}
311
- */
312
- /**
313
- * @param {Pagination} [pagination]
314
- * @returns {Promise<PaginatedResult | QueryError>}
315
- */
316
- export async function queryReadyIssues(pagination) {
317
- const pool = getPool();
318
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
319
- try {
320
- const readyWhere = `status = 'open' AND id NOT IN (
321
- SELECT bl.issue_id FROM dependencies bl
322
- JOIN issues blocker ON bl.depends_on_id = blocker.id
323
- WHERE bl.type = 'blocks' AND blocker.status != 'closed')`;
324
- const total = await fetchTotal(pool, readyWhere);
325
- const { limitClause } = buildPagination(pagination);
326
- const [rows] = await pool.query(
327
- `SELECT ${LIST_COLS_ALIASED}, pc.depends_on_id AS parent
328
- FROM issues i
329
- LEFT JOIN dependencies pc ON pc.issue_id = i.id AND pc.type = 'parent-child'
330
- WHERE i.status = 'open'
331
- AND i.id NOT IN (
332
- SELECT bl.issue_id FROM dependencies bl
333
- JOIN issues blocker ON bl.depends_on_id = blocker.id
334
- WHERE bl.type = 'blocks' AND blocker.status != 'closed'
335
- )
336
- ORDER BY i.updated_at DESC${limitClause}`
337
- );
338
- return { ok: true, items: /** @type {any[]} */ (rows).map(normalizeRow), total };
339
- } catch (err) {
340
- log('queryReadyIssues error: %o', err);
341
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
342
- }
343
- }
344
-
345
- /**
346
- * Fetch issues by status (for 'in-progress-issues', 'closed-issues' subscriptions).
347
- *
348
- * @param {string} status
349
- * @param {number} [limit]
350
- * @returns {Promise<{ ok: true, items: Array<Record<string, unknown>> } | { ok: false, error: { code: string, message: string } }>}
351
- */
352
- /**
353
- * @param {string} status
354
- * @param {Pagination} [pagination]
355
- * @returns {Promise<PaginatedResult | QueryError>}
356
- */
357
- export async function queryIssuesByStatus(status, pagination) {
358
- const pool = getPool();
359
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
360
- try {
361
- const total = await fetchTotal(pool, 'status = ?', [status]);
362
- const { limitClause } = buildPagination(pagination);
363
- const [rows] = await pool.query(
364
- `SELECT ${LIST_COLS_ALIASED}, pc.depends_on_id AS parent
365
- FROM issues i
366
- LEFT JOIN dependencies pc ON pc.issue_id = i.id AND pc.type = 'parent-child'
367
- WHERE i.status = ?
368
- ORDER BY i.updated_at DESC${limitClause}`,
369
- [status]
370
- );
371
- return { ok: true, items: /** @type {any[]} */ (rows).map(normalizeRow), total };
372
- } catch (err) {
373
- log('queryIssuesByStatus error: %o', err);
374
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
375
- }
376
- }
377
-
378
- /**
379
- * Search issues with optional FULLTEXT query and status/type filters.
380
- *
381
- * When `query` is non-empty, uses Dolt FULLTEXT MATCH...AGAINST for
382
- * relevance-ranked results. Falls back to LIKE on id for exact ID matches.
383
- * Status and type filters narrow results server-side.
384
- *
385
- * @param {string} query - Search terms (empty string = no text filter)
386
- * @param {Pagination & { status?: string, type?: string }} [options]
387
- * @returns {Promise<PaginatedResult | QueryError>}
388
- */
389
- export async function querySearchIssues(query, options) {
390
- const pool = getPool();
391
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
392
- try {
393
- const conditions = [];
394
- const params = [];
395
-
396
- // FULLTEXT search on title + description
397
- if (query.length > 0) {
398
- // Use MATCH...AGAINST for natural language search, plus LIKE on id for exact ID matches
399
- conditions.push('(MATCH(i.title, i.description) AGAINST (?) OR i.id LIKE ?)');
400
- params.push(query, `%${query}%`);
401
- }
402
-
403
- // Status filter
404
- if (options?.status && options.status !== 'all') {
405
- conditions.push('i.status = ?');
406
- params.push(options.status);
407
- }
408
-
409
- // Type filter
410
- if (options?.type && options.type !== 'all') {
411
- conditions.push('i.issue_type = ?');
412
- params.push(options.type);
413
- }
414
-
415
- const where = conditions.length > 0 ? conditions.join(' AND ') : '1=1';
416
-
417
- const [countRows] = await pool.query(
418
- `SELECT COUNT(*) AS total FROM issues i WHERE ${where}`, params
419
- );
420
- const total = /** @type {any[]} */ (countRows)[0]?.total || 0;
421
-
422
- const { limitClause } = buildPagination(options);
423
- const [rows] = await pool.query(
424
- `SELECT ${LIST_COLS_ALIASED}, pc.depends_on_id AS parent
425
- FROM issues i
426
- LEFT JOIN dependencies pc ON pc.issue_id = i.id AND pc.type = 'parent-child'
427
- WHERE ${where}
428
- ORDER BY i.updated_at DESC${limitClause}`,
429
- params
430
- );
431
- return { ok: true, items: /** @type {any[]} */ (rows).map(normalizeRow), total };
432
- } catch (err) {
433
- log('querySearchIssues error: %o', err);
434
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
435
- }
436
- }
437
-
438
- /**
439
- * Fetch a single issue with dependencies, labels and parent info
440
- * (for 'issue-detail' subscription and post-mutation show).
441
- *
442
- * @param {string} id
443
- * @returns {Promise<{ ok: true, item: Record<string, unknown> } | { ok: false, error: { code: string, message: string } }>}
444
- */
445
- export async function queryIssueDetail(id) {
446
- const pool = getPool();
447
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
448
- try {
449
- const [issueRows] = await pool.query(
450
- `SELECT ${DETAIL_COLS} FROM issues WHERE id = ?`, [id]
451
- );
452
- const issues = /** @type {any[]} */ (issueRows);
453
- if (issues.length === 0) {
454
- return { ok: false, error: { code: 'not_found', message: `Issue ${id} not found` } };
455
- }
456
-
457
- const issue = normalizeRow(issues[0]);
458
-
459
- // Fetch dependencies
460
- const [depRows] = await pool.query(
461
- `SELECT issue_id, depends_on_id, type, created_at, created_by, metadata
462
- FROM dependencies WHERE issue_id = ? OR depends_on_id = ?`, [id, id]
463
- );
464
- issue.dependencies = /** @type {any[]} */ (depRows).map(normalizeRow);
465
-
466
- // Derive parent from parent-child dependency (with context)
467
- const parentDep = /** @type {any[]} */ (depRows).find(
468
- (d) => d.issue_id === id && d.type === 'parent-child'
469
- );
470
- if (parentDep) {
471
- issue.parent_id = parentDep.depends_on_id;
472
- issue.parent = parentDep.depends_on_id;
473
- // Fetch parent title/status for sidebar context
474
- const [parentRows] = await pool.query(
475
- `SELECT id, title, status, issue_type FROM issues WHERE id = ?`,
476
- [parentDep.depends_on_id]
477
- );
478
- const parentIssues = /** @type {any[]} */ (parentRows);
479
- if (parentIssues.length > 0) {
480
- issue.parent_title = parentIssues[0].title;
481
- issue.parent_status = parentIssues[0].status;
482
- issue.parent_type = parentIssues[0].issue_type;
483
- }
484
- }
485
-
486
- // Derive dependency/dependent counts
487
- issue.dependency_count = /** @type {any[]} */ (depRows).filter(
488
- (d) => d.issue_id === id && d.type === 'blocks'
489
- ).length;
490
- issue.dependent_count = /** @type {any[]} */ (depRows).filter(
491
- (d) => d.depends_on_id === id && d.type === 'blocks'
492
- ).length;
493
-
494
- // Fetch labels
495
- const [labelRows] = await pool.query(
496
- `SELECT label FROM labels WHERE issue_id = ?`, [id]
497
- );
498
- issue.labels = /** @type {any[]} */ (labelRows).map((r) => r.label);
499
-
500
- // Fetch children (issues that have a parent-child dep pointing to this issue)
501
- const [childRows] = await pool.query(
502
- `SELECT i.id, i.title, i.status, i.priority, i.issue_type, i.assignee
503
- FROM dependencies d
504
- JOIN issues i ON i.id = d.issue_id
505
- WHERE d.depends_on_id = ? AND d.type = 'parent-child'
506
- ORDER BY i.created_at ASC`, [id]
507
- );
508
- const children = /** @type {any[]} */ (childRows).map(normalizeRow);
509
- if (children.length > 0) {
510
- issue.dependents = children;
511
- issue.total_children = children.length;
512
- issue.closed_children = children.filter((c) => c.status === 'closed').length;
513
- }
514
-
515
- // Fetch comments
516
- const [commentRows] = await pool.query(
517
- `SELECT id, issue_id, author, text, created_at FROM comments
518
- WHERE issue_id = ? ORDER BY created_at ASC`, [id]
519
- );
520
- const comments = /** @type {any[]} */ (commentRows).map(normalizeRow);
521
- issue.comments = comments;
522
- issue.comment_count = comments.length;
523
-
524
- return { ok: true, item: issue };
525
- } catch (err) {
526
- log('queryIssueDetail error: %o', err);
527
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
528
- }
529
- }
530
-
531
- /**
532
- * Fetch comments for an issue.
533
- *
534
- * @param {string} issueId
535
- * @returns {Promise<{ ok: true, items: Array<Record<string, unknown>> } | { ok: false, error: { code: string, message: string } }>}
536
- */
537
- export async function queryComments(issueId) {
538
- const pool = getPool();
539
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
540
- try {
541
- const [rows] = await pool.query(
542
- `SELECT id, issue_id, author, text, created_at FROM comments
543
- WHERE issue_id = ? ORDER BY created_at ASC`, [issueId]
544
- );
545
- return { ok: true, items: /** @type {any[]} */ (rows).map(normalizeRow) };
546
- } catch (err) {
547
- log('queryComments error: %o', err);
548
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
549
- }
550
- }
551
-
552
- /**
553
- * Update a single field on an issue.
554
- *
555
- * @param {string} id
556
- * @param {string} field - SQL column name
557
- * @param {string | number | null} value
558
- * @returns {Promise<{ ok: true } | { ok: false, error: { code: string, message: string } }>}
559
- */
560
- export async function updateIssueField(id, field, value) {
561
- const pool = getPool();
562
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
563
-
564
- // Whitelist allowed columns to prevent SQL injection
565
- const ALLOWED_FIELDS = new Set([
566
- 'title', 'description', 'design', 'acceptance_criteria', 'notes',
567
- 'status', 'priority', 'assignee', 'issue_type'
568
- ]);
569
- if (!ALLOWED_FIELDS.has(field)) {
570
- return { ok: false, error: { code: 'bad_request', message: `Field '${field}' not allowed` } };
571
- }
572
-
573
- try {
574
- const now = new Date().toISOString().replace('T', ' ').replace('Z', '');
575
- // Handle status=closed → set closed_at
576
- if (field === 'status' && value === 'closed') {
577
- await pool.query(
578
- `UPDATE issues SET status = 'closed', closed_at = ?, updated_at = ? WHERE id = ?`,
579
- [now, now, id]
580
- );
581
- } else if (field === 'status' && value !== 'closed') {
582
- // Reopening: clear closed_at
583
- await pool.query(
584
- `UPDATE issues SET status = ?, closed_at = NULL, updated_at = ? WHERE id = ?`,
585
- [value, now, id]
586
- );
587
- } else {
588
- await pool.query(
589
- `UPDATE issues SET \`${field}\` = ?, updated_at = ? WHERE id = ?`,
590
- [value, now, id]
591
- );
592
- }
593
- await doltCommit(pool, `update ${field} on ${id}`);
594
- return { ok: true };
595
- } catch (err) {
596
- log('updateIssueField error: %o', err);
597
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
598
- }
599
- }
600
-
601
- /**
602
- * Add a comment to an issue.
603
- *
604
- * @param {string} issueId
605
- * @param {string} text
606
- * @param {string} author
607
- * @returns {Promise<{ ok: true } | { ok: false, error: { code: string, message: string } }>}
608
- */
609
- export async function addComment(issueId, text, author) {
610
- const pool = getPool();
611
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
612
- try {
613
- await pool.query(
614
- `INSERT INTO comments (issue_id, author, text) VALUES (?, ?, ?)`,
615
- [issueId, author, text]
616
- );
617
- await doltCommit(pool, `add comment on ${issueId}`);
618
- return { ok: true };
619
- } catch (err) {
620
- log('addComment error: %o', err);
621
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
622
- }
623
- }
624
-
625
- /**
626
- * Add a dependency.
627
- *
628
- * @param {string} issueId
629
- * @param {string} dependsOnId
630
- * @param {string} [createdBy]
631
- * @returns {Promise<{ ok: true } | { ok: false, error: { code: string, message: string } }>}
632
- */
633
- export async function addDependency(issueId, dependsOnId, createdBy = '') {
634
- const pool = getPool();
635
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
636
- try {
637
- await pool.query(
638
- `INSERT IGNORE INTO dependencies (issue_id, depends_on_id, type, created_by)
639
- VALUES (?, ?, 'blocks', ?)`,
640
- [issueId, dependsOnId, createdBy]
641
- );
642
- await doltCommit(pool, `add dep ${issueId} → ${dependsOnId}`);
643
- return { ok: true };
644
- } catch (err) {
645
- log('addDependency error: %o', err);
646
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
647
- }
648
- }
649
-
650
- /**
651
- * Remove a dependency.
652
- *
653
- * @param {string} issueId
654
- * @param {string} dependsOnId
655
- * @returns {Promise<{ ok: true } | { ok: false, error: { code: string, message: string } }>}
656
- */
657
- export async function removeDependency(issueId, dependsOnId) {
658
- const pool = getPool();
659
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
660
- try {
661
- await pool.query(
662
- `DELETE FROM dependencies WHERE issue_id = ? AND depends_on_id = ? AND type = 'blocks'`,
663
- [issueId, dependsOnId]
664
- );
665
- await doltCommit(pool, `remove dep ${issueId} → ${dependsOnId}`);
666
- return { ok: true };
667
- } catch (err) {
668
- log('removeDependency error: %o', err);
669
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
670
- }
671
- }
672
-
673
- /**
674
- * Add a label to an issue.
675
- *
676
- * @param {string} issueId
677
- * @param {string} label
678
- * @returns {Promise<{ ok: true } | { ok: false, error: { code: string, message: string } }>}
679
- */
680
- export async function addLabel(issueId, label) {
681
- const pool = getPool();
682
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
683
- try {
684
- await pool.query(
685
- `INSERT IGNORE INTO labels (issue_id, label) VALUES (?, ?)`,
686
- [issueId, label]
687
- );
688
- await doltCommit(pool, `add label '${label}' on ${issueId}`);
689
- return { ok: true };
690
- } catch (err) {
691
- log('addLabel error: %o', err);
692
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
693
- }
694
- }
695
-
696
- /**
697
- * Remove a label from an issue.
698
- *
699
- * @param {string} issueId
700
- * @param {string} label
701
- * @returns {Promise<{ ok: true } | { ok: false, error: { code: string, message: string } }>}
702
- */
703
- export async function removeLabel(issueId, label) {
704
- const pool = getPool();
705
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
706
- try {
707
- await pool.query(
708
- `DELETE FROM labels WHERE issue_id = ? AND label = ?`,
709
- [issueId, label]
710
- );
711
- await doltCommit(pool, `remove label '${label}' from ${issueId}`);
712
- return { ok: true };
713
- } catch (err) {
714
- log('removeLabel error: %o', err);
715
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
716
- }
717
- }
718
-
719
- /**
720
- * Delete an issue.
721
- *
722
- * @param {string} id
723
- * @returns {Promise<{ ok: true } | { ok: false, error: { code: string, message: string } }>}
724
- */
725
- export async function deleteIssue(id) {
726
- const pool = getPool();
727
- if (!pool) return { ok: false, error: { code: 'no_pool', message: 'Dolt pool not available' } };
728
- const conn = await pool.getConnection();
729
- try {
730
- await conn.beginTransaction();
731
- await conn.query(`DELETE FROM comments WHERE issue_id = ?`, [id]);
732
- await conn.query(`DELETE FROM labels WHERE issue_id = ?`, [id]);
733
- await conn.query(`DELETE FROM dependencies WHERE issue_id = ? OR depends_on_id = ?`, [id, id]);
734
- await conn.query(`DELETE FROM issues WHERE id = ?`, [id]);
735
- await conn.commit();
736
- conn.release();
737
- await doltCommit(pool, `delete issue ${id}`);
738
- return { ok: true };
739
- } catch (err) {
740
- try { await conn.rollback(); } catch { /* ignore */ }
741
- conn.release();
742
- log('deleteIssue error: %o', err);
743
- return { ok: false, error: { code: 'db_error', message: String(/** @type {any} */ (err).message) } };
744
- }
745
- }
746
-
747
- /**
748
- * Dolt commit (auto-commit to working set).
749
- * Uses CALL dolt_commit() to persist changes to the Dolt commit graph.
750
- *
751
- * @param {import('mysql2/promise').Pool} pool
752
- * @param {string} message
753
- */
754
- async function doltCommit(pool, message) {
755
- try {
756
- await pool.query(`CALL dolt_commit('-Am', ?)`, [message]);
757
- } catch (err) {
758
- // If nothing to commit, that's fine
759
- const msg = /** @type {any} */ (err).message || '';
760
- if (!msg.includes('nothing to commit')) {
761
- log('dolt_commit warning: %s', msg);
762
- }
763
- }
764
- }