datapeek 0.1.13 → 0.1.15
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 +10 -10
- package/dist/cli/dev.js +225 -52
- package/dist/cli/index.js +225 -52
- package/dist/client/assets/index-C0Gnjc5g.css +1 -0
- package/dist/client/assets/index-CPRX2vzW.js +390 -0
- package/dist/client/index.html +2 -2
- package/dist/server/dev.js +225 -52
- package/dist/server/index.js +225 -52
- package/package.json +1 -1
- package/dist/client/assets/index-H7ji6dtA.js +0 -385
- package/dist/client/assets/index-TFw4Z1Ky.css +0 -1
package/README.md
CHANGED
|
@@ -4,16 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
Datapeek provides an intuitive web-based interface to browse, query, and explore your SQL Server and PostgreSQL databases directly from your terminal.
|
|
6
6
|
|
|
7
|
-
## Screenshots
|
|
8
|
-
|
|
9
|
-
### Light Mode
|
|
10
|
-

|
|
11
|
-

|
|
12
|
-
|
|
13
|
-
### Dark Mode
|
|
14
|
-

|
|
15
|
-

|
|
16
|
-
|
|
17
7
|
## Quick Start
|
|
18
8
|
|
|
19
9
|
### Option 1: With Connection String (Recommended)
|
|
@@ -42,6 +32,16 @@ npx datapeek
|
|
|
42
32
|
|
|
43
33
|
A connection dialog will open in your browser where you can enter your database connection details.
|
|
44
34
|
|
|
35
|
+
## Screenshots
|
|
36
|
+
|
|
37
|
+
### Light Mode
|
|
38
|
+

|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
### Dark Mode
|
|
42
|
+

|
|
43
|
+

|
|
44
|
+
|
|
45
45
|
## Features
|
|
46
46
|
|
|
47
47
|
- 🗄️ **Multi-Database Support** - Works with both SQL Server and PostgreSQL
|
package/dist/cli/dev.js
CHANGED
|
@@ -56,8 +56,15 @@ connectionRoutes.post("/test", async (req, res) => {
|
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
58
|
connectionRoutes.get("/provided", (req, res) => {
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
try {
|
|
60
|
+
const connString = getProvidedConnectionString();
|
|
61
|
+
res.json({ connectionString: connString || null });
|
|
62
|
+
} catch (error) {
|
|
63
|
+
res.status(500).json({
|
|
64
|
+
connectionString: null,
|
|
65
|
+
error: error.message || "Failed to get connection string"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
61
68
|
});
|
|
62
69
|
connectionRoutes.post("/", async (req, res) => {
|
|
63
70
|
const timeout = setTimeout(() => {
|
|
@@ -160,12 +167,17 @@ tableRoutes.get("/", async (req, res) => {
|
|
|
160
167
|
const result = await executeQuery(query);
|
|
161
168
|
res.json(result);
|
|
162
169
|
} catch (error) {
|
|
170
|
+
console.error("Error fetching tables:", error);
|
|
163
171
|
const errorMessage = error.message || "";
|
|
164
172
|
if (errorMessage.includes("Login failed") || errorMessage.includes("authentication") || errorMessage.includes("password authentication")) {
|
|
165
173
|
const { disconnect: disconnect2 } = await import("./db-OIJ2PJK6.js");
|
|
166
174
|
await disconnect2();
|
|
167
175
|
}
|
|
168
|
-
|
|
176
|
+
const errorDetails = error.originalError?.message || error.originalError?.info?.message || error.message || "Failed to fetch tables";
|
|
177
|
+
res.status(500).json({
|
|
178
|
+
error: error.message || "Failed to fetch tables",
|
|
179
|
+
details: errorDetails
|
|
180
|
+
});
|
|
169
181
|
}
|
|
170
182
|
});
|
|
171
183
|
tableRoutes.get("/:schema/:table", async (req, res) => {
|
|
@@ -222,13 +234,13 @@ tableRoutes.get("/:schema/:table", async (req, res) => {
|
|
|
222
234
|
) fk ON c.table_schema = fk.table_schema
|
|
223
235
|
AND c.table_name = fk.table_name
|
|
224
236
|
AND c.column_name = fk.column_name
|
|
225
|
-
WHERE c.table_schema =
|
|
226
|
-
AND c.table_name =
|
|
237
|
+
WHERE c.table_schema = ${dialect.param(1)}
|
|
238
|
+
AND c.table_name = ${dialect.param(2)}
|
|
227
239
|
ORDER BY c.ordinal_position
|
|
228
240
|
`;
|
|
229
241
|
const result = await executeQuery(query, [
|
|
230
|
-
{ name: "
|
|
231
|
-
{ name: "
|
|
242
|
+
{ name: "p1", value: schema },
|
|
243
|
+
{ name: "p2", value: table }
|
|
232
244
|
]);
|
|
233
245
|
res.json(result);
|
|
234
246
|
} catch (error) {
|
|
@@ -316,9 +328,9 @@ tableRoutes.get("/:schema/:table/data", async (req, res) => {
|
|
|
316
328
|
WHERE table_schema = ${dialect.param(1)} AND table_name = ${dialect.param(2)} AND column_name IN (${placeholders})
|
|
317
329
|
`;
|
|
318
330
|
const validateParams = [
|
|
319
|
-
{ name: "
|
|
320
|
-
{ name: "
|
|
321
|
-
...columnNames.map((col) => ({ name:
|
|
331
|
+
{ name: "p1", value: schema },
|
|
332
|
+
{ name: "p2", value: table },
|
|
333
|
+
...columnNames.map((col, idx) => ({ name: `p${idx + 3}`, value: col }))
|
|
322
334
|
];
|
|
323
335
|
const validateResult = await executeQuery(validateQuery, validateParams);
|
|
324
336
|
validateResult.forEach((r) => {
|
|
@@ -358,75 +370,111 @@ tableRoutes.get("/:schema/:table/data", async (req, res) => {
|
|
|
358
370
|
switch (operator) {
|
|
359
371
|
// Text operators
|
|
360
372
|
case "contains":
|
|
361
|
-
filterParams.push({ name: `
|
|
373
|
+
filterParams.push({ name: `p${paramIndex}`, value: `%${String(value)}%` });
|
|
362
374
|
condition = `${quotedColumn} LIKE ${dialect.param(paramIndex)}`;
|
|
363
375
|
break;
|
|
364
376
|
case "equals":
|
|
365
|
-
filterParams.push({ name: `
|
|
377
|
+
filterParams.push({ name: `p${paramIndex}`, value: String(value) });
|
|
366
378
|
condition = `${quotedColumn} = ${dialect.param(paramIndex)}`;
|
|
367
379
|
break;
|
|
368
380
|
case "startsWith":
|
|
369
|
-
filterParams.push({ name: `
|
|
381
|
+
filterParams.push({ name: `p${paramIndex}`, value: `${String(value)}%` });
|
|
370
382
|
condition = `${quotedColumn} LIKE ${dialect.param(paramIndex)}`;
|
|
371
383
|
break;
|
|
372
384
|
case "endsWith":
|
|
373
|
-
filterParams.push({ name: `
|
|
385
|
+
filterParams.push({ name: `p${paramIndex}`, value: `%${String(value)}` });
|
|
374
386
|
condition = `${quotedColumn} LIKE ${dialect.param(paramIndex)}`;
|
|
375
387
|
break;
|
|
376
388
|
case "notContains":
|
|
377
|
-
filterParams.push({ name: `
|
|
389
|
+
filterParams.push({ name: `p${paramIndex}`, value: `%${String(value)}%` });
|
|
378
390
|
condition = `${quotedColumn} NOT LIKE ${dialect.param(paramIndex)}`;
|
|
379
391
|
break;
|
|
380
392
|
// Number operators
|
|
381
393
|
case "eq":
|
|
382
|
-
|
|
394
|
+
const isNumericType = dataType && ["int", "bigint", "decimal", "numeric", "float", "double", "real", "smallint", "tinyint", "money", "smallmoney"].some((t) => dataType.toLowerCase().includes(t));
|
|
395
|
+
if (isNumericType) {
|
|
396
|
+
const numValue = Number(value);
|
|
397
|
+
if (isNaN(numValue)) {
|
|
398
|
+
console.warn(`Cannot convert filter value to number for ${columnName}, skipping`);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
filterParams.push({ name: `p${paramIndex}`, value: numValue });
|
|
402
|
+
} else {
|
|
403
|
+
filterParams.push({ name: `p${paramIndex}`, value: String(value) });
|
|
404
|
+
}
|
|
383
405
|
condition = `${quotedColumn} = ${dialect.param(paramIndex)}`;
|
|
384
406
|
break;
|
|
385
407
|
case "gt":
|
|
386
|
-
|
|
408
|
+
const gtValue = Number(value);
|
|
409
|
+
if (isNaN(gtValue)) {
|
|
410
|
+
console.warn(`Cannot convert filter value to number for ${columnName} (gt), skipping`);
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
filterParams.push({ name: `p${paramIndex}`, value: gtValue });
|
|
387
414
|
condition = `${quotedColumn} > ${dialect.param(paramIndex)}`;
|
|
388
415
|
break;
|
|
389
416
|
case "gte":
|
|
390
|
-
|
|
417
|
+
const gteValue = Number(value);
|
|
418
|
+
if (isNaN(gteValue)) {
|
|
419
|
+
console.warn(`Cannot convert filter value to number for ${columnName} (gte), skipping`);
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
filterParams.push({ name: `p${paramIndex}`, value: gteValue });
|
|
391
423
|
condition = `${quotedColumn} >= ${dialect.param(paramIndex)}`;
|
|
392
424
|
break;
|
|
393
425
|
case "lt":
|
|
394
|
-
|
|
426
|
+
const ltValue = Number(value);
|
|
427
|
+
if (isNaN(ltValue)) {
|
|
428
|
+
console.warn(`Cannot convert filter value to number for ${columnName} (lt), skipping`);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
filterParams.push({ name: `p${paramIndex}`, value: ltValue });
|
|
395
432
|
condition = `${quotedColumn} < ${dialect.param(paramIndex)}`;
|
|
396
433
|
break;
|
|
397
434
|
case "lte":
|
|
398
|
-
|
|
435
|
+
const lteValue = Number(value);
|
|
436
|
+
if (isNaN(lteValue)) {
|
|
437
|
+
console.warn(`Cannot convert filter value to number for ${columnName} (lte), skipping`);
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
filterParams.push({ name: `p${paramIndex}`, value: lteValue });
|
|
399
441
|
condition = `${quotedColumn} <= ${dialect.param(paramIndex)}`;
|
|
400
442
|
break;
|
|
401
443
|
case "between":
|
|
402
444
|
if (typeof value === "object" && "from" in value && "to" in value) {
|
|
403
445
|
const fromParamIndex = paramIndex;
|
|
404
446
|
const toParamIndex = paramIndex + 1;
|
|
405
|
-
|
|
406
|
-
|
|
447
|
+
const fromValue = Number(value.from);
|
|
448
|
+
const toValue = Number(value.to);
|
|
449
|
+
if (isNaN(fromValue) || isNaN(toValue)) {
|
|
450
|
+
console.warn(`Cannot convert filter values to numbers for ${columnName} (between), skipping`);
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
filterParams.push({ name: `p${fromParamIndex}`, value: fromValue });
|
|
454
|
+
filterParams.push({ name: `p${toParamIndex}`, value: toValue });
|
|
407
455
|
condition = `${quotedColumn} BETWEEN ${dialect.param(fromParamIndex)} AND ${dialect.param(toParamIndex)}`;
|
|
408
456
|
paramIndex++;
|
|
409
457
|
}
|
|
410
458
|
break;
|
|
411
459
|
// Date operators
|
|
412
460
|
case "dateEq":
|
|
413
|
-
filterParams.push({ name: `
|
|
461
|
+
filterParams.push({ name: `p${paramIndex}`, value: String(value) });
|
|
414
462
|
condition = `${dialect.castToDate(quotedColumn)} = ${dialect.castToDate(dialect.param(paramIndex))}`;
|
|
415
463
|
break;
|
|
416
464
|
case "dateAfter":
|
|
417
|
-
filterParams.push({ name: `
|
|
465
|
+
filterParams.push({ name: `p${paramIndex}`, value: String(value) });
|
|
418
466
|
condition = `${dialect.castToDate(quotedColumn)} > ${dialect.castToDate(dialect.param(paramIndex))}`;
|
|
419
467
|
break;
|
|
420
468
|
case "dateBefore":
|
|
421
|
-
filterParams.push({ name: `
|
|
469
|
+
filterParams.push({ name: `p${paramIndex}`, value: String(value) });
|
|
422
470
|
condition = `${dialect.castToDate(quotedColumn)} < ${dialect.castToDate(dialect.param(paramIndex))}`;
|
|
423
471
|
break;
|
|
424
472
|
case "dateBetween":
|
|
425
473
|
if (typeof value === "object" && "from" in value && "to" in value) {
|
|
426
474
|
const fromParamIndex = paramIndex;
|
|
427
475
|
const toParamIndex = paramIndex + 1;
|
|
428
|
-
filterParams.push({ name: `
|
|
429
|
-
filterParams.push({ name: `
|
|
476
|
+
filterParams.push({ name: `p${fromParamIndex}`, value: String(value.from) });
|
|
477
|
+
filterParams.push({ name: `p${toParamIndex}`, value: String(value.to) });
|
|
430
478
|
condition = `${dialect.castToDate(quotedColumn)} BETWEEN ${dialect.castToDate(dialect.param(fromParamIndex))} AND ${dialect.castToDate(dialect.param(toParamIndex))}`;
|
|
431
479
|
paramIndex++;
|
|
432
480
|
}
|
|
@@ -438,7 +486,7 @@ tableRoutes.get("/:schema/:table/data", async (req, res) => {
|
|
|
438
486
|
const placeholders = [];
|
|
439
487
|
value.forEach((val, i) => {
|
|
440
488
|
const currentIndex = paramIndex + i;
|
|
441
|
-
filterParams.push({ name: `
|
|
489
|
+
filterParams.push({ name: `p${currentIndex}`, value: val });
|
|
442
490
|
placeholders.push(dialect.param(currentIndex));
|
|
443
491
|
});
|
|
444
492
|
const inOperator = operator === "in" ? "IN" : "NOT IN";
|
|
@@ -448,7 +496,7 @@ tableRoutes.get("/:schema/:table/data", async (req, res) => {
|
|
|
448
496
|
break;
|
|
449
497
|
default:
|
|
450
498
|
console.warn(`Unknown filter operator: ${operator}, falling back to contains`);
|
|
451
|
-
filterParams.push({ name: `
|
|
499
|
+
filterParams.push({ name: `p${paramIndex}`, value: `%${String(value)}%` });
|
|
452
500
|
condition = `${quotedColumn} LIKE ${dialect.param(paramIndex)}`;
|
|
453
501
|
}
|
|
454
502
|
if (condition) {
|
|
@@ -485,9 +533,9 @@ tableRoutes.get("/:schema/:table/data", async (req, res) => {
|
|
|
485
533
|
WHERE table_schema = ${dialect.param(1)} AND table_name = ${dialect.param(2)} AND column_name = ${dialect.param(3)}
|
|
486
534
|
`;
|
|
487
535
|
const validateResult = await executeQuery(validateQuery, [
|
|
488
|
-
{ name: "
|
|
489
|
-
{ name: "
|
|
490
|
-
{ name: "
|
|
536
|
+
{ name: "p1", value: schema },
|
|
537
|
+
{ name: "p2", value: table },
|
|
538
|
+
{ name: "p3", value: orderByColumn }
|
|
491
539
|
]);
|
|
492
540
|
if (validateResult.length === 0) {
|
|
493
541
|
orderByColumn = "";
|
|
@@ -509,8 +557,8 @@ tableRoutes.get("/:schema/:table/data", async (req, res) => {
|
|
|
509
557
|
${!topClause ? dialect.limitOffset(0, 1) : ""}
|
|
510
558
|
`;
|
|
511
559
|
const structureResult = await executeQuery(structureQuery, [
|
|
512
|
-
{ name: "
|
|
513
|
-
{ name: "
|
|
560
|
+
{ name: "p1", value: schema },
|
|
561
|
+
{ name: "p2", value: table }
|
|
514
562
|
]);
|
|
515
563
|
if (structureResult.length > 0) {
|
|
516
564
|
orderByColumn = structureResult[0].column_name || structureResult[0].COLUMN_NAME;
|
|
@@ -542,8 +590,8 @@ tableRoutes.get("/:schema/:table/data", async (req, res) => {
|
|
|
542
590
|
`;
|
|
543
591
|
console.log("Fetching foreign keys with query:", fkQuery);
|
|
544
592
|
const foreignKeys = await executeQuery(fkQuery, [
|
|
545
|
-
{ name: "
|
|
546
|
-
{ name: "
|
|
593
|
+
{ name: "p1", value: schema },
|
|
594
|
+
{ name: "p2", value: table }
|
|
547
595
|
]);
|
|
548
596
|
console.log(`Found ${foreignKeys.length} foreign key(s)`);
|
|
549
597
|
if (foreignKeys.length > 0) {
|
|
@@ -560,11 +608,11 @@ tableRoutes.get("/:schema/:table/data", async (req, res) => {
|
|
|
560
608
|
WHERE ${tableConditions}
|
|
561
609
|
ORDER BY table_schema, table_name, ordinal_position
|
|
562
610
|
`;
|
|
563
|
-
const batchParams = uniqueRefTables.flatMap((tableRef) => {
|
|
611
|
+
const batchParams = uniqueRefTables.flatMap((tableRef, idx) => {
|
|
564
612
|
const [refSchema, refTable] = tableRef.split(".");
|
|
565
613
|
return [
|
|
566
|
-
{ name:
|
|
567
|
-
{ name:
|
|
614
|
+
{ name: `p${idx * 2 + 1}`, value: refSchema },
|
|
615
|
+
{ name: `p${idx * 2 + 2}`, value: refTable }
|
|
568
616
|
];
|
|
569
617
|
});
|
|
570
618
|
console.log("Fetching referenced table columns with query:", batchColumnsQuery);
|
|
@@ -797,8 +845,8 @@ tableRoutes.post("/:schema/:table/related-data", async (req, res) => {
|
|
|
797
845
|
ORDER BY ordinal_position
|
|
798
846
|
`;
|
|
799
847
|
const columns = await executeQuery(columnsQuery, [
|
|
800
|
-
{ name: "
|
|
801
|
-
{ name: "
|
|
848
|
+
{ name: "p1", value: referencedSchema },
|
|
849
|
+
{ name: "p2", value: referencedTable }
|
|
802
850
|
]);
|
|
803
851
|
const referencedColInfo = columns.find((col) => {
|
|
804
852
|
const colName = col.column_name || col.COLUMN_NAME;
|
|
@@ -840,8 +888,8 @@ tableRoutes.post("/:schema/:table/related-data", async (req, res) => {
|
|
|
840
888
|
FROM ${quotedRefSchema}.${quotedRefTable}
|
|
841
889
|
WHERE ${quotedRefColumn} IN (${placeholders})
|
|
842
890
|
`;
|
|
843
|
-
const params = ids.map((id) => ({
|
|
844
|
-
name:
|
|
891
|
+
const params = ids.map((id, idx) => ({
|
|
892
|
+
name: `p${idx + 1}`,
|
|
845
893
|
value: id
|
|
846
894
|
}));
|
|
847
895
|
const result = await executeQuery(dataQuery, params);
|
|
@@ -907,9 +955,9 @@ tableRoutes.get("/:schema/:table/distinct-values/:column", async (req, res) => {
|
|
|
907
955
|
WHERE c.table_schema = ${dialect.param(1)} AND c.table_name = ${dialect.param(2)} AND c.column_name = ${dialect.param(3)}
|
|
908
956
|
`;
|
|
909
957
|
const columnResult = await executeQuery(columnQuery, [
|
|
910
|
-
{ name: "
|
|
911
|
-
{ name: "
|
|
912
|
-
{ name: "
|
|
958
|
+
{ name: "p1", value: schema },
|
|
959
|
+
{ name: "p2", value: table },
|
|
960
|
+
{ name: "p3", value: column }
|
|
913
961
|
]);
|
|
914
962
|
if (columnResult.length === 0) {
|
|
915
963
|
return res.status(400).json({ error: "Column not found" });
|
|
@@ -936,7 +984,7 @@ tableRoutes.get("/:schema/:table/distinct-values/:column", async (req, res) => {
|
|
|
936
984
|
if (searchQuery && searchQuery.trim()) {
|
|
937
985
|
const searchCols = columnsToSelect.map((col) => `${dialect.quoteId(col)} LIKE ${dialect.param(1)}`).join(" OR ");
|
|
938
986
|
query += ` WHERE (${searchCols}) AND ${quotedRefColumn} IS NOT NULL`;
|
|
939
|
-
params.push({ name: "
|
|
987
|
+
params.push({ name: "p1", value: `%${searchQuery.trim()}%` });
|
|
940
988
|
} else {
|
|
941
989
|
query += ` WHERE ${quotedRefColumn} IS NOT NULL`;
|
|
942
990
|
}
|
|
@@ -950,8 +998,8 @@ tableRoutes.get("/:schema/:table/distinct-values/:column", async (req, res) => {
|
|
|
950
998
|
ORDER BY ordinal_position
|
|
951
999
|
`;
|
|
952
1000
|
const refColumns = await executeQuery(refColumnsQuery, [
|
|
953
|
-
{ name: "
|
|
954
|
-
{ name: "
|
|
1001
|
+
{ name: "p1", value: refSchema },
|
|
1002
|
+
{ name: "p2", value: refTable }
|
|
955
1003
|
]);
|
|
956
1004
|
const preferredNames = ["name", "title", "description", "code"];
|
|
957
1005
|
for (const preferredName of preferredNames) {
|
|
@@ -984,7 +1032,7 @@ tableRoutes.get("/:schema/:table/distinct-values/:column", async (req, res) => {
|
|
|
984
1032
|
} else {
|
|
985
1033
|
query += ` WHERE ${quotedRefColumn} LIKE ${dialect.param(1)}`;
|
|
986
1034
|
}
|
|
987
|
-
params.push({ name: "
|
|
1035
|
+
params.push({ name: "p1", value: `%${searchQuery.trim()}%` });
|
|
988
1036
|
query += ` AND ${quotedRefColumn} IS NOT NULL`;
|
|
989
1037
|
} else {
|
|
990
1038
|
query += ` WHERE ${quotedRefColumn} IS NOT NULL`;
|
|
@@ -1005,7 +1053,7 @@ tableRoutes.get("/:schema/:table/distinct-values/:column", async (req, res) => {
|
|
|
1005
1053
|
if (searchQuery && searchQuery.trim()) {
|
|
1006
1054
|
const searchCols = quotedColumns.map((col) => `${dialect.tryCastToNVarChar(col)} LIKE ${dialect.param(1)}`).join(" OR ");
|
|
1007
1055
|
query += ` WHERE (${searchCols}) AND ${keyColumn} IS NOT NULL`;
|
|
1008
|
-
params.push({ name: "
|
|
1056
|
+
params.push({ name: "p1", value: `%${searchQuery.trim()}%` });
|
|
1009
1057
|
} else {
|
|
1010
1058
|
query += ` WHERE ${keyColumn} IS NOT NULL`;
|
|
1011
1059
|
}
|
|
@@ -1015,7 +1063,7 @@ tableRoutes.get("/:schema/:table/distinct-values/:column", async (req, res) => {
|
|
|
1015
1063
|
if (searchQuery && searchQuery.trim()) {
|
|
1016
1064
|
if (["varchar", "nvarchar", "char", "nchar", "text", "ntext"].some((t) => dataType.includes(t))) {
|
|
1017
1065
|
query += ` WHERE ${quotedColumn} LIKE ${dialect.param(1)}`;
|
|
1018
|
-
params.push({ name: "
|
|
1066
|
+
params.push({ name: "p1", value: `%${searchQuery.trim()}%` });
|
|
1019
1067
|
query += ` AND ${quotedColumn} IS NOT NULL`;
|
|
1020
1068
|
} else {
|
|
1021
1069
|
query += ` WHERE ${quotedColumn} IS NOT NULL`;
|
|
@@ -1038,6 +1086,131 @@ tableRoutes.get("/:schema/:table/distinct-values/:column", async (req, res) => {
|
|
|
1038
1086
|
res.status(500).json({ error: error.message || "Failed to fetch distinct values" });
|
|
1039
1087
|
}
|
|
1040
1088
|
});
|
|
1089
|
+
tableRoutes.get("/:schema/:table/reverse-foreign-keys", async (req, res) => {
|
|
1090
|
+
try {
|
|
1091
|
+
const { schema, table } = req.params;
|
|
1092
|
+
const pool = getConnection();
|
|
1093
|
+
if (!pool) {
|
|
1094
|
+
return res.status(400).json({ error: "Not connected to database" });
|
|
1095
|
+
}
|
|
1096
|
+
const isConnected = "connected" in pool ? pool.connected === true : !pool.ended;
|
|
1097
|
+
if (!isConnected) {
|
|
1098
|
+
return res.status(400).json({ error: "Not connected to database" });
|
|
1099
|
+
}
|
|
1100
|
+
const dialect = getDialect();
|
|
1101
|
+
const query = `
|
|
1102
|
+
SELECT
|
|
1103
|
+
kcu1.table_schema as "referencingSchema",
|
|
1104
|
+
kcu1.table_name as "referencingTable",
|
|
1105
|
+
kcu1.column_name as "fkColumnName",
|
|
1106
|
+
kcu2.column_name as "referencedColumn"
|
|
1107
|
+
FROM information_schema.referential_constraints rc
|
|
1108
|
+
INNER JOIN information_schema.key_column_usage kcu1
|
|
1109
|
+
ON rc.constraint_catalog = kcu1.constraint_catalog
|
|
1110
|
+
AND rc.constraint_schema = kcu1.constraint_schema
|
|
1111
|
+
AND rc.constraint_name = kcu1.constraint_name
|
|
1112
|
+
INNER JOIN information_schema.key_column_usage kcu2
|
|
1113
|
+
ON rc.unique_constraint_catalog = kcu2.constraint_catalog
|
|
1114
|
+
AND rc.unique_constraint_schema = kcu2.constraint_schema
|
|
1115
|
+
AND rc.unique_constraint_name = kcu2.constraint_name
|
|
1116
|
+
AND kcu1.ordinal_position = kcu2.ordinal_position
|
|
1117
|
+
WHERE kcu2.table_schema = ${dialect.param(1)}
|
|
1118
|
+
AND kcu2.table_name = ${dialect.param(2)}
|
|
1119
|
+
ORDER BY kcu1.table_schema, kcu1.table_name, kcu1.ordinal_position
|
|
1120
|
+
`;
|
|
1121
|
+
const result = await executeQuery(query, [
|
|
1122
|
+
{ name: "p1", value: schema },
|
|
1123
|
+
{ name: "p2", value: table }
|
|
1124
|
+
]);
|
|
1125
|
+
const grouped = {};
|
|
1126
|
+
result.forEach((row) => {
|
|
1127
|
+
const referencingSchema = row.referencingSchema || row.referencing_schema;
|
|
1128
|
+
const referencingTable = row.referencingTable || row.referencing_table;
|
|
1129
|
+
const fkColumnName = row.fkColumnName || row.fk_column_name;
|
|
1130
|
+
const referencedColumn = row.referencedColumn || row.referenced_column;
|
|
1131
|
+
const key = `${referencingSchema}.${referencingTable}`;
|
|
1132
|
+
if (!grouped[key]) {
|
|
1133
|
+
grouped[key] = {
|
|
1134
|
+
referencingSchema,
|
|
1135
|
+
referencingTable,
|
|
1136
|
+
fkColumns: [],
|
|
1137
|
+
referencedColumns: []
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
grouped[key].fkColumns.push(fkColumnName);
|
|
1141
|
+
grouped[key].referencedColumns.push(referencedColumn);
|
|
1142
|
+
});
|
|
1143
|
+
const reverseFks = Object.values(grouped);
|
|
1144
|
+
res.json(reverseFks);
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
console.error("Error fetching reverse foreign keys:", error);
|
|
1147
|
+
const errorMessage = error.message || "";
|
|
1148
|
+
if (errorMessage.includes("Login failed") || errorMessage.includes("authentication") || errorMessage.includes("password authentication")) {
|
|
1149
|
+
const { disconnect: disconnect2 } = await import("./db-OIJ2PJK6.js");
|
|
1150
|
+
await disconnect2();
|
|
1151
|
+
}
|
|
1152
|
+
res.status(500).json({ error: error.message || "Failed to fetch reverse foreign keys" });
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
tableRoutes.post("/:schema/:table/count-related", async (req, res) => {
|
|
1156
|
+
try {
|
|
1157
|
+
const { schema, table } = req.params;
|
|
1158
|
+
const { referencingSchema, referencingTable, fkColumns, referencedColumns, primaryKeyValues } = req.body;
|
|
1159
|
+
if (!referencingSchema || !referencingTable || !fkColumns || !Array.isArray(fkColumns) || !referencedColumns || !Array.isArray(referencedColumns) || !primaryKeyValues || !Array.isArray(primaryKeyValues)) {
|
|
1160
|
+
return res.status(400).json({ error: "Missing required parameters" });
|
|
1161
|
+
}
|
|
1162
|
+
if (fkColumns.length !== referencedColumns.length || fkColumns.length !== primaryKeyValues.length) {
|
|
1163
|
+
return res.status(400).json({ error: "FK columns, referenced columns, and primary key values arrays must have the same length" });
|
|
1164
|
+
}
|
|
1165
|
+
const pool = getConnection();
|
|
1166
|
+
if (!pool) {
|
|
1167
|
+
return res.status(400).json({ error: "Not connected to database" });
|
|
1168
|
+
}
|
|
1169
|
+
const isConnected = "connected" in pool ? pool.connected === true : !pool.ended;
|
|
1170
|
+
if (!isConnected) {
|
|
1171
|
+
return res.status(400).json({ error: "Not connected to database" });
|
|
1172
|
+
}
|
|
1173
|
+
const dialect = getDialect();
|
|
1174
|
+
const quotedSchema = dialect.quoteId(referencingSchema);
|
|
1175
|
+
const quotedTable = dialect.quoteId(referencingTable);
|
|
1176
|
+
const whereConditions = [];
|
|
1177
|
+
let paramIndex = 1;
|
|
1178
|
+
const params = [];
|
|
1179
|
+
fkColumns.forEach((fkCol, idx) => {
|
|
1180
|
+
const quotedCol = dialect.quoteId(fkCol);
|
|
1181
|
+
let pkValue = primaryKeyValues[idx];
|
|
1182
|
+
if (typeof pkValue === "number" && isNaN(pkValue)) {
|
|
1183
|
+
pkValue = null;
|
|
1184
|
+
}
|
|
1185
|
+
if (pkValue === "NaN" || pkValue === "nan") {
|
|
1186
|
+
pkValue = null;
|
|
1187
|
+
}
|
|
1188
|
+
if (pkValue === null || pkValue === void 0) {
|
|
1189
|
+
whereConditions.push(`${quotedCol} IS NULL`);
|
|
1190
|
+
} else {
|
|
1191
|
+
whereConditions.push(`${quotedCol} = ${dialect.param(paramIndex)}`);
|
|
1192
|
+
params.push({ name: `p${paramIndex}`, value: pkValue });
|
|
1193
|
+
paramIndex++;
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
const query = `
|
|
1197
|
+
SELECT COUNT(*) as "count"
|
|
1198
|
+
FROM ${quotedSchema}.${quotedTable}
|
|
1199
|
+
WHERE ${whereConditions.join(" AND ")}
|
|
1200
|
+
`;
|
|
1201
|
+
const result = await executeQuery(query, params);
|
|
1202
|
+
const count = result[0]?.count || result[0]?.COUNT || 0;
|
|
1203
|
+
res.json({ count: Number(count) });
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
console.error("Error counting related rows:", error);
|
|
1206
|
+
const errorMessage = error.message || "";
|
|
1207
|
+
if (errorMessage.includes("Login failed") || errorMessage.includes("authentication") || errorMessage.includes("password authentication")) {
|
|
1208
|
+
const { disconnect: disconnect2 } = await import("./db-OIJ2PJK6.js");
|
|
1209
|
+
await disconnect2();
|
|
1210
|
+
}
|
|
1211
|
+
res.status(500).json({ error: error.message || "Failed to count related rows" });
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1041
1214
|
|
|
1042
1215
|
// src/server/routes/query.ts
|
|
1043
1216
|
import { Router as Router3 } from "express";
|