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/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 filters = {};
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 && req.query[key] && String(req.query[key]).trim()) {
226
- filters[match[1]] = String(req.query[key]).trim();
257
+ if (match) {
258
+ const columnName = match[1];
259
+ parseFilterEntry(columnName, req.query[key]);
227
260
  }
228
261
  });
229
- console.log("Received filters from query:", filters);
230
- console.log("All query params:", req.query);
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 filterColumns = [];
236
- if (Object.keys(filters).length > 0) {
273
+ const columnMetadata = {};
274
+ if (parsedFilters.length > 0) {
237
275
  try {
238
- const columnNames = Object.keys(filters);
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
- filterColumns.push(...validateResult.map((r) => r.COLUMN_NAME));
252
- console.log("Validated filter columns:", filterColumns, "from filters:", filters);
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
- if (filterColumns.length > 0) {
261
- const validFilters = filterColumns.filter((col) => {
262
- const filterValue = filters[col];
263
- return filterValue && filterValue.trim() !== "";
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 (validFilters.length > 0) {
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
- if (fkDisplayMode === "key-display" || fkDisplayMode === "display-only") {
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
- console.log("Fetching foreign keys with query:", fkQuery);
343
- const foreignKeys = await executeQuery(fkQuery, [
344
- { name: "schema", value: schema, type: sql.NVarChar },
345
- { name: "table", value: table, type: sql.NVarChar }
346
- ]);
347
- console.log(`Found ${foreignKeys.length} foreign key(s)`);
348
- if (foreignKeys.length > 0) {
349
- const uniqueRefTables = Array.from(
350
- new Set(foreignKeys.map((fk) => `${fk.referencedSchema}.${fk.referencedTable}`))
351
- );
352
- const tableConditions = uniqueRefTables.map((tableRef, idx) => {
353
- const [refSchema, refTable] = tableRef.split(".");
354
- return `(TABLE_SCHEMA = @refSchema${idx} AND TABLE_NAME = @refTable${idx})`;
355
- }).join(" OR ");
356
- const batchColumnsQuery = `
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
- const batchParams = uniqueRefTables.flatMap((tableRef, idx) => {
363
- const [refSchema, refTable] = tableRef.split(".");
364
- return [
365
- { name: `refSchema${idx}`, value: refSchema, type: sql.NVarChar },
366
- { name: `refTable${idx}`, value: refTable, type: sql.NVarChar }
367
- ];
368
- });
369
- console.log("Fetching referenced table columns with query:", batchColumnsQuery);
370
- console.log("Batch parameters:", batchParams.map((p) => ({ name: p.name, value: p.value })));
371
- const allRefColumns = await executeQuery(batchColumnsQuery, batchParams);
372
- console.log(`Found columns for ${uniqueRefTables.length} referenced table(s)`);
373
- const columnsByTable = {};
374
- allRefColumns.forEach((col) => {
375
- const key = `${col.TABLE_SCHEMA}.${col.TABLE_NAME}`;
376
- if (!columnsByTable[key]) {
377
- columnsByTable[key] = [];
378
- }
379
- columnsByTable[key].push(col);
380
- });
381
- for (const fk of foreignKeys) {
382
- const fkColumn = fk.fkColumnName;
383
- const refSchema = fk.referencedSchema;
384
- const refTable = fk.referencedTable;
385
- const refColumn = fk.referencedColumn;
386
- const tableKey = `${refSchema}.${refTable}`;
387
- const refColumns = columnsByTable[tableKey] || [];
388
- const preferredNames = ["name", "title", "description", "code"];
389
- let displayColumn = null;
390
- for (const preferredName of preferredNames) {
391
- const found = refColumns.find(
392
- (col) => col.COLUMN_NAME.toLowerCase() === preferredName.toLowerCase()
393
- );
394
- if (found) {
395
- displayColumn = found.COLUMN_NAME;
396
- break;
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
- if (!displayColumn) {
400
- const stringTypes = ["varchar", "nvarchar", "char", "nchar", "text", "ntext"];
401
- const found = refColumns.find(
402
- (col) => stringTypes.some((type) => col.DATA_TYPE.toLowerCase().includes(type))
403
- );
404
- if (found) {
405
- displayColumn = found.COLUMN_NAME;
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
- if (displayColumn) {
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
- ${whereClause}
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) : ""}${whereClause ? "\n" + whereClause : ""}
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";