datapeek 0.1.10 → 0.1.11
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/dist/cli/dev.js +440 -89
- package/dist/cli/index.js +440 -89
- package/dist/client/assets/index-CPoX5Biw.css +1 -0
- package/dist/client/assets/index-KtK9Gzwi.js +375 -0
- package/dist/client/index.html +2 -2
- package/dist/server/dev.js +440 -89
- package/dist/server/index.js +440 -89
- package/package.json +1 -1
- package/dist/client/assets/index-BhZ7NrhL.css +0 -1
- package/dist/client/assets/index-D-mash8_.js +0 -370
package/dist/client/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Datapeek - SQL Database Browser</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-KtK9Gzwi.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CPoX5Biw.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/dist/server/dev.js
CHANGED
|
@@ -214,26 +214,64 @@ tableRoutes.get("/:schema/:table/data", async (req, res) => {
|
|
|
214
214
|
const sortDirection = req.query.sortDirection || "asc";
|
|
215
215
|
const fkDisplayMode = req.query.fkDisplayMode || "key-only";
|
|
216
216
|
const offset = (page - 1) * pageSize;
|
|
217
|
-
const
|
|
217
|
+
const parsedFilters = [];
|
|
218
|
+
const seenFilterColumns = /* @__PURE__ */ new Set();
|
|
219
|
+
const parseFilterEntry = (columnName, rawValue) => {
|
|
220
|
+
if (!columnName || rawValue === null || rawValue === void 0 || seenFilterColumns.has(columnName)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const filterValue = String(rawValue).trim();
|
|
224
|
+
if (!filterValue) return;
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(filterValue);
|
|
227
|
+
if (parsed && typeof parsed === "object" && parsed.operator) {
|
|
228
|
+
parsedFilters.push({
|
|
229
|
+
column: columnName,
|
|
230
|
+
operator: parsed.operator,
|
|
231
|
+
value: parsed.value,
|
|
232
|
+
dataType: parsed.dataType
|
|
233
|
+
});
|
|
234
|
+
} else {
|
|
235
|
+
parsedFilters.push({
|
|
236
|
+
column: columnName,
|
|
237
|
+
operator: "contains",
|
|
238
|
+
value: filterValue
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
parsedFilters.push({
|
|
243
|
+
column: columnName,
|
|
244
|
+
operator: "contains",
|
|
245
|
+
value: filterValue
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
seenFilterColumns.add(columnName);
|
|
249
|
+
};
|
|
218
250
|
Object.keys(req.query).forEach((key) => {
|
|
219
251
|
const match = key.match(/^filter\[(.+)\]$/);
|
|
220
|
-
if (match
|
|
221
|
-
|
|
252
|
+
if (match) {
|
|
253
|
+
const columnName = match[1];
|
|
254
|
+
parseFilterEntry(columnName, req.query[key]);
|
|
222
255
|
}
|
|
223
256
|
});
|
|
224
|
-
|
|
225
|
-
|
|
257
|
+
const nestedFilters = req.query.filter;
|
|
258
|
+
if (nestedFilters && typeof nestedFilters === "object" && !Array.isArray(nestedFilters)) {
|
|
259
|
+
Object.entries(nestedFilters).forEach(([columnName, rawValue]) => {
|
|
260
|
+
parseFilterEntry(columnName, rawValue);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
console.log("Parsed filters:", parsedFilters);
|
|
226
264
|
const pool = getConnection();
|
|
227
265
|
if (!pool || !pool.connected) {
|
|
228
266
|
return res.status(400).json({ error: "Not connected to database" });
|
|
229
267
|
}
|
|
230
|
-
const
|
|
231
|
-
if (
|
|
268
|
+
const columnMetadata = {};
|
|
269
|
+
if (parsedFilters.length > 0) {
|
|
232
270
|
try {
|
|
233
|
-
const columnNames =
|
|
271
|
+
const columnNames = [...new Set(parsedFilters.map((f) => f.column))];
|
|
234
272
|
const placeholders = columnNames.map((_, i) => `@col${i}`).join(", ");
|
|
235
273
|
const validateQuery = `
|
|
236
|
-
SELECT COLUMN_NAME
|
|
274
|
+
SELECT COLUMN_NAME, DATA_TYPE
|
|
237
275
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
238
276
|
WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @table AND COLUMN_NAME IN (${placeholders})
|
|
239
277
|
`;
|
|
@@ -243,32 +281,167 @@ tableRoutes.get("/:schema/:table/data", async (req, res) => {
|
|
|
243
281
|
...columnNames.map((col, i) => ({ name: `col${i}`, value: col, type: sql.NVarChar }))
|
|
244
282
|
];
|
|
245
283
|
const validateResult = await executeQuery(validateQuery, validateParams);
|
|
246
|
-
|
|
247
|
-
|
|
284
|
+
validateResult.forEach((r) => {
|
|
285
|
+
columnMetadata[r.COLUMN_NAME] = {
|
|
286
|
+
dataType: r.DATA_TYPE,
|
|
287
|
+
exists: true
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
console.log("Column metadata:", columnMetadata);
|
|
248
291
|
} catch (e) {
|
|
249
292
|
console.error("Error validating filter columns:", e);
|
|
250
|
-
console.error("Filters that failed validation:", filters);
|
|
251
293
|
}
|
|
252
294
|
}
|
|
295
|
+
const getSqlType = (dataType) => {
|
|
296
|
+
const dt = dataType.toLowerCase();
|
|
297
|
+
if (dt === "int" || dt === "integer") return sql.Int;
|
|
298
|
+
if (dt === "bigint") return sql.BigInt;
|
|
299
|
+
if (dt === "smallint") return sql.SmallInt;
|
|
300
|
+
if (dt === "tinyint") return sql.TinyInt;
|
|
301
|
+
if (dt === "bit") return sql.Bit;
|
|
302
|
+
if (dt === "float" || dt === "real" || dt === "double precision") return sql.Float;
|
|
303
|
+
if (dt === "decimal" || dt === "numeric" || dt === "money" || dt === "smallmoney") return sql.Decimal(18, 2);
|
|
304
|
+
if (dt === "datetime" || dt === "datetime2" || dt === "smalldatetime") return sql.DateTime;
|
|
305
|
+
if (dt === "date") return sql.Date;
|
|
306
|
+
if (dt === "time") return sql.Time;
|
|
307
|
+
if (dt === "uniqueidentifier") return sql.UniqueIdentifier;
|
|
308
|
+
return sql.NVarChar;
|
|
309
|
+
};
|
|
253
310
|
let whereClause = "";
|
|
254
311
|
const filterParams = [];
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
312
|
+
let paramIndex = 0;
|
|
313
|
+
if (parsedFilters.length > 0) {
|
|
314
|
+
const whereConditions = [];
|
|
315
|
+
parsedFilters.forEach((filter) => {
|
|
316
|
+
const columnName = filter.column;
|
|
317
|
+
const metadata = columnMetadata[columnName];
|
|
318
|
+
if (!metadata || !metadata.exists) {
|
|
319
|
+
console.warn(`Filter column ${columnName} not found, skipping`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const dataType = filter.dataType || metadata.dataType;
|
|
323
|
+
const operator = filter.operator;
|
|
324
|
+
const value = filter.value;
|
|
325
|
+
if (value === null || value === void 0 || value === "") {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const sqlType = getSqlType(dataType);
|
|
329
|
+
const paramName = `filter${paramIndex}`;
|
|
330
|
+
try {
|
|
331
|
+
let condition = "";
|
|
332
|
+
switch (operator) {
|
|
333
|
+
// Text operators
|
|
334
|
+
case "contains":
|
|
335
|
+
filterParams.push({ name: paramName, value: `%${String(value)}%`, type: sql.NVarChar });
|
|
336
|
+
condition = `[${columnName}] LIKE @${paramName}`;
|
|
337
|
+
break;
|
|
338
|
+
case "equals":
|
|
339
|
+
filterParams.push({ name: paramName, value: String(value), type: sqlType });
|
|
340
|
+
condition = `[${columnName}] = @${paramName}`;
|
|
341
|
+
break;
|
|
342
|
+
case "startsWith":
|
|
343
|
+
filterParams.push({ name: paramName, value: `${String(value)}%`, type: sql.NVarChar });
|
|
344
|
+
condition = `[${columnName}] LIKE @${paramName}`;
|
|
345
|
+
break;
|
|
346
|
+
case "endsWith":
|
|
347
|
+
filterParams.push({ name: paramName, value: `%${String(value)}`, type: sql.NVarChar });
|
|
348
|
+
condition = `[${columnName}] LIKE @${paramName}`;
|
|
349
|
+
break;
|
|
350
|
+
case "notContains":
|
|
351
|
+
filterParams.push({ name: paramName, value: `%${String(value)}%`, type: sql.NVarChar });
|
|
352
|
+
condition = `[${columnName}] NOT LIKE @${paramName}`;
|
|
353
|
+
break;
|
|
354
|
+
// Number operators
|
|
355
|
+
case "eq":
|
|
356
|
+
filterParams.push({ name: paramName, value: Number(value), type: sqlType });
|
|
357
|
+
condition = `[${columnName}] = @${paramName}`;
|
|
358
|
+
break;
|
|
359
|
+
case "gt":
|
|
360
|
+
filterParams.push({ name: paramName, value: Number(value), type: sqlType });
|
|
361
|
+
condition = `[${columnName}] > @${paramName}`;
|
|
362
|
+
break;
|
|
363
|
+
case "gte":
|
|
364
|
+
filterParams.push({ name: paramName, value: Number(value), type: sqlType });
|
|
365
|
+
condition = `[${columnName}] >= @${paramName}`;
|
|
366
|
+
break;
|
|
367
|
+
case "lt":
|
|
368
|
+
filterParams.push({ name: paramName, value: Number(value), type: sqlType });
|
|
369
|
+
condition = `[${columnName}] < @${paramName}`;
|
|
370
|
+
break;
|
|
371
|
+
case "lte":
|
|
372
|
+
filterParams.push({ name: paramName, value: Number(value), type: sqlType });
|
|
373
|
+
condition = `[${columnName}] <= @${paramName}`;
|
|
374
|
+
break;
|
|
375
|
+
case "between":
|
|
376
|
+
if (typeof value === "object" && "from" in value && "to" in value) {
|
|
377
|
+
const fromParam = `filter${paramIndex}`;
|
|
378
|
+
const toParam = `filter${paramIndex + 1}`;
|
|
379
|
+
filterParams.push({ name: fromParam, value: Number(value.from), type: sqlType });
|
|
380
|
+
filterParams.push({ name: toParam, value: Number(value.to), type: sqlType });
|
|
381
|
+
condition = `[${columnName}] BETWEEN @${fromParam} AND @${toParam}`;
|
|
382
|
+
paramIndex++;
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
// Date operators
|
|
386
|
+
case "dateEq":
|
|
387
|
+
filterParams.push({ name: paramName, value: String(value), type: sqlType });
|
|
388
|
+
condition = `CAST([${columnName}] AS DATE) = CAST(@${paramName} AS DATE)`;
|
|
389
|
+
break;
|
|
390
|
+
case "dateAfter":
|
|
391
|
+
filterParams.push({ name: paramName, value: String(value), type: sqlType });
|
|
392
|
+
condition = `CAST([${columnName}] AS DATE) > CAST(@${paramName} AS DATE)`;
|
|
393
|
+
break;
|
|
394
|
+
case "dateBefore":
|
|
395
|
+
filterParams.push({ name: paramName, value: String(value), type: sqlType });
|
|
396
|
+
condition = `CAST([${columnName}] AS DATE) < CAST(@${paramName} AS DATE)`;
|
|
397
|
+
break;
|
|
398
|
+
case "dateBetween":
|
|
399
|
+
if (typeof value === "object" && "from" in value && "to" in value) {
|
|
400
|
+
const fromParam = `filter${paramIndex}`;
|
|
401
|
+
const toParam = `filter${paramIndex + 1}`;
|
|
402
|
+
filterParams.push({ name: fromParam, value: String(value.from), type: sqlType });
|
|
403
|
+
filterParams.push({ name: toParam, value: String(value.to), type: sqlType });
|
|
404
|
+
condition = `CAST([${columnName}] AS DATE) BETWEEN CAST(@${fromParam} AS DATE) AND CAST(@${toParam} AS DATE)`;
|
|
405
|
+
paramIndex++;
|
|
406
|
+
}
|
|
407
|
+
break;
|
|
408
|
+
// Multiple select operators
|
|
409
|
+
case "in":
|
|
410
|
+
case "notIn":
|
|
411
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
412
|
+
const placeholders = value.map((_, i) => `@${paramName}_${i}`).join(", ");
|
|
413
|
+
value.forEach((val, i) => {
|
|
414
|
+
filterParams.push({ name: `${paramName}_${i}`, value: val, type: sqlType });
|
|
415
|
+
});
|
|
416
|
+
const inOperator = operator === "in" ? "IN" : "NOT IN";
|
|
417
|
+
condition = `[${columnName}] ${inOperator} (${placeholders})`;
|
|
418
|
+
paramIndex += value.length - 1;
|
|
419
|
+
}
|
|
420
|
+
break;
|
|
421
|
+
default:
|
|
422
|
+
console.warn(`Unknown filter operator: ${operator}, falling back to contains`);
|
|
423
|
+
filterParams.push({ name: paramName, value: `%${String(value)}%`, type: sql.NVarChar });
|
|
424
|
+
condition = `[${columnName}] LIKE @${paramName}`;
|
|
425
|
+
}
|
|
426
|
+
if (condition) {
|
|
427
|
+
whereConditions.push(condition);
|
|
428
|
+
paramIndex++;
|
|
429
|
+
}
|
|
430
|
+
} catch (error) {
|
|
431
|
+
console.error(`Error building filter condition for ${columnName}:`, error);
|
|
432
|
+
}
|
|
259
433
|
});
|
|
260
|
-
if (
|
|
261
|
-
const whereConditions = validFilters.map((col, index) => {
|
|
262
|
-
const filterValue = filters[col];
|
|
263
|
-
filterParams.push({ name: `filter${index}`, value: `%${filterValue.trim()}%`, type: sql.NVarChar });
|
|
264
|
-
return `[${col}] LIKE @filter${index}`;
|
|
265
|
-
});
|
|
434
|
+
if (whereConditions.length > 0) {
|
|
266
435
|
whereClause = `WHERE ${whereConditions.join(" AND ")}`;
|
|
267
436
|
console.log("Applying WHERE clause:", whereClause);
|
|
268
|
-
console.log("Filter params:", filterParams);
|
|
269
|
-
console.log("Valid filters:", validFilters);
|
|
437
|
+
console.log("Filter params:", filterParams.map((p) => ({ name: p.name, value: p.value })));
|
|
270
438
|
}
|
|
271
439
|
}
|
|
440
|
+
const qualifiedDataWhereClause = parsedFilters.reduce((clause, filter) => {
|
|
441
|
+
const escapedColumn = filter.column.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
442
|
+
const pattern = new RegExp(`\\[${escapedColumn}\\]`, "g");
|
|
443
|
+
return clause.replace(pattern, `[t].[${filter.column}]`);
|
|
444
|
+
}, whereClause);
|
|
272
445
|
const countQuery = `SELECT COUNT(*) as total FROM [${schema}].[${table}]${whereClause ? " " + whereClause : ""}`;
|
|
273
446
|
const countResult = await executeQuery(countQuery, filterParams.length > 0 ? filterParams : []);
|
|
274
447
|
const total = countResult[0]?.total || 0;
|
|
@@ -314,8 +487,7 @@ tableRoutes.get("/:schema/:table/data", async (req, res) => {
|
|
|
314
487
|
const fkJoins = [];
|
|
315
488
|
const fkSelects = [];
|
|
316
489
|
const fkDisplayColumns = {};
|
|
317
|
-
|
|
318
|
-
const fkQuery = `
|
|
490
|
+
const fkQuery = `
|
|
319
491
|
SELECT
|
|
320
492
|
kcu1.COLUMN_NAME as fkColumnName,
|
|
321
493
|
kcu2.TABLE_SCHEMA as referencedSchema,
|
|
@@ -334,73 +506,74 @@ tableRoutes.get("/:schema/:table/data", async (req, res) => {
|
|
|
334
506
|
WHERE kcu1.TABLE_SCHEMA = @schema
|
|
335
507
|
AND kcu1.TABLE_NAME = @table
|
|
336
508
|
`;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
509
|
+
console.log("Fetching foreign keys with query:", fkQuery);
|
|
510
|
+
const foreignKeys = await executeQuery(fkQuery, [
|
|
511
|
+
{ name: "schema", value: schema, type: sql.NVarChar },
|
|
512
|
+
{ name: "table", value: table, type: sql.NVarChar }
|
|
513
|
+
]);
|
|
514
|
+
console.log(`Found ${foreignKeys.length} foreign key(s)`);
|
|
515
|
+
if (foreignKeys.length > 0) {
|
|
516
|
+
const uniqueRefTables = Array.from(
|
|
517
|
+
new Set(foreignKeys.map((fk) => `${fk.referencedSchema}.${fk.referencedTable}`))
|
|
518
|
+
);
|
|
519
|
+
const tableConditions = uniqueRefTables.map((tableRef, idx) => {
|
|
520
|
+
const [refSchema, refTable] = tableRef.split(".");
|
|
521
|
+
return `(TABLE_SCHEMA = @refSchema${idx} AND TABLE_NAME = @refTable${idx})`;
|
|
522
|
+
}).join(" OR ");
|
|
523
|
+
const batchColumnsQuery = `
|
|
352
524
|
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, ORDINAL_POSITION
|
|
353
525
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
354
526
|
WHERE ${tableConditions}
|
|
355
527
|
ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION
|
|
356
528
|
`;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
529
|
+
const batchParams = uniqueRefTables.flatMap((tableRef, idx) => {
|
|
530
|
+
const [refSchema, refTable] = tableRef.split(".");
|
|
531
|
+
return [
|
|
532
|
+
{ name: `refSchema${idx}`, value: refSchema, type: sql.NVarChar },
|
|
533
|
+
{ name: `refTable${idx}`, value: refTable, type: sql.NVarChar }
|
|
534
|
+
];
|
|
535
|
+
});
|
|
536
|
+
console.log("Fetching referenced table columns with query:", batchColumnsQuery);
|
|
537
|
+
console.log("Batch parameters:", batchParams.map((p) => ({ name: p.name, value: p.value })));
|
|
538
|
+
const allRefColumns = await executeQuery(batchColumnsQuery, batchParams);
|
|
539
|
+
console.log(`Found columns for ${uniqueRefTables.length} referenced table(s)`);
|
|
540
|
+
const columnsByTable = {};
|
|
541
|
+
allRefColumns.forEach((col) => {
|
|
542
|
+
const key = `${col.TABLE_SCHEMA}.${col.TABLE_NAME}`;
|
|
543
|
+
if (!columnsByTable[key]) {
|
|
544
|
+
columnsByTable[key] = [];
|
|
545
|
+
}
|
|
546
|
+
columnsByTable[key].push(col);
|
|
547
|
+
});
|
|
548
|
+
for (const fk of foreignKeys) {
|
|
549
|
+
const fkColumn = fk.fkColumnName;
|
|
550
|
+
const refSchema = fk.referencedSchema;
|
|
551
|
+
const refTable = fk.referencedTable;
|
|
552
|
+
const refColumn = fk.referencedColumn;
|
|
553
|
+
const tableKey = `${refSchema}.${refTable}`;
|
|
554
|
+
const refColumns = columnsByTable[tableKey] || [];
|
|
555
|
+
const preferredNames = ["name", "title", "description", "code"];
|
|
556
|
+
let displayColumn = null;
|
|
557
|
+
for (const preferredName of preferredNames) {
|
|
558
|
+
const found = refColumns.find(
|
|
559
|
+
(col) => col.COLUMN_NAME.toLowerCase() === preferredName.toLowerCase()
|
|
560
|
+
);
|
|
561
|
+
if (found) {
|
|
562
|
+
displayColumn = found.COLUMN_NAME;
|
|
563
|
+
break;
|
|
393
564
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
565
|
+
}
|
|
566
|
+
if (!displayColumn) {
|
|
567
|
+
const stringTypes = ["varchar", "nvarchar", "char", "nchar", "text", "ntext"];
|
|
568
|
+
const found = refColumns.find(
|
|
569
|
+
(col) => stringTypes.some((type) => col.DATA_TYPE.toLowerCase().includes(type))
|
|
570
|
+
);
|
|
571
|
+
if (found) {
|
|
572
|
+
displayColumn = found.COLUMN_NAME;
|
|
402
573
|
}
|
|
403
|
-
|
|
574
|
+
}
|
|
575
|
+
if (displayColumn) {
|
|
576
|
+
if (fkDisplayMode === "key-display" || fkDisplayMode === "display-only") {
|
|
404
577
|
const alias = `fk_${fkColumn}`;
|
|
405
578
|
fkJoins.push({
|
|
406
579
|
alias,
|
|
@@ -411,8 +584,8 @@ tableRoutes.get("/:schema/:table/data", async (req, res) => {
|
|
|
411
584
|
displayColumn
|
|
412
585
|
});
|
|
413
586
|
fkSelects.push(`${alias}.[${displayColumn}] as [${fkColumn}_display]`);
|
|
414
|
-
fkDisplayColumns[fkColumn] = displayColumn;
|
|
415
587
|
}
|
|
588
|
+
fkDisplayColumns[fkColumn] = displayColumn;
|
|
416
589
|
}
|
|
417
590
|
}
|
|
418
591
|
}
|
|
@@ -437,13 +610,13 @@ tableRoutes.get("/:schema/:table/data", async (req, res) => {
|
|
|
437
610
|
SELECT ${allSelects}
|
|
438
611
|
FROM [${schema}].[${table}] ${baseTableAlias}
|
|
439
612
|
${buildJoinClauses(baseTableAlias)}
|
|
440
|
-
${
|
|
613
|
+
${qualifiedDataWhereClause}
|
|
441
614
|
ORDER BY ${baseTableAlias}.[${orderByColumn}] ${orderByDirection}
|
|
442
615
|
OFFSET @offset ROWS
|
|
443
616
|
FETCH NEXT @pageSize ROWS ONLY
|
|
444
617
|
`;
|
|
445
618
|
generatedQuery = `SELECT ${allSelects}
|
|
446
|
-
FROM [${schema}].[${table}] ${baseTableAlias}${fkJoins.length > 0 ? "\n" + buildJoinClauses(baseTableAlias) : ""}${
|
|
619
|
+
FROM [${schema}].[${table}] ${baseTableAlias}${fkJoins.length > 0 ? "\n" + buildJoinClauses(baseTableAlias) : ""}${qualifiedDataWhereClause ? "\n" + qualifiedDataWhereClause : ""}
|
|
447
620
|
ORDER BY ${baseTableAlias}.[${orderByColumn}] ${orderByDirection}
|
|
448
621
|
OFFSET ${offset} ROWS
|
|
449
622
|
FETCH NEXT ${pageSize} ROWS ONLY`;
|
|
@@ -509,6 +682,22 @@ ORDER BY ${baseTableAlias}.rn`;
|
|
|
509
682
|
return filteredRow;
|
|
510
683
|
});
|
|
511
684
|
}
|
|
685
|
+
if (generatedQuery && filterParams.length > 0) {
|
|
686
|
+
const formatSqlLiteral = (value) => {
|
|
687
|
+
if (value === null || value === void 0) return "NULL";
|
|
688
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "NULL";
|
|
689
|
+
if (typeof value === "boolean") return value ? "1" : "0";
|
|
690
|
+
if (value instanceof Date) return `'${value.toISOString().replace("T", " ").slice(0, 19)}'`;
|
|
691
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
692
|
+
};
|
|
693
|
+
const paramValues = new Map(
|
|
694
|
+
filterParams.map((p) => [p.name, p.value])
|
|
695
|
+
);
|
|
696
|
+
generatedQuery = generatedQuery.replace(/@([A-Za-z0-9_]+)/g, (full, name) => {
|
|
697
|
+
if (!paramValues.has(name)) return full;
|
|
698
|
+
return formatSqlLiteral(paramValues.get(name));
|
|
699
|
+
});
|
|
700
|
+
}
|
|
512
701
|
res.json({
|
|
513
702
|
data,
|
|
514
703
|
query: generatedQuery,
|
|
@@ -637,6 +826,168 @@ tableRoutes.post("/:schema/:table/related-data", async (req, res) => {
|
|
|
637
826
|
res.status(500).json({ error: error.message || "Failed to fetch related data" });
|
|
638
827
|
}
|
|
639
828
|
});
|
|
829
|
+
tableRoutes.get("/:schema/:table/distinct-values/:column", async (req, res) => {
|
|
830
|
+
try {
|
|
831
|
+
const { schema, table, column } = req.params;
|
|
832
|
+
const searchQuery = req.query.search;
|
|
833
|
+
const columnsParam = req.query.columns;
|
|
834
|
+
const pool = getConnection();
|
|
835
|
+
if (!pool || !pool.connected) {
|
|
836
|
+
return res.status(400).json({ error: "Not connected to database" });
|
|
837
|
+
}
|
|
838
|
+
const columnQuery = `
|
|
839
|
+
SELECT
|
|
840
|
+
c.COLUMN_NAME,
|
|
841
|
+
c.DATA_TYPE,
|
|
842
|
+
fk.REFERENCED_TABLE_SCHEMA as referencedSchema,
|
|
843
|
+
fk.REFERENCED_TABLE_NAME as referencedTable,
|
|
844
|
+
fk.REFERENCED_COLUMN_NAME as referencedColumn
|
|
845
|
+
FROM INFORMATION_SCHEMA.COLUMNS c
|
|
846
|
+
LEFT JOIN (
|
|
847
|
+
SELECT
|
|
848
|
+
kcu1.TABLE_SCHEMA,
|
|
849
|
+
kcu1.TABLE_NAME,
|
|
850
|
+
kcu1.COLUMN_NAME,
|
|
851
|
+
kcu2.TABLE_SCHEMA as REFERENCED_TABLE_SCHEMA,
|
|
852
|
+
kcu2.TABLE_NAME as REFERENCED_TABLE_NAME,
|
|
853
|
+
kcu2.COLUMN_NAME as REFERENCED_COLUMN_NAME
|
|
854
|
+
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc
|
|
855
|
+
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu1
|
|
856
|
+
ON rc.CONSTRAINT_CATALOG = kcu1.CONSTRAINT_CATALOG
|
|
857
|
+
AND rc.CONSTRAINT_SCHEMA = kcu1.CONSTRAINT_SCHEMA
|
|
858
|
+
AND rc.CONSTRAINT_NAME = kcu1.CONSTRAINT_NAME
|
|
859
|
+
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu2
|
|
860
|
+
ON rc.UNIQUE_CONSTRAINT_CATALOG = kcu2.CONSTRAINT_CATALOG
|
|
861
|
+
AND rc.UNIQUE_CONSTRAINT_SCHEMA = kcu2.CONSTRAINT_SCHEMA
|
|
862
|
+
AND rc.UNIQUE_CONSTRAINT_NAME = kcu2.CONSTRAINT_NAME
|
|
863
|
+
AND kcu1.ORDINAL_POSITION = kcu2.ORDINAL_POSITION
|
|
864
|
+
) fk ON c.TABLE_SCHEMA = fk.TABLE_SCHEMA
|
|
865
|
+
AND c.TABLE_NAME = fk.TABLE_NAME
|
|
866
|
+
AND c.COLUMN_NAME = fk.COLUMN_NAME
|
|
867
|
+
WHERE c.TABLE_SCHEMA = @schema AND c.TABLE_NAME = @table AND c.COLUMN_NAME = @column
|
|
868
|
+
`;
|
|
869
|
+
const columnResult = await executeQuery(columnQuery, [
|
|
870
|
+
{ name: "schema", value: schema, type: sql.NVarChar },
|
|
871
|
+
{ name: "table", value: table, type: sql.NVarChar },
|
|
872
|
+
{ name: "column", value: column, type: sql.NVarChar }
|
|
873
|
+
]);
|
|
874
|
+
if (columnResult.length === 0) {
|
|
875
|
+
return res.status(400).json({ error: "Column not found" });
|
|
876
|
+
}
|
|
877
|
+
const columnInfo = columnResult[0];
|
|
878
|
+
const isForeignKey = !!columnInfo.referencedSchema && !!columnInfo.referencedTable;
|
|
879
|
+
let query = "";
|
|
880
|
+
const params = [];
|
|
881
|
+
let displayColumn = null;
|
|
882
|
+
if (isForeignKey) {
|
|
883
|
+
const refSchema = columnInfo.referencedSchema;
|
|
884
|
+
const refTable = columnInfo.referencedTable;
|
|
885
|
+
const refColumn = columnInfo.referencedColumn;
|
|
886
|
+
let columnsToSelect = [];
|
|
887
|
+
if (columnsParam) {
|
|
888
|
+
columnsToSelect = columnsParam.split(",").map((c) => c.trim()).filter((c) => c);
|
|
889
|
+
}
|
|
890
|
+
if (columnsToSelect.length > 0) {
|
|
891
|
+
const selectCols = columnsToSelect.map((col) => `[${col}]`).join(", ");
|
|
892
|
+
query = `SELECT DISTINCT ${selectCols} FROM [${refSchema}].[${refTable}]`;
|
|
893
|
+
if (searchQuery && searchQuery.trim()) {
|
|
894
|
+
const searchCols = columnsToSelect.map((col) => `[${col}] LIKE @search`).join(" OR ");
|
|
895
|
+
query += ` WHERE (${searchCols}) AND [${refColumn}] IS NOT NULL`;
|
|
896
|
+
params.push({ name: "search", value: `%${searchQuery.trim()}%`, type: sql.NVarChar });
|
|
897
|
+
} else {
|
|
898
|
+
query += ` WHERE [${refColumn}] IS NOT NULL`;
|
|
899
|
+
}
|
|
900
|
+
const orderByCol = columnsToSelect.length > 1 ? columnsToSelect[1] : columnsToSelect[0];
|
|
901
|
+
query += ` ORDER BY [${orderByCol}] OFFSET 0 ROWS FETCH NEXT 1000 ROWS ONLY`;
|
|
902
|
+
} else {
|
|
903
|
+
const refColumnsQuery = `
|
|
904
|
+
SELECT COLUMN_NAME, DATA_TYPE
|
|
905
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
906
|
+
WHERE TABLE_SCHEMA = @refSchema AND TABLE_NAME = @refTable
|
|
907
|
+
ORDER BY ORDINAL_POSITION
|
|
908
|
+
`;
|
|
909
|
+
const refColumns = await executeQuery(refColumnsQuery, [
|
|
910
|
+
{ name: "refSchema", value: refSchema, type: sql.NVarChar },
|
|
911
|
+
{ name: "refTable", value: refTable, type: sql.NVarChar }
|
|
912
|
+
]);
|
|
913
|
+
const preferredNames = ["name", "title", "description", "code"];
|
|
914
|
+
for (const preferredName of preferredNames) {
|
|
915
|
+
const found = refColumns.find(
|
|
916
|
+
(col) => col.COLUMN_NAME.toLowerCase() === preferredName.toLowerCase() && col.COLUMN_NAME.toLowerCase() !== refColumn.toLowerCase()
|
|
917
|
+
);
|
|
918
|
+
if (found) {
|
|
919
|
+
displayColumn = found.COLUMN_NAME;
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (!displayColumn) {
|
|
924
|
+
const stringTypes = ["varchar", "nvarchar", "char", "nchar", "text", "ntext"];
|
|
925
|
+
const found = refColumns.find(
|
|
926
|
+
(col) => col.COLUMN_NAME.toLowerCase() !== refColumn.toLowerCase() && stringTypes.some((type) => col.DATA_TYPE.toLowerCase().includes(type))
|
|
927
|
+
);
|
|
928
|
+
if (found) {
|
|
929
|
+
displayColumn = found.COLUMN_NAME;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const selectCols = displayColumn ? `[${refColumn}], [${displayColumn}]` : `[${refColumn}]`;
|
|
933
|
+
query = `SELECT DISTINCT ${selectCols} FROM [${refSchema}].[${refTable}]`;
|
|
934
|
+
if (searchQuery && searchQuery.trim()) {
|
|
935
|
+
if (displayColumn) {
|
|
936
|
+
query += ` WHERE [${displayColumn}] LIKE @search OR [${refColumn}] LIKE @search`;
|
|
937
|
+
} else {
|
|
938
|
+
query += ` WHERE [${refColumn}] LIKE @search`;
|
|
939
|
+
}
|
|
940
|
+
params.push({ name: "search", value: `%${searchQuery.trim()}%`, type: sql.NVarChar });
|
|
941
|
+
query += ` AND [${refColumn}] IS NOT NULL`;
|
|
942
|
+
} else {
|
|
943
|
+
query += ` WHERE [${refColumn}] IS NOT NULL`;
|
|
944
|
+
}
|
|
945
|
+
query += ` ORDER BY ${displayColumn ? `[${displayColumn}]` : `[${refColumn}]`} OFFSET 0 ROWS FETCH NEXT 1000 ROWS ONLY`;
|
|
946
|
+
}
|
|
947
|
+
} else {
|
|
948
|
+
const dataType = columnInfo.DATA_TYPE.toLowerCase();
|
|
949
|
+
const columnsToSelect = columnsParam ? columnsParam.split(",").map((c) => c.trim()).filter((c) => c) : [];
|
|
950
|
+
if (columnsToSelect.length > 0) {
|
|
951
|
+
const escapedColumns = columnsToSelect.map((col) => col.replace(/]/g, "]]"));
|
|
952
|
+
const keyColumn = escapedColumns[0];
|
|
953
|
+
const orderByColumn = escapedColumns.length > 1 ? escapedColumns[1] : keyColumn;
|
|
954
|
+
query = `SELECT DISTINCT ${escapedColumns.map((col) => `[${col}]`).join(", ")} FROM [${schema}].[${table}]`;
|
|
955
|
+
if (searchQuery && searchQuery.trim()) {
|
|
956
|
+
const searchCols = escapedColumns.map((col) => `TRY_CAST([${col}] AS NVARCHAR(4000)) LIKE @search`).join(" OR ");
|
|
957
|
+
query += ` WHERE (${searchCols}) AND [${keyColumn}] IS NOT NULL`;
|
|
958
|
+
params.push({ name: "search", value: `%${searchQuery.trim()}%`, type: sql.NVarChar });
|
|
959
|
+
} else {
|
|
960
|
+
query += ` WHERE [${keyColumn}] IS NOT NULL`;
|
|
961
|
+
}
|
|
962
|
+
query += ` ORDER BY [${orderByColumn}] OFFSET 0 ROWS FETCH NEXT 1000 ROWS ONLY`;
|
|
963
|
+
} else {
|
|
964
|
+
query = `SELECT DISTINCT [${column}] FROM [${schema}].[${table}]`;
|
|
965
|
+
if (searchQuery && searchQuery.trim()) {
|
|
966
|
+
if (["varchar", "nvarchar", "char", "nchar", "text", "ntext"].some((t) => dataType.includes(t))) {
|
|
967
|
+
query += ` WHERE [${column}] LIKE @search`;
|
|
968
|
+
params.push({ name: "search", value: `%${searchQuery.trim()}%`, type: sql.NVarChar });
|
|
969
|
+
query += ` AND [${column}] IS NOT NULL`;
|
|
970
|
+
} else {
|
|
971
|
+
query += ` WHERE [${column}] IS NOT NULL`;
|
|
972
|
+
}
|
|
973
|
+
} else {
|
|
974
|
+
query += ` WHERE [${column}] IS NOT NULL`;
|
|
975
|
+
}
|
|
976
|
+
query += ` ORDER BY [${column}] OFFSET 0 ROWS FETCH NEXT 1000 ROWS ONLY`;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
const result = await executeQuery(query, params);
|
|
980
|
+
res.json(result);
|
|
981
|
+
} catch (error) {
|
|
982
|
+
console.error("Error fetching distinct values:", error);
|
|
983
|
+
const errorMessage = error.message || "";
|
|
984
|
+
if (errorMessage.includes("Login failed") || errorMessage.includes("authentication")) {
|
|
985
|
+
const { disconnect: disconnect2 } = await import("./mssql-UZUN227W.js");
|
|
986
|
+
await disconnect2();
|
|
987
|
+
}
|
|
988
|
+
res.status(500).json({ error: error.message || "Failed to fetch distinct values" });
|
|
989
|
+
}
|
|
990
|
+
});
|
|
640
991
|
|
|
641
992
|
// src/server/routes/query.ts
|
|
642
993
|
import { Router as Router3 } from "express";
|