UNPKG

niledatabase

Version:

Command line interface for Nile databases

395 lines (354 loc) 15.7 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 { handleTenantError, 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 TenantsCommand { constructor(program: Command, getGlobalOptions: () => GlobalOptions) { const tenants = program .command('tenants') .description('Manage tenants in your database') .addHelpText('after', ` Examples: # List tenants ${formatCommand('nile tenants list')} List all tenants ${formatCommand('nile tenants list', '--workspace myworkspace')} List tenants in specific workspace ${formatCommand('nile tenants list', '--db mydb')} List tenants in specific database ${formatCommand('nile tenants list', '--format json')} Output in JSON format ${formatCommand('nile tenants list', '--format csv')} Output in CSV format # Create tenants ${formatCommand('nile tenants create', '--name "My Tenant"')} Create tenant with auto-generated ID ${formatCommand('nile tenants create', '--name "My Tenant" --id custom-id')} Create tenant with custom ID ${formatCommand('nile tenants create', '--name "Customer A"')} Create tenant for a customer ${formatCommand('nile tenants create', '--name "Organization B" --id org-b')} Create tenant with organization ID # Update tenants ${formatCommand('nile tenants update', '--id tenant-123 --new_name "New Name"')} Update tenant name ${formatCommand('nile tenants update', '--id org-b --new_name "Organization B Ltd"')} Update organization name ${formatCommand('nile tenants update', '--id custom-id --new_name "Updated Name"')} Update custom tenant name # Delete tenants ${formatCommand('nile tenants delete', '--id tenant-123')} Delete tenant by ID ${formatCommand('nile tenants delete', '--id org-b')} Delete organization tenant ${formatCommand('nile tenants delete', '--id 550e8400-e29b-41d4-a716-446655440000')} Delete tenant by UUID # Using with different output formats ${formatCommand('nile tenants list', '--format json')} List tenants in JSON format ${formatCommand('nile tenants list', '--format csv')} List tenants in CSV format ${formatCommand('nile tenants create', '--name "New Tenant" --format json')} Create tenant with JSON output ${getGlobalOptionsHelp()}`); const listCmd = new Command('list') .description('List all tenants in the database') .addHelpText('after', ` Examples: ${formatCommand('nile tenants list')} List all tenants ${formatCommand('nile tenants list', '--workspace myworkspace')} List tenants in specific workspace ${formatCommand('nile tenants list', '--db mydb')} List tenants 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 tenants...')); const result = await client.query('SELECT * FROM tenants'); const tenants = result.rows; if (tenants.length === 0) { console.log('No tenants found'); return; } console.log('\nTenants:'); const tenantsTable = new Table({ head: [ theme.header('ID'), 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': '│' } }); tenants.forEach(tenant => { tenantsTable.push([ theme.primary(tenant.id), theme.info(tenant.name || '(unnamed)') ]); }); console.log(tenantsTable.toString()); } catch (error: any) { if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { await handleTenantError(error, 'list tenants', new ConfigManager(getGlobalOptions())); } else { throw error; } } finally { if (client) { await client.end(); } } }); const createCmd = new Command('create') .description('Create a new tenant') .requiredOption('--name <name>', 'Name of the tenant') .option('--id <id>', 'Optional UUID for the tenant') .addHelpText('after', ` Examples: ${formatCommand('nile tenants create', '--name "My Tenant"')} Create a tenant with auto-generated ID ${formatCommand('nile tenants create', '--name "My Tenant" --id custom-id')} Create tenant with custom ID `) .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 tenant...with name: ${cmdOptions.name} and id: ${cmdOptions.id}`)); let result; if (cmdOptions.id) { result = await client.query( 'INSERT INTO tenants (id, name) VALUES ($1, $2) RETURNING *', [cmdOptions.id, cmdOptions.name] ); } else { result = await client.query( 'INSERT INTO tenants (name) VALUES ($1) RETURNING *', [cmdOptions.name] ); } const tenant = result.rows[0]; console.log('\nTenant 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(tenant.id)], [theme.secondary('Name:'), theme.info(tenant.name)] ); console.log(detailsTable.toString()); } catch (error: any) { if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { await handleTenantError(error, 'create tenant', new ConfigManager(getGlobalOptions())); } else { throw error; } } finally { if (client) { await client.end(); } } }); const deleteCmd = new Command('delete') .description('Delete a tenant') .requiredOption('--id <id>', 'ID of the tenant to delete') .addHelpText('after', ` Examples: ${formatCommand('nile tenants delete', '--id tenant-123')} Delete a tenant by ID ${formatCommand('nile tenants delete', '--id 550e8400-e29b-41d4-a716-446655440000')} Delete tenant 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 tenant exists client = await getPostgresClient(api, workspaceSlug, databaseName, options); const checkResult = await client.query('SELECT id, name FROM tenants WHERE id = $1', [cmdOptions.id]); if (checkResult.rowCount === 0) { throw new Error(`Tenant with ID '${cmdOptions.id}' not found`); } const tenant = checkResult.rows[0]; console.log(theme.dim(`\nDeleting tenant '${tenant.name}' (${tenant.id})...`)); // Delete the tenant await client.query('DELETE FROM tenants WHERE id = $1', [cmdOptions.id]); console.log(theme.success(`\nTenant '${theme.bold(tenant.name)}' deleted successfully.`)); } catch (error: any) { if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { await handleTenantError(error, 'delete tenant', new ConfigManager(getGlobalOptions())); } else { throw error; } } finally { if (client) { await client.end(); } } }); const updateCmd = new Command('update') .description('Update a tenant') .requiredOption('--id <id>', 'ID of the tenant to update') .requiredOption('--new_name <name>', 'New name for the tenant') .addHelpText('after', ` Examples: ${formatCommand('nile tenants update', '--id tenant-123 --new_name "New Name"')} Update tenant name ${formatCommand('nile tenants update', '--id 550e8400-e29b-41d4-a716-446655440000 --new_name "New Name"')} Update tenant with UUID `) .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 tenant...')); client = await getPostgresClient(api, workspaceSlug, databaseName, options); const result = await client.query( 'UPDATE tenants SET name = $1 WHERE id = $2 RETURNING *', [cmdOptions.new_name, cmdOptions.id] ); if (result.rowCount === 0) { throw new Error(`Tenant with ID '${cmdOptions.id}' not found`); } const tenant = result.rows[0]; console.log(theme.success(`\nTenant '${theme.bold(tenant.id)}' updated successfully.`)); console.log(theme.primary('\nUpdated tenant details:')); console.log(`${theme.secondary('ID:')} ${theme.primary(tenant.id)}`); console.log(`${theme.secondary('Name:')} ${theme.info(tenant.name)}`); } catch (error: any) { if (axios.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) { await handleTenantError(error, 'update tenant', new ConfigManager(getGlobalOptions())); } else { throw error; } } finally { if (client) { await client.end(); } } }); tenants.addCommand(listCmd); tenants.addCommand(createCmd); tenants.addCommand(deleteCmd); tenants.addCommand(updateCmd); } } export function createTenantsCommand(getGlobalOptions: () => GlobalOptions): Command { const program = new Command(); new TenantsCommand(program, getGlobalOptions); return program.commands[0]; }