UNPKG

niledatabase

Version:

Command line interface for Nile databases

671 lines (669 loc) 30.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createDbCommand = createDbCommand; const commander_1 = require("commander"); const config_1 = require("../lib/config"); const api_1 = require("../lib/api"); const colors_1 = require("../lib/colors"); const globalOptions_1 = require("../lib/globalOptions"); const child_process_1 = require("child_process"); const cli_table3_1 = __importDefault(require("cli-table3")); const errorHandling_1 = require("../lib/errorHandling"); const axios_1 = __importDefault(require("axios")); const fs_1 = __importDefault(require("fs")); const pg_1 = require("pg"); const pg_format_1 = __importDefault(require("pg-format")); const cli_progress_1 = require("cli-progress"); function createDbCommand(getOptions) { const db = new commander_1.Command('db') .description('Manage databases') .addHelpText('after', ` Examples: ${(0, colors_1.formatCommand)('nile db list')} List all databases ${(0, colors_1.formatCommand)('nile db show mydb')} Show database details ${(0, colors_1.formatCommand)('nile db create', '--name mydb --region AWS_US_WEST_2')} Create a database ${(0, colors_1.formatCommand)('nile db delete mydb')} Delete a database ${(0, colors_1.formatCommand)('nile config --db <n>')} Set default database ${(0, globalOptions_1.getGlobalOptionsHelp)()}`); db .command('list') .description('List all databases') .action(async () => { try { const options = getOptions(); const configManager = new config_1.ConfigManager(options); let token = configManager.getToken(); if (!token) { await (0, errorHandling_1.forceRelogin)(configManager); token = configManager.getToken(); if (!token) { throw new Error('Failed to get token after re-login'); } } const api = new api_1.NileAPI({ token, dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug }); const workspaceSlug = configManager.getWorkspace(); if (!workspaceSlug) { throw new Error('No workspace specified. Use one of:\n' + '1. --workspace flag\n' + '2. nile config --workspace <n>\n' + '3. NILE_WORKSPACE environment variable'); } const databases = await api.listDatabases(workspaceSlug); if (options.format === 'json') { console.log(JSON.stringify(databases, null, 2)); return; } if (options.format === 'csv') { console.log('NAME,REGION,STATUS'); databases.forEach((db) => { console.log(`${db.name},${db.region},${db.status}`); }); return; } if (databases.length === 0) { console.log(colors_1.theme.warning(`\nNo databases found in workspace '${colors_1.theme.bold(configManager.getWorkspace())}'`)); console.log(colors_1.theme.secondary('Run "nile db create" to create a database')); return; } // Create a nicely formatted table using cli-table3 console.log(colors_1.theme.primary(`\nDatabases in workspace '${colors_1.theme.bold(configManager.getWorkspace())}':`)); const table = new cli_table3_1.default({ head: [ colors_1.theme.header('NAME'), colors_1.theme.header('REGION'), colors_1.theme.header('STATUS') ], style: { head: [], // Disable default styling border: [], // Disable default styling }, chars: { 'top': '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐', 'bottom': '─', 'bottom-mid': '┴', 'bottom-left': '└', 'bottom-right': '┘', 'left': '│', 'left-mid': '├', 'mid': '─', 'mid-mid': '┼', 'right': '│', 'right-mid': '┤', 'middle': '│' } }); // Add rows to the table databases.forEach((db) => { table.push([ colors_1.theme.primary(db.name), colors_1.theme.info(db.region), formatStatus(db.status) ]); }); // Print the table console.log(table.toString()); } catch (error) { if (axios_1.default.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { await (0, errorHandling_1.handleDatabaseError)(error, 'list databases', new config_1.ConfigManager(getOptions())); } else { throw error; } } }); db .command('show <name>') .description('Show database details') .action(async (name) => { try { const options = getOptions(); const configManager = new config_1.ConfigManager(options); let token = configManager.getToken(); if (!token) { await (0, errorHandling_1.forceRelogin)(configManager); token = configManager.getToken(); if (!token) { throw new Error('Failed to get token after re-login'); } } const api = new api_1.NileAPI({ token, dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug }); const workspaceSlug = configManager.getWorkspace(); if (!workspaceSlug) { throw new Error('No workspace specified. Use one of:\n' + '1. --workspace flag\n' + '2. nile config --workspace <n>\n' + '3. NILE_WORKSPACE environment variable'); } const database = await api.getDatabase(workspaceSlug, name); if (options.format === 'json') { console.log(JSON.stringify(database, null, 2)); return; } if (options.format === 'csv') { console.log('NAME,REGION,STATUS'); console.log(`${database.name},${database.region},${database.status}`); return; } const detailsTable = new cli_table3_1.default({ style: { head: [], border: [], }, chars: { 'top': '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐', 'bottom': '─', 'bottom-mid': '┴', 'bottom-left': '└', 'bottom-right': '┘', 'left': '│', 'left-mid': '├', 'mid': '─', 'mid-mid': '┼', 'right': '│', 'right-mid': '┤', 'middle': '│' } }); detailsTable.push([colors_1.theme.secondary('Name:'), colors_1.theme.primary(database.name)], [colors_1.theme.secondary('Region:'), colors_1.theme.info(database.region)], [colors_1.theme.secondary('Status:'), formatStatus(database.status)]); console.log(detailsTable.toString()); } catch (error) { if (axios_1.default.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { await (0, errorHandling_1.handleDatabaseError)(error, 'get database details', new config_1.ConfigManager(getOptions())); } else { throw error; } } }); db .command('create') .description('Create a new database') .requiredOption('--name <name>', 'Database name') .requiredOption('--region <region>', 'Database region') .action(async (options) => { try { const globalOptions = getOptions(); const configManager = new config_1.ConfigManager(globalOptions); let token = configManager.getToken(); if (!token) { await (0, errorHandling_1.forceRelogin)(configManager); token = configManager.getToken(); if (!token) { throw new Error('Failed to get token after re-login'); } } const api = new api_1.NileAPI({ token, dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: globalOptions.debug }); const workspaceSlug = configManager.getWorkspace(); if (!workspaceSlug) { throw new Error('No workspace specified. Use one of:\n' + '1. --workspace flag\n' + '2. nile config --workspace <n>\n' + '3. NILE_WORKSPACE environment variable'); } const database = await api.createDatabase(workspaceSlug, options.name, options.region); if (globalOptions.format === 'json') { console.log(JSON.stringify(database, null, 2)); return; } if (globalOptions.format === 'csv') { console.log('NAME,REGION,STATUS'); console.log(`${database.name},${database.region},${database.status}`); return; } console.log(`${colors_1.theme.secondary('Name:')} ${colors_1.theme.primary(database.name)}`); console.log(`${colors_1.theme.secondary('Region:')} ${colors_1.theme.info(database.region)}`); console.log(`${colors_1.theme.secondary('Status:')} ${formatStatus(database.status)}`); } catch (error) { if (axios_1.default.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { await (0, errorHandling_1.handleDatabaseError)(error, 'create database', new config_1.ConfigManager(getOptions())); } else { throw error; } } }); db .command('delete <name>') .description('Delete a database') .action(async (name) => { try { const options = getOptions(); const configManager = new config_1.ConfigManager(options); let token = configManager.getToken(); if (!token) { await (0, errorHandling_1.forceRelogin)(configManager); token = configManager.getToken(); if (!token) { throw new Error('Failed to get token after re-login'); } } const api = new api_1.NileAPI({ token, dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug }); const workspaceSlug = configManager.getWorkspace(); if (!workspaceSlug) { throw new Error('No workspace specified. Use one of:\n' + '1. --workspace flag\n' + '2. nile config --workspace <n>\n' + '3. NILE_WORKSPACE environment variable'); } await api.deleteDatabase(workspaceSlug, name); console.log(colors_1.theme.success('Database deleted successfully.')); } catch (error) { if (axios_1.default.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { await (0, errorHandling_1.handleDatabaseError)(error, 'delete database', new config_1.ConfigManager(getOptions())); } else { throw error; } } }); db .command('regions') .description('List available regions') .action(async () => { try { const options = getOptions(); const configManager = new config_1.ConfigManager(options); let token = configManager.getToken(); if (!token) { await (0, errorHandling_1.forceRelogin)(configManager); token = configManager.getToken(); if (!token) { throw new Error('Failed to get token after re-login'); } } const api = new api_1.NileAPI({ token, dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug }); const workspaceSlug = configManager.getWorkspace(); if (!workspaceSlug) { throw new Error('No workspace specified. Use one of:\n' + '1. --workspace flag\n' + '2. nile config --workspace <n>\n' + '3. NILE_WORKSPACE environment variable'); } const regions = await api.listRegions(workspaceSlug); if (options.format === 'json') { console.log(JSON.stringify(regions, null, 2)); return; } if (options.format === 'csv') { console.log('NAME'); regions.forEach(r => { console.log(r); }); return; } console.log('\nAvailable regions:'); const regionsTable = new cli_table3_1.default({ head: [ colors_1.theme.header('NAME') ], style: { head: [], border: [] }, chars: { 'top': '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐', 'bottom': '─', 'bottom-mid': '┴', 'bottom-left': '└', 'bottom-right': '┘', 'left': '│', 'left-mid': '├', 'mid': '─', 'mid-mid': '┼', 'right': '│', 'right-mid': '┤', 'middle': '│' } }); regions.forEach(r => { regionsTable.push([ colors_1.theme.primary(r) ]); }); console.log(regionsTable.toString()); } catch (error) { if (axios_1.default.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { await (0, errorHandling_1.handleDatabaseError)(error, 'list regions', new config_1.ConfigManager(getOptions())); } else { throw error; } } }); db .command('psql') .description('Connect to database using psql') .option('--name <n>', 'Database name (overrides selected database)') .action(async (cmdOptions) => { try { const options = getOptions(); const configManager = new config_1.ConfigManager(options); const workspaceSlug = configManager.getWorkspace(); if (!workspaceSlug) { throw new Error('No workspace specified. Use one of:\n' + '1. --workspace flag\n' + '2. nile config --workspace <name>\n' + '3. NILE_WORKSPACE environment variable'); } // Get database name from command option or config let databaseName = cmdOptions.name; if (!databaseName) { databaseName = configManager.getDatabase(); if (!databaseName) { throw new Error('No database specified. Use one of:\n' + '1. --db flag\n' + '2. nile config --db <name>\n' + '3. NILE_DB environment variable'); } } const api = new api_1.NileAPI({ token: configManager.getToken(), dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug }); // Get database connection if (options.debug) { console.log('Fetching database credentials...'); } const connection = await api.getDatabaseConnection(workspaceSlug, databaseName); // Construct psql connection string const connectionString = `postgres://${connection.user}:${connection.password}@${connection.host}:${connection.port}/${connection.database}`; // Execute psql command const psql = (0, child_process_1.spawn)('psql', [connectionString], { stdio: 'inherit' }); psql.on('error', (error) => { const options = getOptions(); if (error.code === 'ENOENT') { console.error(colors_1.theme.error('\nError: psql command not found. Please install PostgreSQL client tools.')); process.exit(1); } else { if (options.debug) { console.error(colors_1.theme.error('\nError executing psql:'), error); } else { console.error(colors_1.theme.error('\nError executing psql:'), error.message || 'Unknown error'); } process.exit(1); } }); psql.on('exit', (code) => { if (code !== 0) { console.error(colors_1.theme.error('\npsql exited with code:'), code); process.exit(code); } }); } catch (error) { if (axios_1.default.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { await (0, errorHandling_1.handleDatabaseError)(error, 'connect to database', new config_1.ConfigManager(getOptions())); } else { throw error; } } }); db .command('connectionstring') .description('Get database connection string') .option('--name <n>', 'Database name (overrides selected database)') .requiredOption('--psql', 'Get PostgreSQL connection string') .action(async (cmdOptions) => { try { const options = getOptions(); const configManager = new config_1.ConfigManager(options); const workspaceSlug = configManager.getWorkspace(); if (!workspaceSlug) { throw new Error('No workspace specified. Use one of:\n' + '1. --workspace flag\n' + '2. nile config --workspace <name>\n' + '3. NILE_WORKSPACE environment variable'); } // Get database name from command option or config let databaseName = cmdOptions.name; if (!databaseName) { databaseName = configManager.getDatabase(); if (!databaseName) { throw new Error('No database specified. Use one of:\n' + '1. --db flag\n' + '2. nile config --db <name>\n' + '3. NILE_DB environment variable'); } } const api = new api_1.NileAPI({ token: configManager.getToken(), dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug }); // Get database connection details const connection = await api.getDatabaseConnection(workspaceSlug, databaseName); // Construct psql connection string const connectionString = `postgres://${connection.user}:${connection.password}@${connection.host}:${connection.port}/${connection.database}`; // Output the connection string console.log(connectionString); } catch (error) { if (axios_1.default.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { await (0, errorHandling_1.handleDatabaseError)(error, 'get connection string', new config_1.ConfigManager(getOptions())); } else { throw error; } } }); db .command('copy') .description('Copy data from a file to a Nile PostgreSQL table') .requiredOption('--table-name <n>', 'Target table name') .option('--column-list <columns>', 'Comma-separated list of column names') .requiredOption('--file-name <file>', 'Input file path') .option('--format [type]', 'File format (csv or text)', 'csv') .option('--delimiter <char>', 'Column delimiter character') .action(async (cmdOptions) => { try { const options = getOptions(); const configManager = new config_1.ConfigManager(options); const workspaceSlug = configManager.getWorkspace(); if (!workspaceSlug) { throw new Error('No workspace specified. Use one of:\n' + '1. --workspace flag\n' + '2. nile config --workspace <n>\n' + '3. NILE_WORKSPACE environment variable'); } // Get database name from config const databaseName = configManager.getDatabase(); if (!databaseName) { throw new Error('No database specified. Use one of:\n' + '1. --db flag\n' + '2. nile config --db <n>\n' + '3. NILE_DB environment variable'); } // Validate format if (cmdOptions.format !== 'csv' && cmdOptions.format !== 'text') { throw new Error('Format must be either "csv" or "text"'); } // Validate file exists if (!fs_1.default.existsSync(cmdOptions.fileName)) { throw new Error(`File not found: ${cmdOptions.fileName}`); } const api = new api_1.NileAPI({ token: configManager.getToken(), dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug }); // Get database connection if (options.debug) { console.log('Fetching database credentials...'); } const connection = await api.getDatabaseConnection(workspaceSlug, databaseName); // Create postgres client const client = new pg_1.Client({ host: connection.host, port: connection.port || 5432, database: connection.database, user: connection.user, password: connection.password, ssl: { rejectUnauthorized: false } }); await client.connect(); try { // Check if tenant_id column exists in the table const tableQuery = await client.query(` SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND column_name = 'tenant_id' `, [cmdOptions.tableName]); const hasTenantId = tableQuery.rows.length > 0; // Read the file content const fileContent = fs_1.default.readFileSync(cmdOptions.fileName, 'utf8'); const lines = fileContent.split('\n').filter(line => line.trim()); // Parse header to get column positions const delimiter = cmdOptions.delimiter || (cmdOptions.format === 'csv' ? ',' : '\t'); const header = lines[0].split(delimiter).map(col => col.trim().replace(/\r$/, '')); if (options.debug) { console.log('Debug - Header:', header); console.log('Debug - tenant_id index:', header.indexOf('tenant_id')); } const tenantIdIndex = header.indexOf('tenant_id'); if (hasTenantId && tenantIdIndex === -1) { throw new Error('Table has tenant_id column but input file does not contain tenant_id'); } // Get column list let columns = header; if (cmdOptions.columnList) { columns = cmdOptions.columnList.split(',').map((col) => col.trim()); } // Skip header row and process data rows const dataRows = lines.slice(1); if (options.debug) { console.log('Debug - Number of data rows:', dataRows.length); } const startTime = Date.now(); let totalRowsInserted = 0; // Create progress bar const progressBar = new cli_progress_1.SingleBar({ format: 'Copying data |' + colors_1.theme.primary('{bar}') + '| {percentage}% || {value}/{total} Rows || Speed: {speed} rows/sec', barCompleteChar: '█', barIncompleteChar: '░', hideCursor: true, clearOnComplete: false, fps: 5 }, cli_progress_1.Presets.shades_classic); // Start the progress bar progressBar.start(dataRows.length, 0, { speed: "N/A" }); // Process in batches of 10,000 rows for (let i = 0; i < dataRows.length; i += 10000) { const batch = dataRows.slice(i, i + 10000); if (options.debug) { console.log(`Debug - Processing batch of ${batch.length} rows starting at index ${i}`); } await client.query('BEGIN'); try { // Format values for batch insert const values = batch.map(row => row.split(delimiter).map(v => v.trim())); if (options.debug) { console.log('Debug - First row in batch:', values[0]); } // Use pg-format to safely format the values const batchQuery = (0, pg_format_1.default)('INSERT INTO %I (%s) VALUES %L', cmdOptions.tableName, columns.join(', '), values); await client.query(batchQuery); await client.query('COMMIT'); totalRowsInserted += batch.length; // Update progress bar const currentTime = Date.now(); const elapsedSeconds = (currentTime - startTime) / 1000; const speed = Math.round(totalRowsInserted / elapsedSeconds); progressBar.update(totalRowsInserted, { speed: speed.toLocaleString() }); if (options.debug) { console.log(colors_1.theme.success(`Inserted ${batch.length} rows`)); } } catch (error) { await client.query('ROLLBACK'); progressBar.stop(); throw error; } } // Stop the progress bar progressBar.stop(); const endTime = Date.now(); const totalTimeSeconds = ((endTime - startTime) / 1000).toFixed(2); const rowsPerSecond = Math.round(totalRowsInserted / (endTime - startTime) * 1000); console.log(colors_1.theme.success('\nCopy operation completed successfully:')); console.log(colors_1.theme.info(`Total rows inserted: ${colors_1.theme.bold(totalRowsInserted.toLocaleString())}`)); console.log(colors_1.theme.info(`Total time: ${colors_1.theme.bold(totalTimeSeconds)} seconds`)); console.log(colors_1.theme.info(`Average speed: ${colors_1.theme.bold(rowsPerSecond.toLocaleString())} rows/second`)); } finally { await client.end(); } } catch (error) { console.error(colors_1.theme.error('Failed to copy data:'), error); process.exit(1); } }); return db; } function formatStatus(status) { switch (status.toLowerCase()) { case 'running': return colors_1.theme.success(status); case 'creating': return colors_1.theme.warning(status); case 'error': return colors_1.theme.error(status); default: return colors_1.theme.info(status); } }