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