UNPKG

niledatabase

Version:

Command line interface for Nile databases

411 lines (364 loc) 16.1 kB
import { Command } from 'commander'; import { Client } from 'pg'; import { ConfigManager } from '../lib/config'; import { NileAPI } from '../lib/api'; import { theme, formatCommand } from '../lib/colors'; import { GlobalOptions, getGlobalOptionsHelp } from '../lib/globalOptions'; import { handleUserError, forceRelogin } from '../lib/errorHandling'; import Table from 'cli-table3'; import axios from 'axios'; async function getWorkspaceAndDatabase(options: GlobalOptions): Promise<{ workspaceSlug: string; databaseName: string }> { const configManager = new 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'); } 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'); } return { workspaceSlug, databaseName }; } async function getPostgresClient(api: NileAPI, workspaceSlug: string, databaseName: string, options: GlobalOptions): Promise<Client> { // Get database credentials from control plane console.log(theme.dim('\nFetching database credentials...')); const credentials = await api.createDatabaseCredentials(workspaceSlug, databaseName); if (!credentials.id || !credentials.password) { throw new Error('Invalid credentials received from server'); } // Create postgres connection URL const region = credentials.database.region.toLowerCase(); const regionParts = region.split('_'); const regionPrefix = `${regionParts[1]}-${regionParts[2]}-${regionParts[3]}`; // e.g., us-west-2 // Use custom host if provided, otherwise use default with region prefix const dbHost = options.dbHost ? `${regionPrefix}.${options.dbHost}` : `${regionPrefix}.db.thenile.dev`; // Create postgres client const client = new Client({ host: dbHost, port: 5432, database: databaseName, user: credentials.id, password: credentials.password, ssl: { rejectUnauthorized: false } }); if (options.debug) { console.log(theme.dim('\nConnecting to PostgreSQL:')); console.log(theme.dim('Host:'), dbHost); console.log(theme.dim('Database:'), databaseName); console.log(theme.dim('User:'), credentials.id); console.log(); } // Connect to the database await client.connect(); return client; } export class UsersCommand { constructor(program: Command, getGlobalOptions: () => GlobalOptions) { const users = program .command('users') .description('Manage users in your database') .addHelpText('after', ` Examples: # List users ${formatCommand('nile users list')} List all users ${formatCommand('nile users list', '--workspace myworkspace')} List users in specific workspace ${formatCommand('nile users list', '--db mydb')} List users in specific database ${formatCommand('nile users list', '--format json')} Output in JSON format ${formatCommand('nile users list', '--format csv')} Output in CSV format # Create users ${formatCommand('nile users create', '--email "user@example.com" --password "password123"')} Create user with email and password ${formatCommand('nile users create', '--email "user@example.com" --password "password123" --tenant tenant-123')} Create user in specific tenant # Update users ${formatCommand('nile users update', '--id user-123 --new_email "new@example.com"')} Update user email ${formatCommand('nile users update', '--id user-123 --new_password "newpassword123"')} Update user password # Delete users ${formatCommand('nile users delete', '--id user-123')} Delete user by ID ${formatCommand('nile users delete', '--id 550e8400-e29b-41d4-a716-446655440000')} Delete user by UUID # Using with different output formats ${formatCommand('nile users list', '--format json')} List users in JSON format ${formatCommand('nile users list', '--format csv')} List users in CSV format ${formatCommand('nile users create', '--email "user@example.com" --password "password123" --format json')} Create user with JSON output ${getGlobalOptionsHelp()}`); const listCmd = new Command('list') .description('List all users in the database') .addHelpText('after', ` Examples: ${formatCommand('nile users list')} List all users ${formatCommand('nile users list', '--workspace myworkspace')} List users in specific workspace ${formatCommand('nile users list', '--db mydb')} List users in specific database `) .action(async () => { let client: Client | undefined; try { const options = getGlobalOptions(); const configManager = new ConfigManager(options); let token = configManager.getToken(); if (!token) { await forceRelogin(configManager); token = configManager.getToken(); if (!token) { throw new Error('Failed to get token after re-login'); } } const api = new NileAPI({ token, dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), debug: options.debug }); const { workspaceSlug, databaseName } = await getWorkspaceAndDatabase(options); client = await getPostgresClient(api, workspaceSlug, databaseName, options); console.log(theme.dim('\nFetching users...')); const result = await client.query('SELECT * FROM users'); const users = result.rows; if (users.length === 0) { console.log('No users found'); return; } console.log('\nUsers:'); const usersTable = new Table({ head: [ theme.header('ID'), theme.header('EMAIL'), theme.header('TENANT ID') ], 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': '│' } }); users.forEach(user => { usersTable.push([ theme.primary(user.id), theme.info(user.email), theme.secondary(user.tenant_id || '(none)') ]); }); console.log(usersTable.toString()); } catch (error: any) { if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { await handleUserError(error, 'list users', new ConfigManager(getGlobalOptions())); } else { throw error; } } finally { if (client) { await client.end(); } } }); const createCmd = new Command('create') .description('Create a new user') .requiredOption('--email <email>', 'Email address of the user') .requiredOption('--password <password>', 'Password for the user') .option('--tenant <tenant>', 'Tenant ID to associate with the user') .addHelpText('after', ` Examples: ${formatCommand('nile users create', '--email "user@example.com" --password "password123"')} Create a user with email and password ${formatCommand('nile users create', '--email "user@example.com" --password "password123" --tenant tenant-123')} Create user in specific tenant `) .action(async (cmdOptions) => { let client: Client | undefined; try { const options = getGlobalOptions(); const configManager = new ConfigManager(options); const api = new NileAPI({ token: configManager.getToken(), dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), }); const { workspaceSlug, databaseName } = await getWorkspaceAndDatabase(options); client = await getPostgresClient(api, workspaceSlug, databaseName, options); console.log(theme.dim(`\nCreating user with email: ${cmdOptions.email}`)); const result = await client.query( 'INSERT INTO users (email, password, tenant_id) VALUES ($1, $2, $3) RETURNING *', [cmdOptions.email, cmdOptions.password, cmdOptions.tenant] ); const user = result.rows[0]; console.log('\nUser created:'); const detailsTable = new Table({ 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( [theme.secondary('ID:'), theme.primary(user.id)], [theme.secondary('Email:'), theme.info(user.email)], [theme.secondary('Tenant ID:'), theme.secondary(user.tenant_id || '(none)')] ); console.log(detailsTable.toString()); } catch (error: any) { if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { await handleUserError(error, 'create user', new ConfigManager(getGlobalOptions())); } else { throw error; } } finally { if (client) { await client.end(); } } }); const deleteCmd = new Command('delete') .description('Delete a user') .requiredOption('--id <id>', 'ID of the user to delete') .addHelpText('after', ` Examples: ${formatCommand('nile users delete', '--id user-123')} Delete a user by ID ${formatCommand('nile users delete', '--id 550e8400-e29b-41d4-a716-446655440000')} Delete user by UUID `) .action(async (cmdOptions) => { let client: Client | undefined; try { const options = getGlobalOptions(); const configManager = new ConfigManager(options); const token = await configManager.getToken(); const api = new NileAPI({ token, dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), }); const { workspaceSlug, databaseName } = await getWorkspaceAndDatabase(options); // First verify the user exists client = await getPostgresClient(api, workspaceSlug, databaseName, options); const checkResult = await client.query('SELECT id, email FROM users WHERE id = $1', [cmdOptions.id]); if (checkResult.rowCount === 0) { throw new Error(`User with ID '${cmdOptions.id}' not found`); } const user = checkResult.rows[0]; console.log(theme.dim(`\nDeleting user '${user.email}' (${user.id})...`)); // Delete the user await client.query('DELETE FROM users WHERE id = $1', [cmdOptions.id]); console.log(theme.success(`\nUser '${theme.bold(user.email)}' deleted successfully.`)); } catch (error: any) { if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { await handleUserError(error, 'delete user', new ConfigManager(getGlobalOptions())); } else { throw error; } } finally { if (client) { await client.end(); } } }); const updateCmd = new Command('update') .description('Update a user') .requiredOption('--id <id>', 'ID of the user to update') .option('--new_email <email>', 'New email address for the user') .option('--new_password <password>', 'New password for the user') .addHelpText('after', ` Examples: ${formatCommand('nile users update', '--id user-123 --new_email "new@example.com"')} Update user email ${formatCommand('nile users update', '--id user-123 --new_password "newpassword123"')} Update user password `) .action(async (cmdOptions) => { let client: Client | undefined; try { const options = getGlobalOptions(); const configManager = new ConfigManager(options); const api = new NileAPI({ token: configManager.getToken(), dbHost: configManager.getDbHost(), controlPlaneUrl: configManager.getGlobalHost(), }); const { workspaceSlug, databaseName } = await getWorkspaceAndDatabase(options); console.log(theme.dim('\nUpdating user...')); client = await getPostgresClient(api, workspaceSlug, databaseName, options); // Build update query based on provided options const updates: string[] = []; const values: any[] = []; let paramCount = 1; if (cmdOptions.new_email) { updates.push(`email = $${paramCount}`); values.push(cmdOptions.new_email); paramCount++; } if (cmdOptions.new_password) { updates.push(`password = $${paramCount}`); values.push(cmdOptions.new_password); paramCount++; } if (updates.length === 0) { throw new Error('No update fields provided. Use --new_email or --new_password'); } values.push(cmdOptions.id); const query = `UPDATE users SET ${updates.join(', ')} WHERE id = $${paramCount} RETURNING *`; const result = await client.query(query, values); if (result.rowCount === 0) { throw new Error(`User with ID '${cmdOptions.id}' not found`); } const user = result.rows[0]; console.log(theme.success(`\nUser '${theme.bold(user.id)}' updated successfully.`)); console.log(theme.primary('\nUpdated user details:')); console.log(`${theme.secondary('ID:')} ${theme.primary(user.id)}`); console.log(`${theme.secondary('Email:')} ${theme.info(user.email)}`); console.log(`${theme.secondary('Tenant ID:')} ${theme.secondary(user.tenant_id || '(none)')}`); } catch (error: any) { if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { await handleUserError(error, 'update user', new ConfigManager(getGlobalOptions())); } else { throw error; } } finally { if (client) { await client.end(); } } }); users.addCommand(listCmd); users.addCommand(createCmd); users.addCommand(deleteCmd); users.addCommand(updateCmd); } } export function createUsersCommand(getGlobalOptions: () => GlobalOptions): Command { const program = new Command(); new UsersCommand(program, getGlobalOptions); return program.commands[0]; }