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