dbdiff-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/bin/cli.js +83 -0
- package/bin/install-local.js +57 -0
- package/electron/generate-icon.mjs +54 -0
- package/electron/icon.icns +0 -0
- package/electron/icon.png +0 -0
- package/electron/icon.svg +21 -0
- package/electron/main.js +169 -0
- package/electron/patch-dev-plist.js +31 -0
- package/electron/preload.cjs +18 -0
- package/electron/wait-for-vite.js +43 -0
- package/index.html +13 -0
- package/package.json +91 -0
- package/public/favicon.svg +15 -0
- package/public/vite.svg +1 -0
- package/server/export.ts +57 -0
- package/server/index.ts +392 -0
- package/src/App.css +1 -0
- package/src/App.tsx +543 -0
- package/src/assets/react.svg +1 -0
- package/src/components/CommandPalette.tsx +243 -0
- package/src/components/ConnectedView.tsx +78 -0
- package/src/components/ConnectionPicker.tsx +381 -0
- package/src/components/ConsoleView.tsx +360 -0
- package/src/components/CsvExportModal.tsx +144 -0
- package/src/components/DataGrid/DataGrid.tsx +262 -0
- package/src/components/DataGrid/DataGridCell.tsx +73 -0
- package/src/components/DataGrid/DataGridHeader.tsx +89 -0
- package/src/components/DataGrid/index.ts +20 -0
- package/src/components/DataGrid/types.ts +63 -0
- package/src/components/DataGrid/useColumnResize.ts +153 -0
- package/src/components/DataGrid/useDataGridSelection.ts +340 -0
- package/src/components/DataGrid/utils.ts +184 -0
- package/src/components/DatabaseMenu.tsx +93 -0
- package/src/components/DatabaseSwitcher.tsx +208 -0
- package/src/components/DiffView.tsx +215 -0
- package/src/components/EditConnectionModal.tsx +417 -0
- package/src/components/ErrorBoundary.tsx +69 -0
- package/src/components/GlobalShortcuts.tsx +201 -0
- package/src/components/InnerTabBar.tsx +129 -0
- package/src/components/JsonTreeViewer.tsx +387 -0
- package/src/components/MemberAccessEditor.tsx +443 -0
- package/src/components/MembersModal.tsx +446 -0
- package/src/components/NewConnectionModal.tsx +274 -0
- package/src/components/Resizer.tsx +66 -0
- package/src/components/ScanSuccessModal.tsx +113 -0
- package/src/components/ShortcutSettingsModal.tsx +318 -0
- package/src/components/Sidebar.tsx +532 -0
- package/src/components/TabBar.tsx +188 -0
- package/src/components/TableView.tsx +2147 -0
- package/src/components/ThemeToggle.tsx +44 -0
- package/src/components/index.ts +17 -0
- package/src/constants.ts +12 -0
- package/src/electron.d.ts +12 -0
- package/src/index.css +44 -0
- package/src/main.tsx +13 -0
- package/src/stores/hooks.ts +1146 -0
- package/src/stores/index.ts +12 -0
- package/src/stores/store.ts +1514 -0
- package/src/stores/useCloudSync.ts +274 -0
- package/src/stores/useSyncDatabase.ts +422 -0
- package/src/types.ts +277 -0
- package/src/utils/csv.ts +27 -0
- package/src/vite-env.d.ts +2 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/tsconfig.server.json +14 -0
- package/vite.config.ts +14 -0
package/public/vite.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
package/server/export.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import type { DatabaseConfigConnection } from "../src/types.js";
|
|
3
|
+
|
|
4
|
+
export function pgDump(
|
|
5
|
+
connection: DatabaseConfigConnection,
|
|
6
|
+
schemaOnly: boolean,
|
|
7
|
+
): Promise<string> {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const args = [
|
|
10
|
+
"-h",
|
|
11
|
+
connection.host,
|
|
12
|
+
"-p",
|
|
13
|
+
String(connection.port),
|
|
14
|
+
"-U",
|
|
15
|
+
connection.username,
|
|
16
|
+
"-d",
|
|
17
|
+
connection.database,
|
|
18
|
+
"--no-password",
|
|
19
|
+
"--format=plain",
|
|
20
|
+
];
|
|
21
|
+
if (schemaOnly) args.push("--schema-only");
|
|
22
|
+
|
|
23
|
+
const proc = spawn("pg_dump", args, {
|
|
24
|
+
env: { ...process.env, PGPASSWORD: connection.password },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
let stdout = "";
|
|
28
|
+
let stderr = "";
|
|
29
|
+
proc.stdout.on("data", (chunk: Buffer) => {
|
|
30
|
+
stdout += chunk;
|
|
31
|
+
});
|
|
32
|
+
proc.stderr.on("data", (chunk: Buffer) => {
|
|
33
|
+
stderr += chunk;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
proc.on("error", (err) => {
|
|
37
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
38
|
+
reject(
|
|
39
|
+
new Error(
|
|
40
|
+
"pg_dump not found. Install PostgreSQL client tools:\n" +
|
|
41
|
+
" macOS: brew install postgresql\n" +
|
|
42
|
+
" Ubuntu: sudo apt install postgresql-client\n" +
|
|
43
|
+
" Windows: https://www.postgresql.org/download/windows/",
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
} else {
|
|
47
|
+
reject(err);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
proc.on("close", (code) => {
|
|
52
|
+
code === 0
|
|
53
|
+
? resolve(stdout)
|
|
54
|
+
: reject(new Error(stderr || `pg_dump exited with code ${code}`));
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
package/server/index.ts
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import pg from "pg";
|
|
6
|
+
import type {
|
|
7
|
+
QueryRequest,
|
|
8
|
+
QueryResponse,
|
|
9
|
+
QueryErrorResponse,
|
|
10
|
+
ScanLocalhostResponse,
|
|
11
|
+
ExportType,
|
|
12
|
+
DatabaseConfigConnection,
|
|
13
|
+
DiffResponse,
|
|
14
|
+
DiffTableResult,
|
|
15
|
+
QueryFieldInfo,
|
|
16
|
+
} from "../src/types.js";
|
|
17
|
+
import { LOCALHOST_SCANNING_ENABLED } from "../src/constants.js";
|
|
18
|
+
import { pgDump } from "./export.js";
|
|
19
|
+
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
// Find project root (where package.json lives) — works whether run from
|
|
23
|
+
// server/index.ts (dev) or dist-server/server/index.js (prod)
|
|
24
|
+
let projectRoot = __dirname;
|
|
25
|
+
while (
|
|
26
|
+
!fs.existsSync(path.join(projectRoot, "package.json")) &&
|
|
27
|
+
projectRoot !== path.dirname(projectRoot)
|
|
28
|
+
) {
|
|
29
|
+
projectRoot = path.dirname(projectRoot);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const app = express();
|
|
33
|
+
const port = parseInt(process.env.PORT || "4088");
|
|
34
|
+
|
|
35
|
+
const distPath = path.join(projectRoot, "dist");
|
|
36
|
+
const hasBuiltFrontend = fs.existsSync(path.join(distPath, "index.html"));
|
|
37
|
+
|
|
38
|
+
app.use(express.json());
|
|
39
|
+
|
|
40
|
+
/** Build a PostgreSQL connection string from a DatabaseConfigConnection */
|
|
41
|
+
function buildConnectionString(conn: {
|
|
42
|
+
username: string;
|
|
43
|
+
password: string;
|
|
44
|
+
host: string;
|
|
45
|
+
port: number;
|
|
46
|
+
database: string;
|
|
47
|
+
params?: Record<string, string>;
|
|
48
|
+
}): string {
|
|
49
|
+
const params =
|
|
50
|
+
conn.params && Object.keys(conn.params).length > 0
|
|
51
|
+
? "?" + new URLSearchParams(conn.params).toString()
|
|
52
|
+
: "";
|
|
53
|
+
return `postgresql://${encodeURIComponent(conn.username)}:${encodeURIComponent(conn.password)}@${conn.host}:${conn.port}/${encodeURIComponent(conn.database)}${params}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Dummy endpoint for testing
|
|
57
|
+
app.get("/api/ping", (_req, res) => {
|
|
58
|
+
res.json({ message: "pong", timestamp: Date.now() });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Execute SQL query against a database connection
|
|
62
|
+
app.post(
|
|
63
|
+
"/api/query",
|
|
64
|
+
async (req, res: express.Response<QueryResponse | QueryErrorResponse>) => {
|
|
65
|
+
const { connection, query } = req.body as QueryRequest;
|
|
66
|
+
|
|
67
|
+
if (!connection || !query) {
|
|
68
|
+
res.status(400).json({ error: "Missing connection or query" });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const client = new pg.Client({
|
|
73
|
+
connectionString: buildConnectionString(connection),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await client.connect();
|
|
78
|
+
const result = await client.query(query);
|
|
79
|
+
// pg returns an array of QueryResult when multiple statements are executed
|
|
80
|
+
const lastResult = Array.isArray(result)
|
|
81
|
+
? result[result.length - 1]
|
|
82
|
+
: result;
|
|
83
|
+
const response: QueryResponse = {
|
|
84
|
+
rows: lastResult.rows ?? [],
|
|
85
|
+
fields: (lastResult.fields ?? []).map((f: pg.FieldDef) => ({
|
|
86
|
+
name: f.name,
|
|
87
|
+
dataTypeID: f.dataTypeID,
|
|
88
|
+
})),
|
|
89
|
+
rowCount: lastResult.rowCount,
|
|
90
|
+
};
|
|
91
|
+
res.json(response);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
94
|
+
res.status(500).json({ error: message });
|
|
95
|
+
} finally {
|
|
96
|
+
await client.end();
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Preview diff of DML statements (INSERT/UPDATE/DELETE) via rolled-back transaction
|
|
102
|
+
app.post(
|
|
103
|
+
"/api/query-diff",
|
|
104
|
+
async (req, res: express.Response<DiffResponse | QueryErrorResponse>) => {
|
|
105
|
+
const { connection, query } = req.body as QueryRequest;
|
|
106
|
+
|
|
107
|
+
if (!connection || !query) {
|
|
108
|
+
res.status(400).json({ error: "Missing connection or query" });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Parse table names from DML statements
|
|
113
|
+
const tableRegex =
|
|
114
|
+
/(?:INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+(?:"?(\w+)"?\.)?"?(\w+)"?/gi;
|
|
115
|
+
const tableNames = new Set<string>();
|
|
116
|
+
let match;
|
|
117
|
+
while ((match = tableRegex.exec(query)) !== null) {
|
|
118
|
+
const schema = match[1] || "public";
|
|
119
|
+
const table = match[2];
|
|
120
|
+
tableNames.add(`${schema}.${table}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (tableNames.size === 0) {
|
|
124
|
+
res
|
|
125
|
+
.status(400)
|
|
126
|
+
.json({ error: "No INSERT/UPDATE/DELETE statements detected" });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const client = new pg.Client({
|
|
131
|
+
connectionString: buildConnectionString(connection),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
await client.connect();
|
|
136
|
+
await client.query("BEGIN");
|
|
137
|
+
|
|
138
|
+
// For each table, get PK columns and snapshot before state
|
|
139
|
+
const tableInfo: Map<
|
|
140
|
+
string,
|
|
141
|
+
{
|
|
142
|
+
schema: string;
|
|
143
|
+
table: string;
|
|
144
|
+
pkColumns: string[];
|
|
145
|
+
columns: QueryFieldInfo[];
|
|
146
|
+
before: Record<string, unknown>[];
|
|
147
|
+
}
|
|
148
|
+
> = new Map();
|
|
149
|
+
|
|
150
|
+
for (const fullName of tableNames) {
|
|
151
|
+
const [schema, table] = fullName.split(".");
|
|
152
|
+
|
|
153
|
+
// Get primary key columns
|
|
154
|
+
const pkResult = await client.query(
|
|
155
|
+
`SELECT a.attname
|
|
156
|
+
FROM pg_constraint c
|
|
157
|
+
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
|
|
158
|
+
WHERE c.contype = 'p'
|
|
159
|
+
AND c.conrelid = (
|
|
160
|
+
SELECT oid FROM pg_class
|
|
161
|
+
WHERE relname = $1
|
|
162
|
+
AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = $2)
|
|
163
|
+
)
|
|
164
|
+
ORDER BY array_position(c.conkey, a.attnum)`,
|
|
165
|
+
[table, schema],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const pkColumns = pkResult.rows.map(
|
|
169
|
+
(r: { attname: string }) => r.attname,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (pkColumns.length === 0) {
|
|
173
|
+
await client.query("ROLLBACK");
|
|
174
|
+
res.status(400).json({
|
|
175
|
+
error: `Table "${schema}"."${table}" has no primary key. Diff requires a primary key to identify rows.`,
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Snapshot before
|
|
181
|
+
const orderBy = pkColumns.map((c: string) => `"${c}"`).join(", ");
|
|
182
|
+
const beforeResult = await client.query(
|
|
183
|
+
`SELECT * FROM "${schema}"."${table}" ORDER BY ${orderBy} LIMIT 10000`,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const columns: QueryFieldInfo[] = (beforeResult.fields ?? []).map(
|
|
187
|
+
(f: pg.FieldDef) => ({
|
|
188
|
+
name: f.name,
|
|
189
|
+
dataTypeID: f.dataTypeID,
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
tableInfo.set(fullName, {
|
|
194
|
+
schema,
|
|
195
|
+
table,
|
|
196
|
+
pkColumns,
|
|
197
|
+
columns,
|
|
198
|
+
before: beforeResult.rows,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Execute the user's SQL
|
|
203
|
+
await client.query(query);
|
|
204
|
+
|
|
205
|
+
// Snapshot after and compute diffs
|
|
206
|
+
const tables: DiffTableResult[] = [];
|
|
207
|
+
|
|
208
|
+
for (const [fullName, info] of tableInfo) {
|
|
209
|
+
const orderBy = info.pkColumns.map((c) => `"${c}"`).join(", ");
|
|
210
|
+
const afterResult = await client.query(
|
|
211
|
+
`SELECT * FROM "${info.schema}"."${info.table}" ORDER BY ${orderBy} LIMIT 10000`,
|
|
212
|
+
);
|
|
213
|
+
const afterRows: Record<string, unknown>[] = afterResult.rows;
|
|
214
|
+
|
|
215
|
+
// Index rows by PK
|
|
216
|
+
const pkKey = (row: Record<string, unknown>) =>
|
|
217
|
+
info.pkColumns.map((c) => JSON.stringify(row[c])).join("|");
|
|
218
|
+
|
|
219
|
+
const beforeMap = new Map<string, Record<string, unknown>>();
|
|
220
|
+
for (const row of info.before) {
|
|
221
|
+
beforeMap.set(pkKey(row), row);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const afterMap = new Map<string, Record<string, unknown>>();
|
|
225
|
+
for (const row of afterRows) {
|
|
226
|
+
afterMap.set(pkKey(row), row);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const deleted: Record<string, unknown>[] = [];
|
|
230
|
+
const added: Record<string, unknown>[] = [];
|
|
231
|
+
const modified: DiffTableResult["modified"] = [];
|
|
232
|
+
let unchangedCount = 0;
|
|
233
|
+
|
|
234
|
+
// Find deleted and modified rows
|
|
235
|
+
for (const [key, beforeRow] of beforeMap) {
|
|
236
|
+
const afterRow = afterMap.get(key);
|
|
237
|
+
if (!afterRow) {
|
|
238
|
+
deleted.push(beforeRow);
|
|
239
|
+
} else {
|
|
240
|
+
// Compare all columns
|
|
241
|
+
const changedColumns: string[] = [];
|
|
242
|
+
for (const col of info.columns) {
|
|
243
|
+
const bVal = beforeRow[col.name];
|
|
244
|
+
const aVal = afterRow[col.name];
|
|
245
|
+
if (JSON.stringify(bVal) !== JSON.stringify(aVal)) {
|
|
246
|
+
changedColumns.push(col.name);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (changedColumns.length > 0) {
|
|
250
|
+
modified.push({
|
|
251
|
+
before: beforeRow,
|
|
252
|
+
after: afterRow,
|
|
253
|
+
changedColumns,
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
unchangedCount++;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Find added rows
|
|
262
|
+
for (const [key, afterRow] of afterMap) {
|
|
263
|
+
if (!beforeMap.has(key)) {
|
|
264
|
+
added.push(afterRow);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
tables.push({
|
|
269
|
+
tableName: fullName,
|
|
270
|
+
columns: info.columns,
|
|
271
|
+
primaryKeyColumns: info.pkColumns,
|
|
272
|
+
deleted,
|
|
273
|
+
added,
|
|
274
|
+
modified,
|
|
275
|
+
unchangedCount,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Always rollback — this is a preview only
|
|
280
|
+
await client.query("ROLLBACK");
|
|
281
|
+
|
|
282
|
+
res.json({ tables });
|
|
283
|
+
} catch (err) {
|
|
284
|
+
try {
|
|
285
|
+
await client.query("ROLLBACK");
|
|
286
|
+
} catch {
|
|
287
|
+
// ignore rollback error
|
|
288
|
+
}
|
|
289
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
290
|
+
res.status(500).json({ error: message });
|
|
291
|
+
} finally {
|
|
292
|
+
await client.end();
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// Scan localhost for PostgreSQL databases
|
|
298
|
+
app.get(
|
|
299
|
+
"/api/scan-localhost",
|
|
300
|
+
async (_req, res: express.Response<ScanLocalhostResponse>) => {
|
|
301
|
+
if (!LOCALHOST_SCANNING_ENABLED) {
|
|
302
|
+
res.json({ databases: [], error: "Localhost scanning is disabled" });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const host = "localhost";
|
|
306
|
+
const port = 5432;
|
|
307
|
+
const username = "postgres";
|
|
308
|
+
const candidates = ["", "password", "postgres"];
|
|
309
|
+
|
|
310
|
+
for (const password of candidates) {
|
|
311
|
+
const client = new pg.Client({
|
|
312
|
+
connectionString: buildConnectionString({
|
|
313
|
+
username,
|
|
314
|
+
password,
|
|
315
|
+
host,
|
|
316
|
+
port,
|
|
317
|
+
database: "postgres",
|
|
318
|
+
}),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
await client.connect();
|
|
323
|
+
const result = await client.query(
|
|
324
|
+
"SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname",
|
|
325
|
+
);
|
|
326
|
+
const databases = result.rows.map((row: { datname: string }) => ({
|
|
327
|
+
host,
|
|
328
|
+
port,
|
|
329
|
+
username,
|
|
330
|
+
password,
|
|
331
|
+
database: row.datname,
|
|
332
|
+
}));
|
|
333
|
+
res.json({ databases });
|
|
334
|
+
return;
|
|
335
|
+
} catch {
|
|
336
|
+
// try next credential
|
|
337
|
+
} finally {
|
|
338
|
+
await client.end();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
res.json({
|
|
343
|
+
databases: [],
|
|
344
|
+
error: "Could not connect with any known credentials",
|
|
345
|
+
});
|
|
346
|
+
},
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// Export database as .sql file via pg_dump
|
|
350
|
+
app.post("/api/export", async (req, res) => {
|
|
351
|
+
const { connection, exportType } = req.body as {
|
|
352
|
+
connection: DatabaseConfigConnection;
|
|
353
|
+
exportType: ExportType;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
if (!connection || !exportType) {
|
|
357
|
+
res.status(400).json({ error: "Missing connection or exportType" });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const sql = await pgDump(connection, exportType === "schema");
|
|
363
|
+
const suffix = exportType === "schema" ? "schema" : "full";
|
|
364
|
+
const filename = `${connection.database}_${suffix}_${new Date().toISOString().slice(0, 10)}.sql`;
|
|
365
|
+
res.setHeader("Content-Type", "application/sql");
|
|
366
|
+
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
|
367
|
+
res.send(sql);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
370
|
+
res.status(500).json({ error: message });
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Serve built frontend if it exists
|
|
375
|
+
if (hasBuiltFrontend) {
|
|
376
|
+
app.use(express.static(distPath));
|
|
377
|
+
app.get("*", (_req, res) => {
|
|
378
|
+
res.sendFile(path.join(distPath, "index.html"));
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export const serverReady = new Promise<void>((resolve) => {
|
|
383
|
+
app.listen(port, () => {
|
|
384
|
+
console.log(`dbdiff running at http://localhost:${port}`);
|
|
385
|
+
if (hasBuiltFrontend) {
|
|
386
|
+
console.log("Serving frontend from dist/");
|
|
387
|
+
} else {
|
|
388
|
+
console.log("No built frontend found - API only (use Vite for frontend)");
|
|
389
|
+
}
|
|
390
|
+
resolve();
|
|
391
|
+
});
|
|
392
|
+
});
|
package/src/App.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* App-specific styles - mostly using Tailwind now */
|