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