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.
@@ -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 filters = {};
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 && req.query[key] && String(req.query[key]).trim()) {
221
- filters[match[1]] = String(req.query[key]).trim();
252
+ if (match) {
253
+ const columnName = match[1];
254
+ parseFilterEntry(columnName, req.query[key]);
222
255
  }
223
256
  });
224
- console.log("Received filters from query:", filters);
225
- console.log("All query params:", req.query);
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 filterColumns = [];
231
- if (Object.keys(filters).length > 0) {
268
+ const columnMetadata = {};
269
+ if (parsedFilters.length > 0) {
232
270
  try {
233
- const columnNames = Object.keys(filters);
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
- filterColumns.push(...validateResult.map((r) => r.COLUMN_NAME));
247
- console.log("Validated filter columns:", filterColumns, "from filters:", filters);
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
- if (filterColumns.length > 0) {
256
- const validFilters = filterColumns.filter((col) => {
257
- const filterValue = filters[col];
258
- return filterValue && filterValue.trim() !== "";
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 (validFilters.length > 0) {
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
- if (fkDisplayMode === "key-display" || fkDisplayMode === "display-only") {
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
- console.log("Fetching foreign keys with query:", fkQuery);
338
- const foreignKeys = await executeQuery(fkQuery, [
339
- { name: "schema", value: schema, type: sql.NVarChar },
340
- { name: "table", value: table, type: sql.NVarChar }
341
- ]);
342
- console.log(`Found ${foreignKeys.length} foreign key(s)`);
343
- if (foreignKeys.length > 0) {
344
- const uniqueRefTables = Array.from(
345
- new Set(foreignKeys.map((fk) => `${fk.referencedSchema}.${fk.referencedTable}`))
346
- );
347
- const tableConditions = uniqueRefTables.map((tableRef, idx) => {
348
- const [refSchema, refTable] = tableRef.split(".");
349
- return `(TABLE_SCHEMA = @refSchema${idx} AND TABLE_NAME = @refTable${idx})`;
350
- }).join(" OR ");
351
- const batchColumnsQuery = `
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
- const batchParams = uniqueRefTables.flatMap((tableRef, idx) => {
358
- const [refSchema, refTable] = tableRef.split(".");
359
- return [
360
- { name: `refSchema${idx}`, value: refSchema, type: sql.NVarChar },
361
- { name: `refTable${idx}`, value: refTable, type: sql.NVarChar }
362
- ];
363
- });
364
- console.log("Fetching referenced table columns with query:", batchColumnsQuery);
365
- console.log("Batch parameters:", batchParams.map((p) => ({ name: p.name, value: p.value })));
366
- const allRefColumns = await executeQuery(batchColumnsQuery, batchParams);
367
- console.log(`Found columns for ${uniqueRefTables.length} referenced table(s)`);
368
- const columnsByTable = {};
369
- allRefColumns.forEach((col) => {
370
- const key = `${col.TABLE_SCHEMA}.${col.TABLE_NAME}`;
371
- if (!columnsByTable[key]) {
372
- columnsByTable[key] = [];
373
- }
374
- columnsByTable[key].push(col);
375
- });
376
- for (const fk of foreignKeys) {
377
- const fkColumn = fk.fkColumnName;
378
- const refSchema = fk.referencedSchema;
379
- const refTable = fk.referencedTable;
380
- const refColumn = fk.referencedColumn;
381
- const tableKey = `${refSchema}.${refTable}`;
382
- const refColumns = columnsByTable[tableKey] || [];
383
- const preferredNames = ["name", "title", "description", "code"];
384
- let displayColumn = null;
385
- for (const preferredName of preferredNames) {
386
- const found = refColumns.find(
387
- (col) => col.COLUMN_NAME.toLowerCase() === preferredName.toLowerCase()
388
- );
389
- if (found) {
390
- displayColumn = found.COLUMN_NAME;
391
- break;
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
- if (!displayColumn) {
395
- const stringTypes = ["varchar", "nvarchar", "char", "nchar", "text", "ntext"];
396
- const found = refColumns.find(
397
- (col) => stringTypes.some((type) => col.DATA_TYPE.toLowerCase().includes(type))
398
- );
399
- if (found) {
400
- displayColumn = found.COLUMN_NAME;
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
- if (displayColumn) {
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
- ${whereClause}
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) : ""}${whereClause ? "\n" + whereClause : ""}
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datapeek",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "A local SQL database browser CLI tool",
5
5
  "type": "module",
6
6
  "bin": {