@hienlh/ppm 0.9.65 → 0.9.67

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 (173) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/web/assets/{_basePickBy-3Xe18azI.js → _basePickBy-5PGDJbfF.js} +1 -1
  3. package/dist/web/assets/{_baseUniq-Yy35llnn.js → _baseUniq-BT4Ow4Kk.js} +1 -1
  4. package/dist/web/assets/{api-settings-CgBII8jW.js → api-settings-Bn-bIxD1.js} +1 -1
  5. package/dist/web/assets/{arc-B9n1Gvb5.js → arc-BAOivWpI.js} +1 -1
  6. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
  7. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DqAZP_F6.js → architectureDiagram-2XIMDMQ5-Z-4eN4za.js} +1 -1
  8. package/dist/web/assets/{blockDiagram-WCTKOSBZ-h3cDF2vI.js → blockDiagram-WCTKOSBZ-BCLqzhuZ.js} +1 -1
  9. package/dist/web/assets/{c4Diagram-IC4MRINW--pF1r5lr.js → c4Diagram-IC4MRINW-0Vp0Jeas.js} +1 -1
  10. package/dist/web/assets/channel-By7bn0Yq.js +1 -0
  11. package/dist/web/assets/chat-tab-onkz52iv.js +10 -0
  12. package/dist/web/assets/{chunk-4BX2VUAB-C3aZvW7B.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
  13. package/dist/web/assets/{chunk-55IACEB6-D5cABeB9.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
  14. package/dist/web/assets/{chunk-7E7YKBS2-CkFGv6Zs.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
  15. package/dist/web/assets/{chunk-7R4GIKGN-Dvbyu4Zw.js → chunk-7R4GIKGN-Dv-4cAYn.js} +2 -2
  16. package/dist/web/assets/{chunk-C72U2L5F-CtqKiH4q.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
  17. package/dist/web/assets/{chunk-EGIJ26TM-Cpr87sBR.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
  18. package/dist/web/assets/{chunk-FMBD7UC4-D23YVTOU.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
  19. package/dist/web/assets/{chunk-GEFDOKGD-tDjHsAUs.js → chunk-GEFDOKGD-D-pKjlVd.js} +1 -1
  20. package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +2 -0
  21. package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +1 -0
  22. package/dist/web/assets/{chunk-JSJVCQXG-BBmymCjA.js → chunk-JSJVCQXG-99JzIdPr.js} +1 -1
  23. package/dist/web/assets/{chunk-KX2RTZJC-DP36BDiU.js → chunk-KX2RTZJC-CRq1OBZv.js} +1 -1
  24. package/dist/web/assets/{chunk-KYZI473N-Djw13C-3.js → chunk-KYZI473N-Bb0MCaIO.js} +1 -1
  25. package/dist/web/assets/{chunk-L3YUKLVL-HG_eMj_C.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
  26. package/dist/web/assets/{chunk-MX3YWQON-C2UEioMs.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
  27. package/dist/web/assets/{chunk-NQ4KR5QH-DXUTQ-BL.js → chunk-NQ4KR5QH-z_blpjxi.js} +1 -1
  28. package/dist/web/assets/{chunk-O4XLMI2P-BsUWb9d0.js → chunk-O4XLMI2P-nDhi_cVu.js} +1 -1
  29. package/dist/web/assets/{chunk-OZEHJAEY-rG0P22U9.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
  30. package/dist/web/assets/{chunk-PQ6SQG4A-DX0xW7kO.js → chunk-PQ6SQG4A-TF58UVMU.js} +1 -1
  31. package/dist/web/assets/{chunk-PU5JKC2W-C7Gry6md.js → chunk-PU5JKC2W-ek7k4QVB.js} +1 -1
  32. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
  33. package/dist/web/assets/{chunk-R5LLSJPH-CMY0PkRK.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
  34. package/dist/web/assets/{chunk-WL4C6EOR-CXuQvlyu.js → chunk-WL4C6EOR-ByUrSRin.js} +1 -1
  35. package/dist/web/assets/{chunk-XIRO2GV7-DRJEb7Zb.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
  36. package/dist/web/assets/{chunk-XPW4576I-BPEX8KhL.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
  37. package/dist/web/assets/{chunk-XZSTWKYB-Cb0iqycX.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
  38. package/dist/web/assets/{chunk-YBOYWFTD-av5aeHLq.js → chunk-YBOYWFTD-rQG3QH5s.js} +1 -1
  39. package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +1 -0
  40. package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +1 -0
  41. package/dist/web/assets/clone-LRxlvnMj.js +1 -0
  42. package/dist/web/assets/code-editor-BixOXePn.js +8 -0
  43. package/dist/web/assets/{cose-bilkent-S5V4N54A-qudEiMCT.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
  44. package/dist/web/assets/csv-parser-CNNw2RVA.js +6 -0
  45. package/dist/web/assets/{csv-preview-sx6DC51G.js → csv-preview-D2pJJj3K.js} +3 -8
  46. package/dist/web/assets/{dagre-BFcnKyBF.js → dagre-DHq9bhnd.js} +1 -1
  47. package/dist/web/assets/{dagre-KLK3FWXG-C3O-MTLf.js → dagre-KLK3FWXG-BdJr7Byp.js} +1 -1
  48. package/dist/web/assets/database-viewer-DfLe8ewt.js +2 -0
  49. package/dist/web/assets/{diagram-E7M64L7V-DxPjK7_c.js → diagram-E7M64L7V-_db4pBVA.js} +1 -1
  50. package/dist/web/assets/{diagram-IFDJBPK2-sqTog_XV.js → diagram-IFDJBPK2-xKoeuiJx.js} +1 -1
  51. package/dist/web/assets/{diagram-P4PSJMXO-hzmp0GHK.js → diagram-P4PSJMXO-C8tjJsev.js} +1 -1
  52. package/dist/web/assets/diff-viewer-eFO08m_L.js +4 -0
  53. package/dist/web/assets/dist-DIV6WgAG.js +41 -0
  54. package/dist/web/assets/{erDiagram-INFDFZHY-DLeYhAAT.js → erDiagram-INFDFZHY-BSh2z9Df.js} +1 -1
  55. package/dist/web/assets/{extension-webview-2XjXXtyy.js → extension-webview-4CL9kCKR.js} +1 -1
  56. package/dist/web/assets/{flowDiagram-PKNHOUZH-CRxlE9Sr.js → flowDiagram-PKNHOUZH-oYaovqyp.js} +1 -1
  57. package/dist/web/assets/{ganttDiagram-A5KZAMGK-BdjmoMLS.js → ganttDiagram-A5KZAMGK-DmL26q2P.js} +1 -1
  58. package/dist/web/assets/git-graph-BqeE_o17.js +1 -0
  59. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
  60. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js → gitGraphDiagram-K3NZZRJ6-CMoukSrY.js} +1 -1
  61. package/dist/web/assets/{graphlib-Duh_bWLa.js → graphlib-BcsNnGcW.js} +1 -1
  62. package/dist/web/assets/index-BWLy2h18.css +2 -0
  63. package/dist/web/assets/index-DwrCg0TN.js +30 -0
  64. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
  65. package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +2 -0
  66. package/dist/web/assets/{isEmpty-B9L-Ge-H.js → isEmpty-bnrF3Qbc.js} +1 -1
  67. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js → ishikawaDiagram-PHBUUO56-D05_LyL7.js} +1 -1
  68. package/dist/web/assets/{journeyDiagram-4ABVD52K-CgDI-UG4.js → journeyDiagram-4ABVD52K-B_L20qMe.js} +1 -1
  69. package/dist/web/assets/{kanban-definition-K7BYSVSG-h4g10UHL.js → kanban-definition-K7BYSVSG-CZ535BbZ.js} +1 -1
  70. package/dist/web/assets/keybindings-store-BNBONtSd.js +1 -0
  71. package/dist/web/assets/{line-B75-Rx70.js → line-CVvo3dRu.js} +1 -1
  72. package/dist/web/assets/{linear-Bcjv9FQt.js → linear-DP4mkX3m.js} +1 -1
  73. package/dist/web/assets/{markdown-renderer-Bb7OSpxF.js → markdown-renderer-BUqab2os.js} +5 -5
  74. package/dist/web/assets/{mermaid-parser.core-8u2leTXI.js → mermaid-parser.core-C7UwoIh6.js} +2 -2
  75. package/dist/web/assets/{mindmap-definition-YRQLILUH-BaOBwb-W.js → mindmap-definition-YRQLILUH-x0MTutJp.js} +1 -1
  76. package/dist/web/assets/{ordinal-LFEjVtwQ.js → ordinal-_K3x1fkz.js} +1 -1
  77. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +1 -0
  78. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +1 -0
  79. package/dist/web/assets/{pieDiagram-SKSYHLDU-At5Kz0KK.js → pieDiagram-SKSYHLDU-C1Gjrtzy.js} +1 -1
  80. package/dist/web/assets/{port-forwarding-tab-bD8MKumH.js → port-forwarding-tab-CfO-UJ84.js} +1 -1
  81. package/dist/web/assets/postgres-viewer-BVJZ44eU.js +13 -0
  82. package/dist/web/assets/{quadrantDiagram-337W2JSQ-CdjGIDfw.js → quadrantDiagram-337W2JSQ-C8bzJCjQ.js} +1 -1
  83. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
  84. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-B9F_Cx_p.js → requirementDiagram-Z7DCOOCP-pQyah6WB.js} +1 -1
  85. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-RolPi8bU.js → sankeyDiagram-WA2Y5GQK-T6RgG-N8.js} +1 -1
  86. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-DM-tMAhx.js → sequenceDiagram-2WXFIKYE-BQDJ4CVs.js} +1 -1
  87. package/dist/web/assets/settings-tab-C6hdJujW.js +1 -0
  88. package/dist/web/assets/sql-completion-provider-DM9Qov6L.js +1 -0
  89. package/dist/web/assets/sql-query-editor-OhZa4Z9F.js +3 -0
  90. package/dist/web/assets/sqlite-viewer-C8p1_jz4.js +1 -0
  91. package/dist/web/assets/{stateDiagram-RAJIS63D-C4EMl6jf.js → stateDiagram-RAJIS63D-66vhiIuk.js} +1 -1
  92. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +1 -0
  93. package/dist/web/assets/{terminal-tab-Cq6vQ9W9.js → terminal-tab-CaO0WnIo.js} +2 -2
  94. package/dist/web/assets/text-wrap-BWNOVswA.js +1 -0
  95. package/dist/web/assets/{timeline-definition-YZTLITO2-A4PN_Efm.js → timeline-definition-YZTLITO2-DwZqB3nn.js} +1 -1
  96. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
  97. package/dist/web/assets/use-monaco-theme-U9ZhfvHB.js +11 -0
  98. package/dist/web/assets/{vennDiagram-LZ73GAT5-ywK7LMaH.js → vennDiagram-LZ73GAT5-s9Z71fz-.js} +1 -1
  99. package/dist/web/assets/x-D2_KzIET.js +1 -0
  100. package/dist/web/assets/{xychartDiagram-JWTSCODW-DylHYNtJ.js → xychartDiagram-JWTSCODW-DRa_TH4B.js} +1 -1
  101. package/dist/web/index.html +9 -8
  102. package/dist/web/sw.js +1 -1
  103. package/package.json +1 -1
  104. package/src/cli/commands/bot-cmd.ts +4 -0
  105. package/src/cli/commands/db-cmd.ts +4 -3
  106. package/src/server/routes/database.ts +126 -9
  107. package/src/services/cloud.service.ts +2 -1
  108. package/src/services/database/sqlite-adapter.ts +1 -0
  109. package/src/services/db.service.ts +42 -3
  110. package/src/services/postgres.service.ts +37 -6
  111. package/src/services/sqlite.service.ts +18 -4
  112. package/src/services/table-cache.service.ts +2 -3
  113. package/src/types/database.ts +2 -0
  114. package/src/web/components/database/connection-form-dialog.tsx +17 -8
  115. package/src/web/components/database/connection-list.tsx +191 -139
  116. package/src/web/components/database/data-grid.tsx +634 -0
  117. package/src/web/components/database/database-sidebar.tsx +4 -1
  118. package/src/web/components/database/database-viewer.tsx +204 -225
  119. package/src/web/components/database/export-button.tsx +100 -0
  120. package/src/web/components/database/sql-completion-provider.ts +301 -0
  121. package/src/web/components/database/sql-query-editor.tsx +123 -0
  122. package/src/web/components/database/use-connections.ts +21 -1
  123. package/src/web/components/database/use-database.ts +59 -7
  124. package/src/web/components/editor/code-editor.tsx +224 -16
  125. package/src/web/components/sqlite/sqlite-query-editor.tsx +3 -90
  126. package/src/web/components/sqlite/sqlite-viewer.tsx +0 -2
  127. package/src/web/components/sqlite/use-sqlite.ts +1 -1
  128. package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +0 -1
  129. package/dist/web/assets/channel-C2fMafck.js +0 -1
  130. package/dist/web/assets/chat-tab-CfdMDCBK.js +0 -10
  131. package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +0 -2
  132. package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +0 -1
  133. package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +0 -1
  134. package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +0 -1
  135. package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +0 -1
  136. package/dist/web/assets/clone-B2hUek6n.js +0 -1
  137. package/dist/web/assets/code-editor-DhCfJIpG.js +0 -2
  138. package/dist/web/assets/database-viewer-DLkAUBpm.js +0 -1
  139. package/dist/web/assets/diff-viewer-DWVWsekJ.js +0 -4
  140. package/dist/web/assets/dist-C40JmyoH.js +0 -13
  141. package/dist/web/assets/dist-DRTW9IWi.js +0 -41
  142. package/dist/web/assets/git-graph-Ds5bs1cM.js +0 -1
  143. package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +0 -1
  144. package/dist/web/assets/index-8b0LM6IC.js +0 -30
  145. package/dist/web/assets/index-BnMECpW3.css +0 -2
  146. package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +0 -1
  147. package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +0 -2
  148. package/dist/web/assets/keybindings-store-C06Z0Zhk.js +0 -1
  149. package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +0 -1
  150. package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +0 -1
  151. package/dist/web/assets/postgres-viewer-C3OND65T.js +0 -1
  152. package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +0 -1
  153. package/dist/web/assets/settings-tab-cuMIkUNV.js +0 -1
  154. package/dist/web/assets/sqlite-viewer-CpjnwDtk.js +0 -1
  155. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +0 -1
  156. package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +0 -1
  157. package/dist/web/assets/use-monaco-theme-BH9sQ-Yu.js +0 -11
  158. /package/dist/web/assets/{api-client-BKIT_Qeg.js → api-client-BfBM3I7n.js} +0 -0
  159. /package/dist/web/assets/{array-DqLCdDFv.js → array-B9UHiPd-.js} +0 -0
  160. /package/dist/web/assets/{chevron-right-5HgK6l7K.js → chevron-right-4zq1jPv6.js} +0 -0
  161. /package/dist/web/assets/{columns-2-cEVJHYd7.js → columns-2-BoZAN-iw.js} +0 -0
  162. /package/dist/web/assets/{cytoscape.esm-CWPXKqbJ.js → cytoscape.esm-BW-DbntU.js} +0 -0
  163. /package/dist/web/assets/{defaultLocale-CrJzLgRD.js → defaultLocale-5eAKkKJC.js} +0 -0
  164. /package/dist/web/assets/{dist-Cep75xXf.js → dist-CSJdAyA9.js} +0 -0
  165. /package/dist/web/assets/{init-C0r9Gk5G.js → init-DlZdxViB.js} +0 -0
  166. /package/dist/web/assets/{isArrayLikeObject-CGBoxvCD.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
  167. /package/dist/web/assets/{katex-DzXRfQ_m.js → katex-Bqvo_ZG0.js} +0 -0
  168. /package/dist/web/assets/{lib-mag4ySk-.js → lib-DurwGtQO.js} +0 -0
  169. /package/dist/web/assets/{math-y9zN1W-N.js → math-069Z4SuC.js} +0 -0
  170. /package/dist/web/assets/{path-DIKpVbHL.js → path-6uRLdFF7.js} +0 -0
  171. /package/dist/web/assets/{rough.esm-nHaDi0Kw.js → rough.esm-JX0wREDd.js} +0 -0
  172. /package/dist/web/assets/{src-Dw4QhedI.js → src-BqX54PbV.js} +0 -0
  173. /package/dist/web/assets/{utils-DMiycH3O.js → utils-BNytJOb1.js} +0 -0
@@ -1,13 +1,13 @@
1
1
  import { Hono } from "hono";
2
2
  import {
3
3
  getConnections, getConnectionById, insertConnection, updateConnection, deleteConnection,
4
+ decryptConfig, getConnectionConfig,
4
5
  type ConnectionConfig, type ConnectionRow,
5
6
  } from "../../services/db.service.ts";
6
7
  import { getAdapter } from "../../services/database/adapter-registry.ts";
7
8
  import { syncTables, searchTables, getTablesFromCache } from "../../services/table-cache.service.ts";
8
9
  import { isReadOnlyQuery } from "../../services/database/readonly-check.ts";
9
10
  import { ok, err } from "../../types/api.ts";
10
- import type { DbConnectionConfig } from "../../types/database.ts";
11
11
 
12
12
  export const databaseRoutes = new Hono();
13
13
 
@@ -43,11 +43,14 @@ databaseRoutes.get("/connections", (c) => {
43
43
  }
44
44
  });
45
45
 
46
- /** GET /api/db/connections/export — full connection data including credentials */
46
+ /** GET /api/db/connections/export — full connection data including credentials (decrypted) */
47
47
  databaseRoutes.get("/connections/export", (c) => {
48
48
  try {
49
49
  const conns = getConnections();
50
- const exported = conns.map(({ id, sort_order, created_at, updated_at, ...rest }) => rest);
50
+ const exported = conns.map(({ id, sort_order, created_at, updated_at, connection_config, ...rest }) => ({
51
+ ...rest,
52
+ connection_config: JSON.stringify(decryptConfig(connection_config)),
53
+ }));
51
54
  return c.json(ok({ version: 1, exported_at: new Date().toISOString(), connections: exported }));
52
55
  } catch (e) {
53
56
  return c.json(err((e as Error).message), 500);
@@ -181,12 +184,25 @@ databaseRoutes.delete("/connections/:id", (c) => {
181
184
  // Connection operations
182
185
  // ---------------------------------------------------------------------------
183
186
 
187
+ /** POST /api/db/test — test a raw (unsaved) connection config */
188
+ databaseRoutes.post("/test", async (c) => {
189
+ try {
190
+ const body = await c.req.json<{ type: "sqlite" | "postgres"; connectionConfig: { type: string; path?: string; connectionString?: string } }>();
191
+ if (!body.type || !body.connectionConfig) return c.json(err("type and connectionConfig required"), 400);
192
+ const adapter = getAdapter(body.type);
193
+ const result = await adapter.testConnection(body.connectionConfig as import("../../types/database.ts").DbConnectionConfig);
194
+ return c.json(ok(result));
195
+ } catch (e) {
196
+ return c.json(err((e as Error).message), 500);
197
+ }
198
+ });
199
+
184
200
  /** POST /api/db/connections/:id/test */
185
201
  databaseRoutes.post("/connections/:id/test", async (c) => {
186
202
  try {
187
203
  const conn = resolveConn(c.req.param("id"));
188
204
  if (!conn) return c.json(err("Connection not found"), 404);
189
- const config = JSON.parse(conn.connection_config) as DbConnectionConfig;
205
+ const config = decryptConfig(conn.connection_config);
190
206
  const adapter = getAdapter(conn.type);
191
207
  const result = await adapter.testConnection(config);
192
208
  return c.json(ok(result));
@@ -217,7 +233,7 @@ databaseRoutes.get("/connections/:id/schema", async (c) => {
217
233
  const table = c.req.query("table");
218
234
  const schema = c.req.query("schema");
219
235
  if (!table) return c.json(err("table query param required"), 400);
220
- const config = JSON.parse(conn.connection_config) as DbConnectionConfig;
236
+ const config = decryptConfig(conn.connection_config);
221
237
  const adapter = getAdapter(conn.type);
222
238
  const cols = await adapter.getTableSchema(config, table, schema);
223
239
  return c.json(ok(cols));
@@ -233,7 +249,7 @@ databaseRoutes.get("/connections/:id/data", async (c) => {
233
249
  if (!conn) return c.json(err("Connection not found"), 404);
234
250
  const table = c.req.query("table");
235
251
  if (!table) return c.json(err("table query param required"), 400);
236
- const config = JSON.parse(conn.connection_config) as DbConnectionConfig;
252
+ const config = decryptConfig(conn.connection_config);
237
253
  const adapter = getAdapter(conn.type);
238
254
  const data = await adapter.getTableData(config, table, {
239
255
  schema: c.req.query("schema"),
@@ -260,7 +276,7 @@ databaseRoutes.post("/connections/:id/query", async (c) => {
260
276
  return c.json(err("Connection is readonly — only SELECT queries allowed. Change this in PPM web UI."), 403);
261
277
  }
262
278
 
263
- const config = JSON.parse(conn.connection_config) as DbConnectionConfig;
279
+ const config = decryptConfig(conn.connection_config);
264
280
  const adapter = getAdapter(conn.type);
265
281
  const result = await adapter.executeQuery(config, body.sql);
266
282
  return c.json(ok(result));
@@ -287,7 +303,7 @@ databaseRoutes.put("/connections/:id/cell", async (c) => {
287
303
  return c.json(err("table, pkColumn, and column are required"), 400);
288
304
  }
289
305
 
290
- const config = JSON.parse(conn.connection_config) as DbConnectionConfig;
306
+ const config = decryptConfig(conn.connection_config);
291
307
  const adapter = getAdapter(conn.type);
292
308
  await adapter.updateCell(config, body.table, {
293
309
  schema: body.schema,
@@ -320,7 +336,7 @@ databaseRoutes.delete("/connections/:id/row", async (c) => {
320
336
  return c.json(err("table, pkColumn, and pkValue are required"), 400);
321
337
  }
322
338
 
323
- const config = JSON.parse(conn.connection_config) as DbConnectionConfig;
339
+ const config = decryptConfig(conn.connection_config);
324
340
  const adapter = getAdapter(conn.type);
325
341
  await adapter.deleteRow(config, body.table, {
326
342
  schema: body.schema,
@@ -333,6 +349,107 @@ databaseRoutes.delete("/connections/:id/row", async (c) => {
333
349
  }
334
350
  });
335
351
 
352
+ // ---------------------------------------------------------------------------
353
+ // Bulk operations
354
+ // ---------------------------------------------------------------------------
355
+
356
+ /** POST /api/db/connections/:id/rows/delete — bulk delete by PK values */
357
+ databaseRoutes.post("/connections/:id/rows/delete", async (c) => {
358
+ try {
359
+ const conn = resolveConn(c.req.param("id"));
360
+ if (!conn) return c.json(err("Connection not found"), 404);
361
+ if (conn.readonly) return c.json(err("Connection is readonly — bulk delete is disabled."), 403);
362
+
363
+ const body = await c.req.json<{ table: string; schema?: string; pkColumn: string; pkValues: unknown[] }>();
364
+ if (!body.table || !body.pkColumn || !Array.isArray(body.pkValues) || body.pkValues.length === 0) {
365
+ return c.json(err("table, pkColumn, and pkValues[] are required"), 400);
366
+ }
367
+
368
+ const config = decryptConfig(conn.connection_config);
369
+ const adapter = getAdapter(conn.type);
370
+ for (const pkValue of body.pkValues) {
371
+ await adapter.deleteRow(config, body.table, { schema: body.schema, pkColumn: body.pkColumn, pkValue });
372
+ }
373
+ return c.json(ok({ deleted: body.pkValues.length }));
374
+ } catch (e) {
375
+ return c.json(err((e as Error).message), 500);
376
+ }
377
+ });
378
+
379
+ /** POST /api/db/connections/:id/row — insert a new row */
380
+ databaseRoutes.post("/connections/:id/row", async (c) => {
381
+ try {
382
+ const conn = resolveConn(c.req.param("id"));
383
+ if (!conn) return c.json(err("Connection not found"), 404);
384
+ if (conn.readonly) return c.json(err("Connection is readonly — insert is disabled."), 403);
385
+
386
+ const body = await c.req.json<{ table: string; schema?: string; values: Record<string, unknown> }>();
387
+ if (!body.table || !body.values || Object.keys(body.values).length === 0) {
388
+ return c.json(err("table and values are required"), 400);
389
+ }
390
+
391
+ const config = decryptConfig(conn.connection_config);
392
+ const adapter = getAdapter(conn.type);
393
+ // Build and execute INSERT query
394
+ const cols = Object.keys(body.values);
395
+ const vals = Object.values(body.values);
396
+ const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
397
+ const schema = body.schema && conn.type === "postgres" ? `"${body.schema}".` : "";
398
+ const sql = `INSERT INTO ${schema}"${body.table}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`;
399
+ await adapter.executeQuery(config, sql.replace(/\$(\d+)/g, (_, n) => {
400
+ const v = vals[Number(n) - 1];
401
+ if (v === null || v === undefined) return "NULL";
402
+ if (typeof v === "number") return String(v);
403
+ return `'${String(v).replace(/'/g, "''")}'`;
404
+ }));
405
+ return c.json(ok({ inserted: true }), 201);
406
+ } catch (e) {
407
+ return c.json(err((e as Error).message), 500);
408
+ }
409
+ });
410
+
411
+ // ---------------------------------------------------------------------------
412
+ // Export
413
+ // ---------------------------------------------------------------------------
414
+
415
+ /** GET /api/db/connections/:id/export?table=X&schema=Y&format=csv|json&limit=10000 */
416
+ databaseRoutes.get("/connections/:id/export", async (c) => {
417
+ try {
418
+ const conn = resolveConn(c.req.param("id"));
419
+ if (!conn) return c.json(err("Connection not found"), 404);
420
+ const table = c.req.query("table");
421
+ if (!table) return c.json(err("table query param required"), 400);
422
+ const schema = c.req.query("schema");
423
+ const format = c.req.query("format") === "json" ? "json" : "csv";
424
+ const limit = Math.min(parseInt(c.req.query("limit") ?? "10000", 10), 10000);
425
+
426
+ const config = decryptConfig(conn.connection_config);
427
+ const adapter = getAdapter(conn.type);
428
+ const data = await adapter.getTableData(config, table, { schema, page: 1, limit });
429
+
430
+ if (format === "json") {
431
+ return new Response(JSON.stringify(data.rows, null, 2), {
432
+ headers: { "Content-Type": "application/json", "Content-Disposition": `attachment; filename="${table}.json"` },
433
+ });
434
+ }
435
+
436
+ // CSV
437
+ const escape = (val: string): string => {
438
+ if (val.includes(",") || val.includes('"') || val.includes("\n")) return `"${val.replace(/"/g, '""')}"`;
439
+ return val;
440
+ };
441
+ const lines = [data.columns.map(escape).join(",")];
442
+ for (const row of data.rows) {
443
+ lines.push(data.columns.map((col) => escape(String(row[col] ?? ""))).join(","));
444
+ }
445
+ return new Response(lines.join("\n"), {
446
+ headers: { "Content-Type": "text/csv", "Content-Disposition": `attachment; filename="${table}.csv"` },
447
+ });
448
+ } catch (e) {
449
+ return c.json(err((e as Error).message), 500);
450
+ }
451
+ });
452
+
336
453
  // ---------------------------------------------------------------------------
337
454
  // Search
338
455
  // ---------------------------------------------------------------------------
@@ -3,6 +3,7 @@ import { homedir, hostname } from "node:os";
3
3
  import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, chmodSync } from "node:fs";
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { VERSION } from "../version.ts";
6
+ import { configService } from "./config.service.ts";
6
7
 
7
8
  const PPM_DIR = resolve(homedir(), ".ppm");
8
9
  const AUTH_FILE = resolve(PPM_DIR, "cloud-auth.json");
@@ -284,7 +285,7 @@ export async function linkDevice(name?: string): Promise<CloudDevice> {
284
285
  if (!auth) throw new Error("Not logged in. Run 'ppm cloud login' first.");
285
286
 
286
287
  const machineId = getMachineId();
287
- const deviceName = name || hostname() || "Unknown Machine";
288
+ const deviceName = name || (configService.get("device_name") as string) || hostname() || "Unknown Machine";
288
289
 
289
290
  const res = await cloudFetch("/api/devices", {
290
291
  method: "POST",
@@ -31,6 +31,7 @@ export const sqliteAdapter: DatabaseAdapter = {
31
31
  nullable: !c.notnull,
32
32
  pk: !!c.pk,
33
33
  defaultValue: c.dflt_value,
34
+ fk: c.fk,
34
35
  }));
35
36
  },
36
37
 
@@ -2,9 +2,10 @@ import { Database, type SQLQueryBindings } from "bun:sqlite";
2
2
  import { resolve } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { mkdirSync, existsSync } from "node:fs";
5
+ import { encrypt, decrypt } from "../lib/account-crypto.ts";
5
6
 
6
7
  const PPM_DIR = process.env.PPM_HOME || resolve(homedir(), ".ppm");
7
- const CURRENT_SCHEMA_VERSION = 14;
8
+ const CURRENT_SCHEMA_VERSION = 15;
8
9
 
9
10
  let db: Database | null = null;
10
11
  let dbProfile: string | null = null;
@@ -414,6 +415,22 @@ function runMigrations(database: Database): void {
414
415
  PRAGMA user_version = 14;
415
416
  `);
416
417
  }
418
+
419
+ if (current < 15) {
420
+ // Encrypt all existing plaintext connection_config values
421
+ const rows = database.query("SELECT id, connection_config FROM connections").all() as { id: number; connection_config: string }[];
422
+ for (const row of rows) {
423
+ try {
424
+ // Try decrypting — if it works, already encrypted
425
+ decrypt(row.connection_config);
426
+ } catch {
427
+ // Plaintext — encrypt it
428
+ const encrypted = encrypt(row.connection_config);
429
+ database.query("UPDATE connections SET connection_config = ? WHERE id = ?").run(encrypted, row.id);
430
+ }
431
+ }
432
+ database.exec("PRAGMA user_version = 15");
433
+ }
417
434
  }
418
435
 
419
436
  // ---------------------------------------------------------------------------
@@ -796,6 +813,28 @@ export type ConnectionConfig =
796
813
  | { type: "sqlite"; path: string }
797
814
  | { type: "postgres"; connectionString: string };
798
815
 
816
+ /** Encrypt a connection config object for storage */
817
+ function encryptConfig(config: ConnectionConfig): string {
818
+ return encrypt(JSON.stringify(config));
819
+ }
820
+
821
+ /** Decrypt a stored connection_config string, with fallback for pre-migration plaintext */
822
+ export function decryptConfig(encrypted: string): ConnectionConfig {
823
+ try {
824
+ return JSON.parse(decrypt(encrypted));
825
+ } catch {
826
+ // Fallback: might be plaintext (pre-migration or test DB)
827
+ return JSON.parse(encrypted);
828
+ }
829
+ }
830
+
831
+ /** Get decrypted connection config by connection ID */
832
+ export function getConnectionConfig(id: number): ConnectionConfig | null {
833
+ const conn = getConnectionById(id);
834
+ if (!conn) return null;
835
+ return decryptConfig(conn.connection_config);
836
+ }
837
+
799
838
  export function getConnections(): ConnectionRow[] {
800
839
  return getDb().query(
801
840
  "SELECT * FROM connections ORDER BY sort_order, id",
@@ -826,7 +865,7 @@ export function insertConnection(
826
865
  const maxOrder = (getDb().query("SELECT COALESCE(MAX(sort_order), -1) as m FROM connections").get() as { m: number }).m;
827
866
  getDb().query(
828
867
  "INSERT INTO connections (type, name, connection_config, group_name, color, sort_order) VALUES (?, ?, ?, ?, ?, ?)",
829
- ).run(type, name, JSON.stringify(config), groupName ?? null, color ?? null, maxOrder + 1);
868
+ ).run(type, name, encryptConfig(config), groupName ?? null, color ?? null, maxOrder + 1);
830
869
  return getConnectionByName(name)!;
831
870
  }
832
871
 
@@ -843,7 +882,7 @@ export function updateConnection(
843
882
  const sets: string[] = [];
844
883
  const vals: unknown[] = [];
845
884
  if (updates.name !== undefined) { sets.push("name = ?"); vals.push(updates.name); }
846
- if (updates.config !== undefined) { sets.push("connection_config = ?"); vals.push(JSON.stringify(updates.config)); }
885
+ if (updates.config !== undefined) { sets.push("connection_config = ?"); vals.push(encryptConfig(updates.config)); }
847
886
  if (updates.groupName !== undefined) { sets.push("group_name = ?"); vals.push(updates.groupName); }
848
887
  if (updates.color !== undefined) { sets.push("color = ?"); vals.push(updates.color); }
849
888
  if (updates.readonly !== undefined) { sets.push("readonly = ?"); vals.push(updates.readonly); }
@@ -12,6 +12,7 @@ export interface PgColumnInfo {
12
12
  nullable: boolean;
13
13
  pk: boolean;
14
14
  defaultValue: string | null;
15
+ fk: { table: string; column: string } | null;
15
16
  }
16
17
 
17
18
  export interface PgQueryResult {
@@ -19,6 +20,7 @@ export interface PgQueryResult {
19
20
  rows: Record<string, unknown>[];
20
21
  rowsAffected: number;
21
22
  changeType: "select" | "modify";
23
+ executionTimeMs: number;
22
24
  }
23
25
 
24
26
  /** Auto-close idle connections after 5 minutes */
@@ -40,7 +42,13 @@ class PostgresService {
40
42
  cached.timer = setTimeout(() => this.disconnect(connectionString), IDLE_TIMEOUT_MS);
41
43
  return cached.sql;
42
44
  }
43
- const sql = postgres(connectionString, { max: 3, idle_timeout: 60 });
45
+ // Parse sslmode from connection string to configure SSL properly
46
+ const url = new URL(connectionString);
47
+ const sslmode = url.searchParams.get("sslmode");
48
+ const sslOpts = sslmode === "no-verify" || sslmode === "require"
49
+ ? { rejectUnauthorized: false }
50
+ : sslmode === "disable" ? false : undefined;
51
+ const sql = postgres(connectionString, { max: 3, idle_timeout: 60, ssl: sslOpts as any });
44
52
  const timer = setTimeout(() => this.disconnect(connectionString), IDLE_TIMEOUT_MS);
45
53
  this.cache.set(connectionString, { sql, timer });
46
54
  return sql;
@@ -82,7 +90,7 @@ class PostgresService {
82
90
  }));
83
91
  }
84
92
 
85
- /** Get column schema for a table */
93
+ /** Get column schema for a table (with FK metadata) */
86
94
  async getTableSchema(connectionString: string, table: string, schema = "public"): Promise<PgColumnInfo[]> {
87
95
  const sql = this.connect(connectionString);
88
96
  const cols = await sql`
@@ -97,12 +105,32 @@ class PostgresService {
97
105
  WHERE c.table_schema = ${schema} AND c.table_name = ${table}
98
106
  ORDER BY c.ordinal_position
99
107
  `;
108
+
109
+ // Query FK references
110
+ const fkRows = await sql`
111
+ SELECT kcu.column_name as from_col,
112
+ ccu.table_name as ref_table,
113
+ ccu.column_name as ref_col
114
+ FROM information_schema.table_constraints tc
115
+ JOIN information_schema.key_column_usage kcu
116
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
117
+ JOIN information_schema.constraint_column_usage ccu
118
+ ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
119
+ WHERE tc.constraint_type = 'FOREIGN KEY'
120
+ AND tc.table_schema = ${schema} AND tc.table_name = ${table}
121
+ `;
122
+ const fkMap = new Map<string, { table: string; column: string }>();
123
+ for (const fk of fkRows) {
124
+ fkMap.set(fk.from_col as string, { table: fk.ref_table as string, column: fk.ref_col as string });
125
+ }
126
+
100
127
  return cols.map((c) => ({
101
128
  name: c.name as string,
102
129
  type: c.type as string,
103
- nullable: c.nullable as boolean,
104
- pk: c.pk as boolean,
130
+ nullable: c.nullable === true || c.nullable === "true" || c.nullable === "t",
131
+ pk: c.pk === true || c.pk === "true" || c.pk === "t",
105
132
  defaultValue: c.default_value as string | null,
133
+ fk: fkMap.get(c.name as string) ?? null,
106
134
  }));
107
135
  }
108
136
 
@@ -139,14 +167,17 @@ class PostgresService {
139
167
  const isSelect = trimmed.startsWith("SELECT") || trimmed.startsWith("EXPLAIN") ||
140
168
  trimmed.startsWith("SHOW") || trimmed.startsWith("\\D");
141
169
 
170
+ const start = performance.now();
142
171
  if (isSelect) {
143
172
  const rows = await sql.unsafe(sqlText);
173
+ const executionTimeMs = Math.round(performance.now() - start);
144
174
  const columns = rows.length > 0 ? Object.keys(rows[0]!) : [];
145
- return { columns, rows: rows as unknown as Record<string, unknown>[], rowsAffected: 0, changeType: "select" };
175
+ return { columns, rows: rows as unknown as Record<string, unknown>[], rowsAffected: 0, changeType: "select", executionTimeMs };
146
176
  }
147
177
 
148
178
  const result = await sql.unsafe(sqlText);
149
- return { columns: [], rows: [], rowsAffected: result.count ?? 0, changeType: "modify" };
179
+ const executionTimeMs = Math.round(performance.now() - start);
180
+ return { columns: [], rows: [], rowsAffected: result.count ?? 0, changeType: "modify", executionTimeMs };
150
181
  }
151
182
 
152
183
  /** Update a single cell value */
@@ -14,6 +14,7 @@ export interface ColumnInfo {
14
14
  notnull: boolean;
15
15
  pk: boolean;
16
16
  dflt_value: string | null;
17
+ fk: { table: string; column: string } | null;
17
18
  }
18
19
 
19
20
  export interface QueryResult {
@@ -21,6 +22,7 @@ export interface QueryResult {
21
22
  rows: Record<string, unknown>[];
22
23
  rowsAffected: number;
23
24
  changeType: "select" | "modify";
25
+ executionTimeMs: number;
24
26
  }
25
27
 
26
28
  /** Auto-close idle databases after 5 minutes */
@@ -81,11 +83,20 @@ class SqliteService {
81
83
  });
82
84
  }
83
85
 
84
- /** Get column schema for a table */
86
+ /** Get column schema for a table (with FK metadata) */
85
87
  getTableSchema(projectPath: string, dbPath: string, table: string): ColumnInfo[] {
86
88
  const abs = this.resolvePath(projectPath, dbPath);
87
89
  const db = this.open(abs);
88
- return db.query(`PRAGMA table_info("${table}")`).all() as ColumnInfo[];
90
+ const cols = db.query(`PRAGMA table_info("${table}")`).all() as Omit<ColumnInfo, "fk">[];
91
+
92
+ // Build FK map from PRAGMA foreign_key_list
93
+ const fkRows = db.query(`PRAGMA foreign_key_list("${table}")`).all() as { from: string; table: string; to: string }[];
94
+ const fkMap = new Map<string, { table: string; column: string }>();
95
+ for (const fk of fkRows) {
96
+ fkMap.set(fk.from, { table: fk.table, column: fk.to });
97
+ }
98
+
99
+ return cols.map((c) => ({ ...c, fk: fkMap.get(c.name) ?? null }));
89
100
  }
90
101
 
91
102
  /** Get paginated rows from a table */
@@ -115,15 +126,18 @@ class SqliteService {
115
126
  const trimmed = sql.trim().toUpperCase();
116
127
  const isSelect = trimmed.startsWith("SELECT") || trimmed.startsWith("PRAGMA") || trimmed.startsWith("EXPLAIN");
117
128
 
129
+ const start = performance.now();
118
130
  if (isSelect) {
119
131
  const stmt = db.query(sql);
120
132
  const rows = stmt.all() as Record<string, unknown>[];
133
+ const executionTimeMs = Math.round(performance.now() - start);
121
134
  const columns = rows.length > 0 ? Object.keys(rows[0]!) : [];
122
- return { columns, rows, rowsAffected: 0, changeType: "select" };
135
+ return { columns, rows, rowsAffected: 0, changeType: "select", executionTimeMs };
123
136
  }
124
137
 
125
138
  const result = db.run(sql);
126
- return { columns: [], rows: [], rowsAffected: result.changes, changeType: "modify" };
139
+ const executionTimeMs = Math.round(performance.now() - start);
140
+ return { columns: [], rows: [], rowsAffected: result.changes, changeType: "modify", executionTimeMs };
127
141
  }
128
142
 
129
143
  /** Update a single cell value */
@@ -1,9 +1,8 @@
1
1
  import {
2
2
  getCachedTables, upsertTableCache, deleteTableCache, searchTableCache,
3
- getConnectionById, type TableCacheRow,
3
+ getConnectionById, decryptConfig, type TableCacheRow,
4
4
  } from "./db.service.ts";
5
5
  import { getAdapter } from "./database/adapter-registry.ts";
6
- import type { DbConnectionConfig } from "../types/database.ts";
7
6
 
8
7
  export interface CachedTable {
9
8
  connectionId: number;
@@ -42,7 +41,7 @@ export async function syncTables(connectionId: number): Promise<CachedTable[]> {
42
41
  const conn = getConnectionById(connectionId);
43
42
  if (!conn) throw new Error(`Connection not found: ${connectionId}`);
44
43
 
45
- const config = JSON.parse(conn.connection_config) as DbConnectionConfig;
44
+ const config = decryptConfig(conn.connection_config);
46
45
  const adapter = getAdapter(conn.type);
47
46
  const tables = await adapter.getTables(config);
48
47
 
@@ -19,6 +19,7 @@ export interface DbColumnInfo {
19
19
  nullable: boolean;
20
20
  pk: boolean;
21
21
  defaultValue: string | null;
22
+ fk: { table: string; column: string } | null;
22
23
  }
23
24
 
24
25
  export interface DbQueryResult {
@@ -26,6 +27,7 @@ export interface DbQueryResult {
26
27
  rows: Record<string, unknown>[];
27
28
  rowsAffected: number;
28
29
  changeType: "select" | "modify";
30
+ executionTimeMs: number;
29
31
  }
30
32
 
31
33
  export interface DbPagedData {
@@ -15,6 +15,8 @@ interface ConnectionFormDialogProps {
15
15
  onSave?: (data: CreateConnectionData) => Promise<void>;
16
16
  onUpdate?: (id: number, data: UpdateConnectionData) => Promise<void>;
17
17
  onTest: (id: number) => Promise<{ ok: boolean; error?: string }>;
18
+ /** Test raw (unsaved) connection config — enables Test button in create mode */
19
+ onTestRaw?: (type: "sqlite" | "postgres", config: { type: string; path?: string; connectionString?: string }) => Promise<{ ok: boolean; error?: string }>;
18
20
  }
19
21
 
20
22
  interface FormState {
@@ -28,7 +30,7 @@ interface FormState {
28
30
  }
29
31
 
30
32
  export function ConnectionFormDialog({
31
- open, onClose, connection, onSave, onUpdate, onTest,
33
+ open, onClose, connection, onSave, onUpdate, onTest, onTestRaw,
32
34
  }: ConnectionFormDialogProps) {
33
35
  const isEdit = !!connection;
34
36
  const [form, setForm] = useState<FormState>({
@@ -63,11 +65,20 @@ export function ConnectionFormDialog({
63
65
  };
64
66
 
65
67
  const handleTest = async () => {
66
- if (!isEdit) return;
67
68
  setTesting(true);
68
69
  setTestResult(null);
69
70
  try {
70
- const result = await onTest(connection!.id);
71
+ let result: { ok: boolean; error?: string };
72
+ if (isEdit) {
73
+ result = await onTest(connection!.id);
74
+ } else if (onTestRaw) {
75
+ const config = form.type === "postgres"
76
+ ? { type: "postgres" as const, connectionString: form.connectionString }
77
+ : { type: "sqlite" as const, path: form.path };
78
+ result = await onTestRaw(form.type, config);
79
+ } else {
80
+ result = { ok: false, error: "Save connection first" };
81
+ }
71
82
  setTestResult(result);
72
83
  } finally {
73
84
  setTesting(false);
@@ -227,11 +238,9 @@ export function ConnectionFormDialog({
227
238
  </div>
228
239
 
229
240
  <DialogFooter>
230
- {isEdit && (
231
- <Button variant="outline" size="sm" onClick={handleTest} disabled={testing} className="mr-auto">
232
- {testing ? "Testing…" : "Test Connection"}
233
- </Button>
234
- )}
241
+ <Button variant="outline" size="sm" onClick={handleTest} disabled={testing} className="mr-auto">
242
+ {testing ? "Testing…" : "Test Connection"}
243
+ </Button>
235
244
  <Button variant="outline" size="sm" onClick={onClose}>Cancel</Button>
236
245
  <Button size="sm" onClick={handleSave} disabled={saving}>
237
246
  {saving ? "Saving…" : isEdit ? "Save" : "Add"}