|
|
@@ -0,0 +1,758 @@
|
|
|
+import express from 'express';
|
|
|
+import { pool } from '../index';
|
|
|
+
|
|
|
+const router = express.Router();
|
|
|
+
|
|
|
+// List all tables in the database (separated by system vs user tables)
|
|
|
+router.get('/tables', async (req: any, res) => {
|
|
|
+ try {
|
|
|
+ // Query to get all tables from the public schema
|
|
|
+ const result = await pool.query(`
|
|
|
+ SELECT
|
|
|
+ table_name,
|
|
|
+ (
|
|
|
+ SELECT COUNT(*)
|
|
|
+ FROM information_schema.columns
|
|
|
+ WHERE table_schema = 'public' AND table_name = t.table_name
|
|
|
+ ) as column_count
|
|
|
+ FROM information_schema.tables t
|
|
|
+ WHERE table_schema = 'public'
|
|
|
+ AND table_type = 'BASE TABLE'
|
|
|
+ ORDER BY table_name
|
|
|
+ `);
|
|
|
+
|
|
|
+ // Get row counts for each table and categorize them
|
|
|
+ const tablesWithCounts = await Promise.all(
|
|
|
+ result.rows.map(async (table) => {
|
|
|
+ try {
|
|
|
+ const countResult = await pool.query(
|
|
|
+ `SELECT COUNT(*) FROM "${table.table_name}"`
|
|
|
+ );
|
|
|
+ return {
|
|
|
+ name: table.table_name,
|
|
|
+ columnCount: parseInt(table.column_count),
|
|
|
+ rowCount: parseInt(countResult.rows[0].count),
|
|
|
+ isSystemTable: table.table_name.startsWith('__sys_'),
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`Error getting count for ${table.table_name}:`, error);
|
|
|
+ return {
|
|
|
+ name: table.table_name,
|
|
|
+ columnCount: parseInt(table.column_count),
|
|
|
+ rowCount: 0,
|
|
|
+ isSystemTable: table.table_name.startsWith('__sys_'),
|
|
|
+ };
|
|
|
+ }
|
|
|
+ })
|
|
|
+ );
|
|
|
+
|
|
|
+ // Separate system and user tables
|
|
|
+ const systemTables = tablesWithCounts.filter(t => t.isSystemTable);
|
|
|
+ const userTables = tablesWithCounts.filter(t => !t.isSystemTable);
|
|
|
+
|
|
|
+ res.json({
|
|
|
+ systemTables,
|
|
|
+ userTables,
|
|
|
+ totalTables: tablesWithCounts.length,
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error listing tables:', error);
|
|
|
+ res.status(500).json({ error: 'Failed to list database tables' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// Get table schema details
|
|
|
+router.get('/tables/:tableName', async (req: any, res) => {
|
|
|
+ try {
|
|
|
+ const { tableName } = req.params;
|
|
|
+
|
|
|
+ // Validate table name to prevent SQL injection
|
|
|
+ const tableCheck = await pool.query(`
|
|
|
+ SELECT table_name
|
|
|
+ FROM information_schema.tables
|
|
|
+ WHERE table_schema = 'public'
|
|
|
+ AND table_type = 'BASE TABLE'
|
|
|
+ AND table_name = $1
|
|
|
+ `, [tableName]);
|
|
|
+
|
|
|
+ if (tableCheck.rows.length === 0) {
|
|
|
+ return res.status(404).json({ error: 'Table not found' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get column information
|
|
|
+ const columnsResult = await pool.query(`
|
|
|
+ SELECT
|
|
|
+ column_name,
|
|
|
+ data_type,
|
|
|
+ character_maximum_length,
|
|
|
+ is_nullable,
|
|
|
+ column_default,
|
|
|
+ (
|
|
|
+ SELECT TRUE
|
|
|
+ FROM information_schema.table_constraints tc
|
|
|
+ JOIN information_schema.key_column_usage kcu
|
|
|
+ ON tc.constraint_name = kcu.constraint_name
|
|
|
+ AND tc.table_schema = kcu.table_schema
|
|
|
+ WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
|
+ AND tc.table_name = $1
|
|
|
+ AND kcu.column_name = c.column_name
|
|
|
+ ) as is_primary_key,
|
|
|
+ (
|
|
|
+ SELECT TRUE
|
|
|
+ FROM information_schema.table_constraints tc
|
|
|
+ JOIN information_schema.key_column_usage kcu
|
|
|
+ ON tc.constraint_name = kcu.constraint_name
|
|
|
+ AND tc.table_schema = kcu.table_schema
|
|
|
+ WHERE tc.constraint_type = 'UNIQUE'
|
|
|
+ AND tc.table_name = $1
|
|
|
+ AND kcu.column_name = c.column_name
|
|
|
+ ) as is_unique
|
|
|
+ FROM information_schema.columns c
|
|
|
+ WHERE table_schema = 'public'
|
|
|
+ AND table_name = $1
|
|
|
+ ORDER BY ordinal_position
|
|
|
+ `, [tableName]);
|
|
|
+
|
|
|
+ // Get foreign key relationships
|
|
|
+ const foreignKeysResult = await pool.query(`
|
|
|
+ SELECT
|
|
|
+ kcu.column_name,
|
|
|
+ ccu.table_name AS referenced_table,
|
|
|
+ ccu.column_name AS referenced_column
|
|
|
+ FROM information_schema.table_constraints AS tc
|
|
|
+ JOIN information_schema.key_column_usage AS kcu
|
|
|
+ ON tc.constraint_name = kcu.constraint_name
|
|
|
+ AND tc.table_schema = kcu.table_schema
|
|
|
+ JOIN information_schema.constraint_column_usage AS ccu
|
|
|
+ ON ccu.constraint_name = tc.constraint_name
|
|
|
+ AND ccu.table_schema = tc.table_schema
|
|
|
+ WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
|
+ AND tc.table_name = $1
|
|
|
+ `, [tableName]);
|
|
|
+
|
|
|
+ // Get row count
|
|
|
+ const countResult = await pool.query(
|
|
|
+ `SELECT COUNT(*) FROM ${tableName}`
|
|
|
+ );
|
|
|
+
|
|
|
+ // Get indexes
|
|
|
+ const indexesResult = await pool.query(`
|
|
|
+ SELECT
|
|
|
+ indexname as index_name,
|
|
|
+ indexdef as definition
|
|
|
+ FROM pg_indexes
|
|
|
+ WHERE schemaname = 'public'
|
|
|
+ AND tablename = $1
|
|
|
+ ORDER BY indexname
|
|
|
+ `, [tableName]);
|
|
|
+
|
|
|
+ res.json({
|
|
|
+ tableName,
|
|
|
+ rowCount: parseInt(countResult.rows[0].count),
|
|
|
+ columns: columnsResult.rows.map(col => ({
|
|
|
+ name: col.column_name,
|
|
|
+ type: col.data_type,
|
|
|
+ maxLength: col.character_maximum_length,
|
|
|
+ nullable: col.is_nullable === 'YES',
|
|
|
+ default: col.column_default,
|
|
|
+ isPrimaryKey: col.is_primary_key || false,
|
|
|
+ isUnique: col.is_unique || false,
|
|
|
+ })),
|
|
|
+ foreignKeys: foreignKeysResult.rows.map(fk => ({
|
|
|
+ column: fk.column_name,
|
|
|
+ referencedTable: fk.referenced_table,
|
|
|
+ referencedColumn: fk.referenced_column,
|
|
|
+ })),
|
|
|
+ indexes: indexesResult.rows,
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error getting table schema:', error);
|
|
|
+ res.status(500).json({ error: 'Failed to get table schema' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// Get table data with pagination
|
|
|
+router.get('/tables/:tableName/data', async (req: any, res) => {
|
|
|
+ try {
|
|
|
+ const { tableName } = req.params;
|
|
|
+ const page = parseInt(req.query.page as string) || 1;
|
|
|
+ const limit = Math.min(parseInt(req.query.limit as string) || 50, 100);
|
|
|
+ const offset = (page - 1) * limit;
|
|
|
+
|
|
|
+ // Validate table name to prevent SQL injection
|
|
|
+ const tableCheck = await pool.query(`
|
|
|
+ SELECT table_name
|
|
|
+ FROM information_schema.tables
|
|
|
+ WHERE table_schema = 'public'
|
|
|
+ AND table_type = 'BASE TABLE'
|
|
|
+ AND table_name = $1
|
|
|
+ `, [tableName]);
|
|
|
+
|
|
|
+ if (tableCheck.rows.length === 0) {
|
|
|
+ return res.status(404).json({ error: 'Table not found' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get total count
|
|
|
+ const countResult = await pool.query(
|
|
|
+ `SELECT COUNT(*) FROM ${tableName}`
|
|
|
+ );
|
|
|
+ const total = parseInt(countResult.rows[0].count);
|
|
|
+
|
|
|
+ // Get data
|
|
|
+ const dataResult = await pool.query(
|
|
|
+ `SELECT * FROM ${tableName} ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
|
|
|
+ [limit, offset]
|
|
|
+ );
|
|
|
+
|
|
|
+ res.json({
|
|
|
+ data: dataResult.rows,
|
|
|
+ pagination: {
|
|
|
+ page,
|
|
|
+ limit,
|
|
|
+ total,
|
|
|
+ totalPages: Math.ceil(total / limit),
|
|
|
+ },
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error getting table data:', error);
|
|
|
+ res.status(500).json({ error: 'Failed to get table data' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// Create a new user table
|
|
|
+router.post('/tables', async (req: any, res) => {
|
|
|
+ try {
|
|
|
+ const { tableName, columns } = req.body;
|
|
|
+ const userId = req.user.userId;
|
|
|
+
|
|
|
+ // Validate input
|
|
|
+ if (!tableName || !columns || columns.length === 0) {
|
|
|
+ return res.status(400).json({ error: 'Table name and columns are required' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Prevent creating system tables
|
|
|
+ if (tableName.startsWith('__sys_')) {
|
|
|
+ return res.status(403).json({ error: 'Cannot create tables with __sys_ prefix' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate table name (only alphanumeric and underscores)
|
|
|
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
|
+ return res.status(400).json({ error: 'Invalid table name. Use only letters, numbers, and underscores.' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if table already exists
|
|
|
+ const existingTable = await pool.query(`
|
|
|
+ SELECT table_name
|
|
|
+ FROM information_schema.tables
|
|
|
+ WHERE table_schema = 'public' AND table_name = $1
|
|
|
+ `, [tableName]);
|
|
|
+
|
|
|
+ if (existingTable.rows.length > 0) {
|
|
|
+ return res.status(400).json({ error: 'Table already exists' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if user defined an id column
|
|
|
+ const hasIdColumn = columns.some((col: any) => col.name.toLowerCase() === 'id');
|
|
|
+
|
|
|
+ // Build column definitions
|
|
|
+ const columnDefs = columns.map((col: any) => {
|
|
|
+ // Validate column name
|
|
|
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(col.name)) {
|
|
|
+ throw new Error(`Invalid column name: ${col.name}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ let def = `"${col.name}" ${col.type.toUpperCase()}`;
|
|
|
+
|
|
|
+ // Add constraints
|
|
|
+ if (col.primaryKey) def += ' PRIMARY KEY';
|
|
|
+ if (col.unique && !col.primaryKey) def += ' UNIQUE';
|
|
|
+ if (col.nullable === false || col.notNull) def += ' NOT NULL';
|
|
|
+ if (col.default !== undefined && col.default !== '') {
|
|
|
+ // Handle default values properly
|
|
|
+ if (col.type.toLowerCase().includes('varchar') || col.type.toLowerCase().includes('text')) {
|
|
|
+ def += ` DEFAULT '${col.default.replace(/'/g, "''")}'`;
|
|
|
+ } else {
|
|
|
+ def += ` DEFAULT ${col.default}`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return def;
|
|
|
+ }).join(',\n ');
|
|
|
+
|
|
|
+ // Add standard columns if not provided
|
|
|
+ let additionalColumns = [];
|
|
|
+ if (!hasIdColumn) {
|
|
|
+ additionalColumns.push('id UUID PRIMARY KEY DEFAULT uuid_generate_v4()');
|
|
|
+ }
|
|
|
+ if (!columns.some((col: any) => col.name.toLowerCase() === 'created_at')) {
|
|
|
+ additionalColumns.push('created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP');
|
|
|
+ }
|
|
|
+ if (!columns.some((col: any) => col.name.toLowerCase() === 'updated_at')) {
|
|
|
+ additionalColumns.push('updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Build final CREATE TABLE statement
|
|
|
+ const allColumns = [...additionalColumns, columnDefs].filter(c => c).join(',\n ');
|
|
|
+
|
|
|
+ const createTableSQL = `CREATE TABLE "${tableName}" (
|
|
|
+ ${allColumns}
|
|
|
+ )`;
|
|
|
+
|
|
|
+ await pool.query(createTableSQL);
|
|
|
+
|
|
|
+ // Log audit (don't fail if audit logging fails)
|
|
|
+ try {
|
|
|
+ await pool.query(`
|
|
|
+ INSERT INTO __sys_audit_logs (user_id, action, resource_type, resource_id, details)
|
|
|
+ VALUES ($1, 'create', 'table', $2, $3)
|
|
|
+ `, [userId, tableName, { tableName, columns }]);
|
|
|
+ } catch (auditError) {
|
|
|
+ console.error('Failed to log table creation audit:', auditError);
|
|
|
+ // Continue - don't fail the request if audit log fails
|
|
|
+ }
|
|
|
+
|
|
|
+ res.status(201).json({ message: 'Table created successfully', tableName });
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('Error creating table:', error);
|
|
|
+
|
|
|
+ // Provide more specific error messages
|
|
|
+ if (error.code === '42P07') {
|
|
|
+ return res.status(400).json({ error: 'Table already exists' });
|
|
|
+ }
|
|
|
+ if (error.code === '42601') {
|
|
|
+ return res.status(400).json({ error: 'Invalid table syntax' });
|
|
|
+ }
|
|
|
+
|
|
|
+ res.status(500).json({
|
|
|
+ error: error.message || 'Failed to create table',
|
|
|
+ details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
|
|
+ });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// Update a row in a table
|
|
|
+router.put('/tables/:tableName/rows/:rowId', async (req: any, res) => {
|
|
|
+ try {
|
|
|
+ const { tableName, rowId } = req.params;
|
|
|
+ const updates = req.body;
|
|
|
+ const userId = req.user.userId;
|
|
|
+
|
|
|
+ // Prevent modifying system tables
|
|
|
+ if (tableName.startsWith('__sys_')) {
|
|
|
+ return res.status(403).json({ error: 'Cannot modify system tables' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate table exists
|
|
|
+ const tableCheck = await pool.query(`
|
|
|
+ SELECT table_name FROM information_schema.tables
|
|
|
+ WHERE table_schema = 'public' AND table_name = $1
|
|
|
+ `, [tableName]);
|
|
|
+
|
|
|
+ if (tableCheck.rows.length === 0) {
|
|
|
+ return res.status(404).json({ error: 'Table not found' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Build UPDATE query
|
|
|
+ const setClauses = Object.keys(updates)
|
|
|
+ .filter(key => key !== 'id')
|
|
|
+ .map((key, idx) => `"${key}" = $${idx + 2}`)
|
|
|
+ .join(', ');
|
|
|
+
|
|
|
+ const values = [rowId, ...Object.keys(updates).filter(k => k !== 'id').map(k => updates[k])];
|
|
|
+
|
|
|
+ if (setClauses.length === 0) {
|
|
|
+ return res.status(400).json({ error: 'No fields to update' });
|
|
|
+ }
|
|
|
+
|
|
|
+ const updateSQL = `
|
|
|
+ UPDATE "${tableName}"
|
|
|
+ SET ${setClauses}, updated_at = CURRENT_TIMESTAMP
|
|
|
+ WHERE id = $1
|
|
|
+ RETURNING *
|
|
|
+ `;
|
|
|
+
|
|
|
+ const result = await pool.query(updateSQL, values);
|
|
|
+
|
|
|
+ if (result.rows.length === 0) {
|
|
|
+ return res.status(404).json({ error: 'Row not found' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Log audit
|
|
|
+ await pool.query(`
|
|
|
+ INSERT INTO __sys_audit_logs (user_id, action, resource_type, resource_id, details)
|
|
|
+ VALUES ($1, 'update', 'table_row', $2, $3)
|
|
|
+ `, [userId, `${tableName}:${rowId}`, { tableName, rowId, updates }]);
|
|
|
+
|
|
|
+ res.json(result.rows[0]);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error updating row:', error);
|
|
|
+ res.status(500).json({ error: 'Failed to update row' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// Delete a row from a table
|
|
|
+router.delete('/tables/:tableName/rows/:rowId', async (req: any, res) => {
|
|
|
+ try {
|
|
|
+ const { tableName, rowId } = req.params;
|
|
|
+ const userId = req.user.userId;
|
|
|
+
|
|
|
+ // Prevent modifying system tables
|
|
|
+ if (tableName.startsWith('__sys_')) {
|
|
|
+ return res.status(403).json({ error: 'Cannot modify system tables' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate table exists
|
|
|
+ const tableCheck = await pool.query(`
|
|
|
+ SELECT table_name FROM information_schema.tables
|
|
|
+ WHERE table_schema = 'public' AND table_name = $1
|
|
|
+ `, [tableName]);
|
|
|
+
|
|
|
+ if (tableCheck.rows.length === 0) {
|
|
|
+ return res.status(404).json({ error: 'Table not found' });
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await pool.query(
|
|
|
+ `DELETE FROM "${tableName}" WHERE id = $1 RETURNING id`,
|
|
|
+ [rowId]
|
|
|
+ );
|
|
|
+
|
|
|
+ if (result.rows.length === 0) {
|
|
|
+ return res.status(404).json({ error: 'Row not found' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Log audit
|
|
|
+ await pool.query(`
|
|
|
+ INSERT INTO __sys_audit_logs (user_id, action, resource_type, resource_id, details)
|
|
|
+ VALUES ($1, 'delete', 'table_row', $2, $3)
|
|
|
+ `, [userId, `${tableName}:${rowId}`, { tableName, rowId }]);
|
|
|
+
|
|
|
+ res.json({ message: 'Row deleted successfully' });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error deleting row:', error);
|
|
|
+ res.status(500).json({ error: 'Failed to delete row' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// Execute SQL query (with safety restrictions)
|
|
|
+router.post('/query', async (req: any, res) => {
|
|
|
+ try {
|
|
|
+ const { query } = req.body;
|
|
|
+ const userId = req.user.userId;
|
|
|
+
|
|
|
+ if (!query) {
|
|
|
+ return res.status(400).json({ error: 'Query is required' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Safety checks
|
|
|
+ const lowerQuery = query.toLowerCase().trim();
|
|
|
+
|
|
|
+ // Block dangerous operations
|
|
|
+ const dangerousOperations = ['drop', 'truncate', 'alter', 'create user', 'grant', 'revoke'];
|
|
|
+ for (const op of dangerousOperations) {
|
|
|
+ if (lowerQuery.includes(op)) {
|
|
|
+ return res.status(403).json({ error: `Operation '${op}' is not allowed` });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Block modifying system tables
|
|
|
+ if (lowerQuery.includes('__sys_')) {
|
|
|
+ return res.status(403).json({ error: 'Cannot query system tables directly' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Execute query with timeout
|
|
|
+ const result = await pool.query(query);
|
|
|
+
|
|
|
+ // Log audit
|
|
|
+ await pool.query(`
|
|
|
+ INSERT INTO __sys_audit_logs (user_id, action, resource_type, resource_id, details)
|
|
|
+ VALUES ($1, 'query', 'database', NULL, $2)
|
|
|
+ `, [userId, { query: query.substring(0, 500) }]);
|
|
|
+
|
|
|
+ res.json({
|
|
|
+ rows: result.rows,
|
|
|
+ rowCount: result.rowCount,
|
|
|
+ command: result.command,
|
|
|
+ });
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('Error executing query:', error);
|
|
|
+ res.status(400).json({ error: error.message || 'Query execution failed' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// Export table data as CSV
|
|
|
+router.get('/tables/:tableName/export', async (req: any, res) => {
|
|
|
+ try {
|
|
|
+ const { tableName } = req.params;
|
|
|
+ const format = req.query.format || 'json';
|
|
|
+
|
|
|
+ // Validate table exists
|
|
|
+ const tableCheck = await pool.query(`
|
|
|
+ SELECT table_name FROM information_schema.tables
|
|
|
+ WHERE table_schema = 'public' AND table_name = $1
|
|
|
+ `, [tableName]);
|
|
|
+
|
|
|
+ if (tableCheck.rows.length === 0) {
|
|
|
+ return res.status(404).json({ error: 'Table not found' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get all data
|
|
|
+ const result = await pool.query(`SELECT * FROM "${tableName}"`);
|
|
|
+
|
|
|
+ if (format === 'csv') {
|
|
|
+ // Convert to CSV
|
|
|
+ if (result.rows.length === 0) {
|
|
|
+ return res.send('');
|
|
|
+ }
|
|
|
+
|
|
|
+ const headers = Object.keys(result.rows[0]);
|
|
|
+ const csv = [
|
|
|
+ headers.join(','),
|
|
|
+ ...result.rows.map(row =>
|
|
|
+ headers.map(h => {
|
|
|
+ const val = row[h];
|
|
|
+ if (val === null) return '';
|
|
|
+ if (typeof val === 'string' && (val.includes(',') || val.includes('"'))) {
|
|
|
+ return `"${val.replace(/"/g, '""')}"`;
|
|
|
+ }
|
|
|
+ return val;
|
|
|
+ }).join(',')
|
|
|
+ )
|
|
|
+ ].join('\n');
|
|
|
+
|
|
|
+ res.setHeader('Content-Type', 'text/csv');
|
|
|
+ res.setHeader('Content-Disposition', `attachment; filename="${tableName}.csv"`);
|
|
|
+ res.send(csv);
|
|
|
+ } else {
|
|
|
+ // Return as JSON
|
|
|
+ res.setHeader('Content-Type', 'application/json');
|
|
|
+ res.setHeader('Content-Disposition', `attachment; filename="${tableName}.json"`);
|
|
|
+ res.json(result.rows);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error exporting table:', error);
|
|
|
+ res.status(500).json({ error: 'Failed to export table' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// Add a column to a table
|
|
|
+router.post('/tables/:tableName/columns', async (req: any, res) => {
|
|
|
+ try {
|
|
|
+ const { tableName } = req.params;
|
|
|
+ const { columnName, dataType, nullable, defaultValue, unique } = req.body;
|
|
|
+ const userId = req.user.userId;
|
|
|
+
|
|
|
+ // Prevent modifying system tables
|
|
|
+ if (tableName.startsWith('__sys_')) {
|
|
|
+ return res.status(403).json({ error: 'Cannot modify system tables' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate inputs
|
|
|
+ if (!columnName || !dataType) {
|
|
|
+ return res.status(400).json({ error: 'Column name and data type are required' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate column name
|
|
|
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(columnName)) {
|
|
|
+ return res.status(400).json({ error: 'Invalid column name' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate table exists
|
|
|
+ const tableCheck = await pool.query(`
|
|
|
+ SELECT table_name FROM information_schema.tables
|
|
|
+ WHERE table_schema = 'public' AND table_name = $1
|
|
|
+ `, [tableName]);
|
|
|
+
|
|
|
+ if (tableCheck.rows.length === 0) {
|
|
|
+ return res.status(404).json({ error: 'Table not found' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if column already exists
|
|
|
+ const columnCheck = await pool.query(`
|
|
|
+ SELECT column_name FROM information_schema.columns
|
|
|
+ WHERE table_schema = 'public' AND table_name = $1 AND column_name = $2
|
|
|
+ `, [tableName, columnName]);
|
|
|
+
|
|
|
+ if (columnCheck.rows.length > 0) {
|
|
|
+ return res.status(400).json({ error: 'Column already exists' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Build ALTER TABLE statement
|
|
|
+ let alterSQL = `ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" ${dataType.toUpperCase()}`;
|
|
|
+
|
|
|
+ if (!nullable) {
|
|
|
+ alterSQL += ' NOT NULL';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (defaultValue !== undefined && defaultValue !== '') {
|
|
|
+ if (dataType.toLowerCase().includes('varchar') || dataType.toLowerCase().includes('text')) {
|
|
|
+ alterSQL += ` DEFAULT '${defaultValue.replace(/'/g, "''")}'`;
|
|
|
+ } else {
|
|
|
+ alterSQL += ` DEFAULT ${defaultValue}`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (unique) {
|
|
|
+ alterSQL += ' UNIQUE';
|
|
|
+ }
|
|
|
+
|
|
|
+ await pool.query(alterSQL);
|
|
|
+
|
|
|
+ // Log audit
|
|
|
+ try {
|
|
|
+ await pool.query(`
|
|
|
+ INSERT INTO __sys_audit_logs (user_id, action, resource_type, resource_id, details)
|
|
|
+ VALUES ($1, 'add_column', 'table', $2, $3)
|
|
|
+ `, [userId, tableName, { tableName, columnName, dataType }]);
|
|
|
+ } catch (auditError) {
|
|
|
+ console.error('Failed to log add column audit:', auditError);
|
|
|
+ }
|
|
|
+
|
|
|
+ res.status(201).json({ message: 'Column added successfully', columnName });
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('Error adding column:', error);
|
|
|
+ res.status(500).json({ error: error.message || 'Failed to add column' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// Drop a column from a table
|
|
|
+router.delete('/tables/:tableName/columns/:columnName', async (req: any, res) => {
|
|
|
+ try {
|
|
|
+ const { tableName, columnName } = req.params;
|
|
|
+ const userId = req.user.userId;
|
|
|
+
|
|
|
+ // Prevent modifying system tables
|
|
|
+ if (tableName.startsWith('__sys_')) {
|
|
|
+ return res.status(403).json({ error: 'Cannot modify system tables' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Prevent dropping critical columns
|
|
|
+ const protectedColumns = ['id', 'created_at', 'updated_at'];
|
|
|
+ if (protectedColumns.includes(columnName.toLowerCase())) {
|
|
|
+ return res.status(403).json({ error: `Cannot drop protected column: ${columnName}` });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate table exists
|
|
|
+ const tableCheck = await pool.query(`
|
|
|
+ SELECT table_name FROM information_schema.tables
|
|
|
+ WHERE table_schema = 'public' AND table_name = $1
|
|
|
+ `, [tableName]);
|
|
|
+
|
|
|
+ if (tableCheck.rows.length === 0) {
|
|
|
+ return res.status(404).json({ error: 'Table not found' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if column exists
|
|
|
+ const columnCheck = await pool.query(`
|
|
|
+ SELECT column_name FROM information_schema.columns
|
|
|
+ WHERE table_schema = 'public' AND table_name = $1 AND column_name = $2
|
|
|
+ `, [tableName, columnName]);
|
|
|
+
|
|
|
+ if (columnCheck.rows.length === 0) {
|
|
|
+ return res.status(404).json({ error: 'Column not found' });
|
|
|
+ }
|
|
|
+
|
|
|
+ await pool.query(`ALTER TABLE "${tableName}" DROP COLUMN "${columnName}"`);
|
|
|
+
|
|
|
+ // Log audit
|
|
|
+ try {
|
|
|
+ await pool.query(`
|
|
|
+ INSERT INTO __sys_audit_logs (user_id, action, resource_type, resource_id, details)
|
|
|
+ VALUES ($1, 'drop_column', 'table', $2, $3)
|
|
|
+ `, [userId, tableName, { tableName, columnName }]);
|
|
|
+ } catch (auditError) {
|
|
|
+ console.error('Failed to log drop column audit:', auditError);
|
|
|
+ }
|
|
|
+
|
|
|
+ res.json({ message: 'Column dropped successfully' });
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('Error dropping column:', error);
|
|
|
+ res.status(500).json({ error: error.message || 'Failed to drop column' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// Rename a table
|
|
|
+router.patch('/tables/:tableName/rename', async (req: any, res) => {
|
|
|
+ try {
|
|
|
+ const { tableName } = req.params;
|
|
|
+ const { newTableName } = req.body;
|
|
|
+ const userId = req.user.userId;
|
|
|
+
|
|
|
+ // Prevent modifying system tables
|
|
|
+ if (tableName.startsWith('__sys_') || newTableName.startsWith('__sys_')) {
|
|
|
+ return res.status(403).json({ error: 'Cannot rename system tables' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate new table name
|
|
|
+ if (!newTableName || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(newTableName)) {
|
|
|
+ return res.status(400).json({ error: 'Invalid table name' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if new table name already exists
|
|
|
+ const existingTable = await pool.query(`
|
|
|
+ SELECT table_name FROM information_schema.tables
|
|
|
+ WHERE table_schema = 'public' AND table_name = $1
|
|
|
+ `, [newTableName]);
|
|
|
+
|
|
|
+ if (existingTable.rows.length > 0) {
|
|
|
+ return res.status(400).json({ error: 'Table with that name already exists' });
|
|
|
+ }
|
|
|
+
|
|
|
+ await pool.query(`ALTER TABLE "${tableName}" RENAME TO "${newTableName}"`);
|
|
|
+
|
|
|
+ // Log audit
|
|
|
+ try {
|
|
|
+ await pool.query(`
|
|
|
+ INSERT INTO __sys_audit_logs (user_id, action, resource_type, resource_id, details)
|
|
|
+ VALUES ($1, 'rename_table', 'table', $2, $3)
|
|
|
+ `, [userId, newTableName, { oldTableName: tableName, newTableName }]);
|
|
|
+ } catch (auditError) {
|
|
|
+ console.error('Failed to log rename table audit:', auditError);
|
|
|
+ }
|
|
|
+
|
|
|
+ res.json({ message: 'Table renamed successfully', newTableName });
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('Error renaming table:', error);
|
|
|
+ res.status(500).json({ error: error.message || 'Failed to rename table' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// Drop a table
|
|
|
+router.delete('/tables/:tableName', async (req: any, res) => {
|
|
|
+ try {
|
|
|
+ const { tableName } = req.params;
|
|
|
+ const userId = req.user.userId;
|
|
|
+
|
|
|
+ // Prevent dropping system tables
|
|
|
+ if (tableName.startsWith('__sys_')) {
|
|
|
+ return res.status(403).json({ error: 'Cannot drop system tables' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate table exists
|
|
|
+ const tableCheck = await pool.query(`
|
|
|
+ SELECT table_name FROM information_schema.tables
|
|
|
+ WHERE table_schema = 'public' AND table_name = $1
|
|
|
+ `, [tableName]);
|
|
|
+
|
|
|
+ if (tableCheck.rows.length === 0) {
|
|
|
+ return res.status(404).json({ error: 'Table not found' });
|
|
|
+ }
|
|
|
+
|
|
|
+ await pool.query(`DROP TABLE "${tableName}"`);
|
|
|
+
|
|
|
+ // Log audit
|
|
|
+ try {
|
|
|
+ await pool.query(`
|
|
|
+ INSERT INTO __sys_audit_logs (user_id, action, resource_type, resource_id, details)
|
|
|
+ VALUES ($1, 'drop_table', 'table', $2, $3)
|
|
|
+ `, [userId, tableName, { tableName }]);
|
|
|
+ } catch (auditError) {
|
|
|
+ console.error('Failed to log drop table audit:', auditError);
|
|
|
+ }
|
|
|
+
|
|
|
+ res.json({ message: 'Table dropped successfully' });
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('Error dropping table:', error);
|
|
|
+ res.status(500).json({ error: error.message || 'Failed to drop table' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+export default router;
|