@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.
- package/CHANGELOG.md +26 -0
- package/dist/web/assets/{_basePickBy-3Xe18azI.js → _basePickBy-5PGDJbfF.js} +1 -1
- package/dist/web/assets/{_baseUniq-Yy35llnn.js → _baseUniq-BT4Ow4Kk.js} +1 -1
- package/dist/web/assets/{api-settings-CgBII8jW.js → api-settings-Bn-bIxD1.js} +1 -1
- package/dist/web/assets/{arc-B9n1Gvb5.js → arc-BAOivWpI.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DqAZP_F6.js → architectureDiagram-2XIMDMQ5-Z-4eN4za.js} +1 -1
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-h3cDF2vI.js → blockDiagram-WCTKOSBZ-BCLqzhuZ.js} +1 -1
- package/dist/web/assets/{c4Diagram-IC4MRINW--pF1r5lr.js → c4Diagram-IC4MRINW-0Vp0Jeas.js} +1 -1
- package/dist/web/assets/channel-By7bn0Yq.js +1 -0
- package/dist/web/assets/chat-tab-onkz52iv.js +10 -0
- package/dist/web/assets/{chunk-4BX2VUAB-C3aZvW7B.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-D5cABeB9.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-CkFGv6Zs.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-Dvbyu4Zw.js → chunk-7R4GIKGN-Dv-4cAYn.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-CtqKiH4q.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-Cpr87sBR.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-D23YVTOU.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-tDjHsAUs.js → chunk-GEFDOKGD-D-pKjlVd.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-BBmymCjA.js → chunk-JSJVCQXG-99JzIdPr.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-DP36BDiU.js → chunk-KX2RTZJC-CRq1OBZv.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-Djw13C-3.js → chunk-KYZI473N-Bb0MCaIO.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-HG_eMj_C.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-C2UEioMs.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-DXUTQ-BL.js → chunk-NQ4KR5QH-z_blpjxi.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-BsUWb9d0.js → chunk-O4XLMI2P-nDhi_cVu.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-rG0P22U9.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-DX0xW7kO.js → chunk-PQ6SQG4A-TF58UVMU.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-C7Gry6md.js → chunk-PU5JKC2W-ek7k4QVB.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-CMY0PkRK.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-CXuQvlyu.js → chunk-WL4C6EOR-ByUrSRin.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-DRJEb7Zb.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-BPEX8KhL.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-Cb0iqycX.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-av5aeHLq.js → chunk-YBOYWFTD-rQG3QH5s.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +1 -0
- package/dist/web/assets/clone-LRxlvnMj.js +1 -0
- package/dist/web/assets/code-editor-BixOXePn.js +8 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-qudEiMCT.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
- package/dist/web/assets/csv-parser-CNNw2RVA.js +6 -0
- package/dist/web/assets/{csv-preview-sx6DC51G.js → csv-preview-D2pJJj3K.js} +3 -8
- package/dist/web/assets/{dagre-BFcnKyBF.js → dagre-DHq9bhnd.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-C3O-MTLf.js → dagre-KLK3FWXG-BdJr7Byp.js} +1 -1
- package/dist/web/assets/database-viewer-DfLe8ewt.js +2 -0
- package/dist/web/assets/{diagram-E7M64L7V-DxPjK7_c.js → diagram-E7M64L7V-_db4pBVA.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-sqTog_XV.js → diagram-IFDJBPK2-xKoeuiJx.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-hzmp0GHK.js → diagram-P4PSJMXO-C8tjJsev.js} +1 -1
- package/dist/web/assets/diff-viewer-eFO08m_L.js +4 -0
- package/dist/web/assets/dist-DIV6WgAG.js +41 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-DLeYhAAT.js → erDiagram-INFDFZHY-BSh2z9Df.js} +1 -1
- package/dist/web/assets/{extension-webview-2XjXXtyy.js → extension-webview-4CL9kCKR.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-CRxlE9Sr.js → flowDiagram-PKNHOUZH-oYaovqyp.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-BdjmoMLS.js → ganttDiagram-A5KZAMGK-DmL26q2P.js} +1 -1
- package/dist/web/assets/git-graph-BqeE_o17.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js → gitGraphDiagram-K3NZZRJ6-CMoukSrY.js} +1 -1
- package/dist/web/assets/{graphlib-Duh_bWLa.js → graphlib-BcsNnGcW.js} +1 -1
- package/dist/web/assets/index-BWLy2h18.css +2 -0
- package/dist/web/assets/index-DwrCg0TN.js +30 -0
- package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +2 -0
- package/dist/web/assets/{isEmpty-B9L-Ge-H.js → isEmpty-bnrF3Qbc.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js → ishikawaDiagram-PHBUUO56-D05_LyL7.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-CgDI-UG4.js → journeyDiagram-4ABVD52K-B_L20qMe.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-h4g10UHL.js → kanban-definition-K7BYSVSG-CZ535BbZ.js} +1 -1
- package/dist/web/assets/keybindings-store-BNBONtSd.js +1 -0
- package/dist/web/assets/{line-B75-Rx70.js → line-CVvo3dRu.js} +1 -1
- package/dist/web/assets/{linear-Bcjv9FQt.js → linear-DP4mkX3m.js} +1 -1
- package/dist/web/assets/{markdown-renderer-Bb7OSpxF.js → markdown-renderer-BUqab2os.js} +5 -5
- package/dist/web/assets/{mermaid-parser.core-8u2leTXI.js → mermaid-parser.core-C7UwoIh6.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-BaOBwb-W.js → mindmap-definition-YRQLILUH-x0MTutJp.js} +1 -1
- package/dist/web/assets/{ordinal-LFEjVtwQ.js → ordinal-_K3x1fkz.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-At5Kz0KK.js → pieDiagram-SKSYHLDU-C1Gjrtzy.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-bD8MKumH.js → port-forwarding-tab-CfO-UJ84.js} +1 -1
- package/dist/web/assets/postgres-viewer-BVJZ44eU.js +13 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-CdjGIDfw.js → quadrantDiagram-337W2JSQ-C8bzJCjQ.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-B9F_Cx_p.js → requirementDiagram-Z7DCOOCP-pQyah6WB.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-RolPi8bU.js → sankeyDiagram-WA2Y5GQK-T6RgG-N8.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-DM-tMAhx.js → sequenceDiagram-2WXFIKYE-BQDJ4CVs.js} +1 -1
- package/dist/web/assets/settings-tab-C6hdJujW.js +1 -0
- package/dist/web/assets/sql-completion-provider-DM9Qov6L.js +1 -0
- package/dist/web/assets/sql-query-editor-OhZa4Z9F.js +3 -0
- package/dist/web/assets/sqlite-viewer-C8p1_jz4.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-C4EMl6jf.js → stateDiagram-RAJIS63D-66vhiIuk.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +1 -0
- package/dist/web/assets/{terminal-tab-Cq6vQ9W9.js → terminal-tab-CaO0WnIo.js} +2 -2
- package/dist/web/assets/text-wrap-BWNOVswA.js +1 -0
- package/dist/web/assets/{timeline-definition-YZTLITO2-A4PN_Efm.js → timeline-definition-YZTLITO2-DwZqB3nn.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
- package/dist/web/assets/use-monaco-theme-U9ZhfvHB.js +11 -0
- package/dist/web/assets/{vennDiagram-LZ73GAT5-ywK7LMaH.js → vennDiagram-LZ73GAT5-s9Z71fz-.js} +1 -1
- package/dist/web/assets/x-D2_KzIET.js +1 -0
- package/dist/web/assets/{xychartDiagram-JWTSCODW-DylHYNtJ.js → xychartDiagram-JWTSCODW-DRa_TH4B.js} +1 -1
- package/dist/web/index.html +9 -8
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/cli/commands/bot-cmd.ts +4 -0
- package/src/cli/commands/db-cmd.ts +4 -3
- package/src/server/routes/database.ts +126 -9
- package/src/services/cloud.service.ts +2 -1
- package/src/services/database/sqlite-adapter.ts +1 -0
- package/src/services/db.service.ts +42 -3
- package/src/services/postgres.service.ts +37 -6
- package/src/services/sqlite.service.ts +18 -4
- package/src/services/table-cache.service.ts +2 -3
- package/src/types/database.ts +2 -0
- package/src/web/components/database/connection-form-dialog.tsx +17 -8
- package/src/web/components/database/connection-list.tsx +191 -139
- package/src/web/components/database/data-grid.tsx +634 -0
- package/src/web/components/database/database-sidebar.tsx +4 -1
- package/src/web/components/database/database-viewer.tsx +204 -225
- package/src/web/components/database/export-button.tsx +100 -0
- package/src/web/components/database/sql-completion-provider.ts +301 -0
- package/src/web/components/database/sql-query-editor.tsx +123 -0
- package/src/web/components/database/use-connections.ts +21 -1
- package/src/web/components/database/use-database.ts +59 -7
- package/src/web/components/editor/code-editor.tsx +224 -16
- package/src/web/components/sqlite/sqlite-query-editor.tsx +3 -90
- package/src/web/components/sqlite/sqlite-viewer.tsx +0 -2
- package/src/web/components/sqlite/use-sqlite.ts +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +0 -1
- package/dist/web/assets/channel-C2fMafck.js +0 -1
- package/dist/web/assets/chat-tab-CfdMDCBK.js +0 -10
- package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +0 -1
- package/dist/web/assets/clone-B2hUek6n.js +0 -1
- package/dist/web/assets/code-editor-DhCfJIpG.js +0 -2
- package/dist/web/assets/database-viewer-DLkAUBpm.js +0 -1
- package/dist/web/assets/diff-viewer-DWVWsekJ.js +0 -4
- package/dist/web/assets/dist-C40JmyoH.js +0 -13
- package/dist/web/assets/dist-DRTW9IWi.js +0 -41
- package/dist/web/assets/git-graph-Ds5bs1cM.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +0 -1
- package/dist/web/assets/index-8b0LM6IC.js +0 -30
- package/dist/web/assets/index-BnMECpW3.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +0 -2
- package/dist/web/assets/keybindings-store-C06Z0Zhk.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +0 -1
- package/dist/web/assets/postgres-viewer-C3OND65T.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +0 -1
- package/dist/web/assets/settings-tab-cuMIkUNV.js +0 -1
- package/dist/web/assets/sqlite-viewer-CpjnwDtk.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +0 -1
- package/dist/web/assets/use-monaco-theme-BH9sQ-Yu.js +0 -11
- /package/dist/web/assets/{api-client-BKIT_Qeg.js → api-client-BfBM3I7n.js} +0 -0
- /package/dist/web/assets/{array-DqLCdDFv.js → array-B9UHiPd-.js} +0 -0
- /package/dist/web/assets/{chevron-right-5HgK6l7K.js → chevron-right-4zq1jPv6.js} +0 -0
- /package/dist/web/assets/{columns-2-cEVJHYd7.js → columns-2-BoZAN-iw.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-CWPXKqbJ.js → cytoscape.esm-BW-DbntU.js} +0 -0
- /package/dist/web/assets/{defaultLocale-CrJzLgRD.js → defaultLocale-5eAKkKJC.js} +0 -0
- /package/dist/web/assets/{dist-Cep75xXf.js → dist-CSJdAyA9.js} +0 -0
- /package/dist/web/assets/{init-C0r9Gk5G.js → init-DlZdxViB.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-CGBoxvCD.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
- /package/dist/web/assets/{katex-DzXRfQ_m.js → katex-Bqvo_ZG0.js} +0 -0
- /package/dist/web/assets/{lib-mag4ySk-.js → lib-DurwGtQO.js} +0 -0
- /package/dist/web/assets/{math-y9zN1W-N.js → math-069Z4SuC.js} +0 -0
- /package/dist/web/assets/{path-DIKpVbHL.js → path-6uRLdFF7.js} +0 -0
- /package/dist/web/assets/{rough.esm-nHaDi0Kw.js → rough.esm-JX0wREDd.js} +0 -0
- /package/dist/web/assets/{src-Dw4QhedI.js → src-BqX54PbV.js} +0 -0
- /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 }) =>
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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",
|
|
@@ -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 =
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
|
104
|
-
pk: c.pk
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
44
|
+
const config = decryptConfig(conn.connection_config);
|
|
46
45
|
const adapter = getAdapter(conn.type);
|
|
47
46
|
const tables = await adapter.getTables(config);
|
|
48
47
|
|
package/src/types/database.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
{
|
|
231
|
-
|
|
232
|
-
|
|
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"}
|