niledatabase
Version:
Command line interface for Nile databases
395 lines (354 loc) • 15.7 kB
text/typescript
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];
}