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 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
- ![Datapeek Light Mode](https://raw.githubusercontent.com/bishoymly/datapeek/main/public/screenshots/light1.png)
11
- ![Datapeek Light Mode](https://raw.githubusercontent.com/bishoymly/datapeek/main/public/screenshots/light2.png)
12
-
13
- ### Dark Mode
14
- ![Datapeek Dark Mode](https://raw.githubusercontent.com/bishoymly/datapeek/main/public/screenshots/dark1.png)
15
- ![Datapeek Dark Mode](https://raw.githubusercontent.com/bishoymly/datapeek/main/public/screenshots/dark2.png)
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
+ ![Datapeek Light Mode](https://raw.githubusercontent.com/bishoymly/datapeek/main/public/screenshots/light1.png)
39
+ ![Datapeek Light Mode](https://raw.githubusercontent.com/bishoymly/datapeek/main/public/screenshots/light2.png)
40
+
41
+ ### Dark Mode
42
+ ![Datapeek Dark Mode](https://raw.githubusercontent.com/bishoymly/datapeek/main/public/screenshots/dark1.png)
43
+ ![Datapeek Dark Mode](https://raw.githubusercontent.com/bishoymly/datapeek/main/public/screenshots/dark2.png)
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
- const connString = getProvidedConnectionString();
60
- res.json({ connectionString: connString || null });
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
- res.status(500).json({ error: error.message || "Failed to fetch tables" });
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 = @schema
226
- AND c.table_name = @table
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: "schema", value: schema },
231
- { name: "table", value: table }
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: "schema", value: schema },
320
- { name: "table", value: table },
321
- ...columnNames.map((col) => ({ name: "col", value: col }))
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: `filter${paramIndex}`, value: `%${String(value)}%` });
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: `filter${paramIndex}`, value: String(value) });
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: `filter${paramIndex}`, value: `${String(value)}%` });
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: `filter${paramIndex}`, value: `%${String(value)}` });
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: `filter${paramIndex}`, value: `%${String(value)}%` });
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
- filterParams.push({ name: `filter${paramIndex}`, value: Number(value) });
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
- filterParams.push({ name: `filter${paramIndex}`, value: Number(value) });
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
- filterParams.push({ name: `filter${paramIndex}`, value: Number(value) });
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
- filterParams.push({ name: `filter${paramIndex}`, value: Number(value) });
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
- filterParams.push({ name: `filter${paramIndex}`, value: Number(value) });
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
- filterParams.push({ name: `filter${fromParamIndex}`, value: Number(value.from) });
406
- filterParams.push({ name: `filter${toParamIndex}`, value: Number(value.to) });
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: `filter${paramIndex}`, value: String(value) });
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: `filter${paramIndex}`, value: String(value) });
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: `filter${paramIndex}`, value: String(value) });
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: `filter${fromParamIndex}`, value: String(value.from) });
429
- filterParams.push({ name: `filter${toParamIndex}`, value: String(value.to) });
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: `filter${currentIndex}`, value: val });
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: `filter${paramIndex}`, value: `%${String(value)}%` });
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: "schema", value: schema },
489
- { name: "table", value: table },
490
- { name: "column", value: orderByColumn }
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: "schema", value: schema },
513
- { name: "table", value: table }
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: "schema", value: schema },
546
- { name: "table", value: table }
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: "refSchema", value: refSchema },
567
- { name: "refTable", value: refTable }
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: "refSchema", value: referencedSchema },
801
- { name: "refTable", value: referencedTable }
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: "id",
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: "schema", value: schema },
911
- { name: "table", value: table },
912
- { name: "column", value: column }
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: "search", value: `%${searchQuery.trim()}%` });
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: "refSchema", value: refSchema },
954
- { name: "refTable", value: refTable }
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: "search", value: `%${searchQuery.trim()}%` });
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: "search", value: `%${searchQuery.trim()}%` });
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: "search", value: `%${searchQuery.trim()}%` });
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";