u
This commit is contained in:
357
src/backendmonitor/dbHelper.js
Normal file
357
src/backendmonitor/dbHelper.js
Normal file
@@ -0,0 +1,357 @@
|
||||
import env from '../config/env.js';
|
||||
import { getProvider } from '../db/store.js';
|
||||
|
||||
/**
|
||||
* Returns dialect ('sqlite' or 'mssql')
|
||||
*/
|
||||
export function getDialect(provider) {
|
||||
if (provider?.dialect === 'sqlite3') return 'sqlite';
|
||||
if (provider?.dialect === 'mssql') return 'mssql';
|
||||
return env.dbProvider === 'sqlite' || env.dbProvider === 'better-sqlite3' ? 'sqlite' : 'mssql';
|
||||
}
|
||||
|
||||
function sanitizeValue(val) {
|
||||
if (val === null || val === undefined) return null;
|
||||
if (Buffer.isBuffer(val) || val instanceof Uint8Array) {
|
||||
return `<Buffer ${val.length} bytes>`;
|
||||
}
|
||||
if (typeof val === 'bigint') {
|
||||
return val.toString();
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
export function sanitizeRow(row) {
|
||||
if (!row || typeof row !== 'object') return row;
|
||||
const sanitized = {};
|
||||
for (const [key, val] of Object.entries(row)) {
|
||||
sanitized[key] = sanitizeValue(val);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
export async function getDatabaseInfo() {
|
||||
const provider = await getProvider();
|
||||
const dialect = getDialect(provider);
|
||||
return {
|
||||
dialect,
|
||||
database: dialect === 'sqlite' ? env.sqlite.filename : env.mssql.database,
|
||||
server: dialect === 'sqlite' ? 'local' : (env.mssql.server || 'localhost'),
|
||||
port: env.monitorPort,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of tables in the database with their row counts.
|
||||
*/
|
||||
export async function getTables() {
|
||||
const provider = await getProvider();
|
||||
const dialect = getDialect(provider);
|
||||
|
||||
let tablesQuery = '';
|
||||
if (dialect === 'sqlite') {
|
||||
tablesQuery = `SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name ASC;`;
|
||||
} else {
|
||||
tablesQuery = `SELECT TABLE_NAME as name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME NOT LIKE 'sys%' AND TABLE_NAME != 'sysdiagrams' ORDER BY TABLE_NAME ASC;`;
|
||||
}
|
||||
|
||||
const result = await provider.query(tablesQuery);
|
||||
const tableList = (result.rows || []).map((r) => ({
|
||||
name: r.name,
|
||||
rowCount: 0,
|
||||
}));
|
||||
|
||||
// Fetch row counts concurrently
|
||||
await Promise.all(
|
||||
tableList.map(async (t) => {
|
||||
try {
|
||||
const countRes = await provider.query(`SELECT COUNT(*) as cnt FROM [${t.name}]`);
|
||||
t.rowCount = Number(countRes.rows[0]?.cnt ?? 0);
|
||||
} catch {
|
||||
t.rowCount = 0;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return tableList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns column schema for a given table.
|
||||
*/
|
||||
export async function getTableSchema(tableName) {
|
||||
const provider = await getProvider();
|
||||
const dialect = getDialect(provider);
|
||||
|
||||
if (dialect === 'sqlite') {
|
||||
const res = await provider.query(`PRAGMA table_info("${tableName}")`);
|
||||
return (res.rows || []).map((row) => ({
|
||||
name: row.name,
|
||||
type: (row.type || 'TEXT').toUpperCase(),
|
||||
isNullable: row.notnull === 0,
|
||||
defaultValue: row.dflt_value,
|
||||
isPrimaryKey: Boolean(row.pk),
|
||||
pkOrder: row.pk || 0,
|
||||
}));
|
||||
} else {
|
||||
const sql = `
|
||||
SELECT
|
||||
c.COLUMN_NAME as name,
|
||||
c.DATA_TYPE as type,
|
||||
CASE WHEN c.IS_NULLABLE = 'YES' THEN 1 ELSE 0 END as isNullable,
|
||||
c.COLUMN_DEFAULT as defaultValue,
|
||||
CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END as isPrimaryKey
|
||||
FROM INFORMATION_SCHEMA.COLUMNS c
|
||||
LEFT JOIN (
|
||||
SELECT ku.TABLE_NAME, ku.COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
||||
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE ku
|
||||
ON tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.CONSTRAINT_NAME = ku.CONSTRAINT_NAME
|
||||
) pk ON c.TABLE_NAME = pk.TABLE_NAME AND c.COLUMN_NAME = pk.COLUMN_NAME
|
||||
WHERE c.TABLE_NAME = @tableName
|
||||
ORDER BY c.ORDINAL_POSITION;
|
||||
`;
|
||||
const res = await provider.query(sql, { tableName });
|
||||
return (res.rows || []).map((row) => ({
|
||||
name: row.name,
|
||||
type: (row.type || 'VARCHAR').toUpperCase(),
|
||||
isNullable: Boolean(row.isNullable),
|
||||
defaultValue: row.defaultValue,
|
||||
isPrimaryKey: Boolean(row.isPrimaryKey),
|
||||
pkOrder: row.isPrimaryKey ? 1 : 0,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Browse table data with pagination, sorting, and search.
|
||||
*/
|
||||
export async function getTableData(tableName, { page = 1, pageSize = 50, sortBy, sortDir = 'ASC', search = '' } = {}) {
|
||||
const provider = await getProvider();
|
||||
const dialect = getDialect(provider);
|
||||
|
||||
// Verify table exists
|
||||
const tables = await getTables();
|
||||
const tableExists = tables.some((t) => t.name.toLowerCase() === tableName.toLowerCase());
|
||||
if (!tableExists) {
|
||||
throw new Error(`Table "${tableName}" not found`);
|
||||
}
|
||||
|
||||
const schema = await getTableSchema(tableName);
|
||||
if (!schema.length) {
|
||||
throw new Error(`No columns found for table "${tableName}"`);
|
||||
}
|
||||
|
||||
const columnNames = schema.map((c) => c.name);
|
||||
const primaryKeys = schema.filter((c) => c.isPrimaryKey).map((c) => c.name);
|
||||
|
||||
const safePage = Math.max(1, parseInt(page, 10) || 1);
|
||||
const safePageSize = Math.min(500, Math.max(1, parseInt(pageSize, 10) || 50));
|
||||
const offset = (safePage - 1) * safePageSize;
|
||||
|
||||
// Validate sorting column
|
||||
let safeSortCol = sortBy && columnNames.includes(sortBy) ? sortBy : null;
|
||||
if (!safeSortCol) {
|
||||
safeSortCol = primaryKeys.length > 0 ? primaryKeys[0] : columnNames[0];
|
||||
}
|
||||
const safeSortDir = String(sortDir).toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
||||
|
||||
// Search filter
|
||||
const trimmedSearch = (search || '').trim();
|
||||
let whereClause = '';
|
||||
const queryParams = {};
|
||||
|
||||
if (trimmedSearch) {
|
||||
// Build search across text/varchar/char columns
|
||||
const searchableCols = schema.filter((c) => {
|
||||
const t = c.type.toLowerCase();
|
||||
return t.includes('char') || t.includes('text') || t.includes('string');
|
||||
});
|
||||
const targetCols = searchableCols.length > 0 ? searchableCols : schema;
|
||||
const searchConditions = targetCols.map((c, i) => {
|
||||
const paramKey = `search${i}`;
|
||||
queryParams[paramKey] = `%${trimmedSearch}%`;
|
||||
return `CAST([${c.name}] AS NVARCHAR(MAX)) LIKE @${paramKey}`;
|
||||
});
|
||||
whereClause = `WHERE (${searchConditions.join(' OR ')})`;
|
||||
}
|
||||
|
||||
// Count query
|
||||
const countSql = `SELECT COUNT(*) as total FROM [${tableName}] ${whereClause}`;
|
||||
const countRes = await provider.query(countSql, queryParams);
|
||||
const total = Number(countRes.rows[0]?.total ?? 0);
|
||||
|
||||
// Data query
|
||||
let dataSql = '';
|
||||
if (dialect === 'sqlite') {
|
||||
dataSql = `SELECT * FROM [${tableName}] ${whereClause} ORDER BY [${safeSortCol}] ${safeSortDir} LIMIT ${safePageSize} OFFSET ${offset};`;
|
||||
} else {
|
||||
dataSql = `SELECT * FROM [${tableName}] ${whereClause} ORDER BY [${safeSortCol}] ${safeSortDir} OFFSET ${offset} ROWS FETCH NEXT ${safePageSize} ROWS ONLY;`;
|
||||
}
|
||||
|
||||
const dataRes = await provider.query(dataSql, queryParams);
|
||||
const rows = (dataRes.rows || []).map((row) => sanitizeRow(row));
|
||||
|
||||
return {
|
||||
tableName,
|
||||
columns: schema,
|
||||
rows,
|
||||
total,
|
||||
page: safePage,
|
||||
pageSize: safePageSize,
|
||||
totalPages: Math.ceil(total / safePageSize) || 1,
|
||||
primaryKeys,
|
||||
sortBy: safeSortCol,
|
||||
sortDir: safeSortDir,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new row into the table.
|
||||
*/
|
||||
export async function insertRow(tableName, rowData) {
|
||||
const provider = await getProvider();
|
||||
const schema = await getTableSchema(tableName);
|
||||
const validColNames = new Set(schema.map((c) => c.name));
|
||||
|
||||
// Filter to valid columns only and strip undefined / empty auto-increment PKs
|
||||
const cleanData = {};
|
||||
for (const [col, val] of Object.entries(rowData || {})) {
|
||||
if (!validColNames.has(col)) continue;
|
||||
const colDef = schema.find((c) => c.name === col);
|
||||
if (colDef?.isPrimaryKey && (val === '' || val === null || val === undefined)) {
|
||||
// omit auto-increment primary key so provider assigns it
|
||||
continue;
|
||||
}
|
||||
cleanData[col] = val === '' ? (colDef?.isNullable ? null : '') : val;
|
||||
}
|
||||
|
||||
const cols = Object.keys(cleanData);
|
||||
if (!cols.length) {
|
||||
throw new Error('No valid column data provided for insert');
|
||||
}
|
||||
|
||||
const placeholders = cols.map((_, i) => `@p${i}`).join(', ');
|
||||
const params = {};
|
||||
cols.forEach((col, i) => {
|
||||
params[`p${i}`] = cleanData[col];
|
||||
});
|
||||
|
||||
const sql = `INSERT INTO [${tableName}] (${cols.map((c) => `[${c}]`).join(', ')}) VALUES (${placeholders});`;
|
||||
const result = await provider.query(sql, params);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
insertId: result.insertId,
|
||||
rowsAffected: result.rowsAffected,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing row.
|
||||
*/
|
||||
export async function updateRow(tableName, { primaryKey, changes } = {}) {
|
||||
const provider = await getProvider();
|
||||
const schema = await getTableSchema(tableName);
|
||||
const validColNames = new Set(schema.map((c) => c.name));
|
||||
|
||||
if (!primaryKey || typeof primaryKey !== 'object' || !Object.keys(primaryKey).length) {
|
||||
throw new Error('Primary key specification required for updating a row');
|
||||
}
|
||||
if (!changes || typeof changes !== 'object' || !Object.keys(changes).length) {
|
||||
throw new Error('No changes specified');
|
||||
}
|
||||
|
||||
// Build SET clause
|
||||
const setCols = Object.keys(changes).filter((c) => validColNames.has(c));
|
||||
if (!setCols.length) {
|
||||
throw new Error('No valid columns in changes');
|
||||
}
|
||||
|
||||
const params = {};
|
||||
const setParts = setCols.map((c, i) => {
|
||||
params[`s${i}`] = changes[c];
|
||||
return `[${c}] = @s${i}`;
|
||||
});
|
||||
|
||||
// Build WHERE clause
|
||||
const pkCols = Object.keys(primaryKey).filter((c) => validColNames.has(c));
|
||||
const whereParts = pkCols.map((c, i) => {
|
||||
params[`pk${i}`] = primaryKey[c];
|
||||
return `[${c}] = @pk${i}`;
|
||||
});
|
||||
|
||||
const sql = `UPDATE [${tableName}] SET ${setParts.join(', ')} WHERE ${whereParts.join(' AND ')};`;
|
||||
const result = await provider.query(sql, params);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
rowsAffected: result.rowsAffected,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a row by primary key.
|
||||
*/
|
||||
export async function deleteRow(tableName, { primaryKey } = {}) {
|
||||
const provider = await getProvider();
|
||||
const schema = await getTableSchema(tableName);
|
||||
const validColNames = new Set(schema.map((c) => c.name));
|
||||
|
||||
if (!primaryKey || typeof primaryKey !== 'object' || !Object.keys(primaryKey).length) {
|
||||
throw new Error('Primary key specification required for deleting a row');
|
||||
}
|
||||
|
||||
const pkCols = Object.keys(primaryKey).filter((c) => validColNames.has(c));
|
||||
if (!pkCols.length) {
|
||||
throw new Error('No valid primary key columns provided');
|
||||
}
|
||||
|
||||
const params = {};
|
||||
const whereParts = pkCols.map((c, i) => {
|
||||
params[`pk${i}`] = primaryKey[c];
|
||||
return `[${c}] = @pk${i}`;
|
||||
});
|
||||
|
||||
const sql = `DELETE FROM [${tableName}] WHERE ${whereParts.join(' AND ')};`;
|
||||
const result = await provider.query(sql, params);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
rowsAffected: result.rowsAffected,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute raw arbitrary SQL query.
|
||||
*/
|
||||
export async function executeRawQuery(sql) {
|
||||
const provider = await getProvider();
|
||||
const trimmed = (sql || '').trim();
|
||||
if (!trimmed) {
|
||||
throw new Error('Query cannot be empty');
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
try {
|
||||
const result = await provider.query(trimmed, {});
|
||||
const executionTimeMs = Math.round((performance.now() - start) * 100) / 100;
|
||||
const rows = (result.rows || []).map((row) => sanitizeRow(row));
|
||||
const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
|
||||
return {
|
||||
success: true,
|
||||
rows,
|
||||
columns,
|
||||
rowsAffected: result.rowsAffected ?? 0,
|
||||
insertId: result.insertId ?? null,
|
||||
executionTimeMs,
|
||||
};
|
||||
} catch (err) {
|
||||
const executionTimeMs = Math.round((performance.now() - start) * 100) / 100;
|
||||
return {
|
||||
success: false,
|
||||
error: err.message,
|
||||
executionTimeMs,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user