niledatabase
Version:
Command line interface for Nile databases
369 lines (363 loc) • 18.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TenantsCommand = void 0;
exports.createTenantsCommand = createTenantsCommand;
const commander_1 = require("commander");
const pg_1 = require("pg");
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 errorHandling_1 = require("../lib/errorHandling");
const cli_table3_1 = __importDefault(require("cli-table3"));
const axios_1 = __importDefault(require("axios"));
async function getWorkspaceAndDatabase(options) {
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');
}
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, workspaceSlug, databaseName, options) {
// Get database credentials from control plane
console.log(colors_1.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 pg_1.Client({
host: dbHost,
port: 5432,
database: databaseName,
user: credentials.id,
password: credentials.password,
ssl: {
rejectUnauthorized: false
}
});
if (options.debug) {
console.log(colors_1.theme.dim('\nConnecting to PostgreSQL:'));
console.log(colors_1.theme.dim('Host:'), dbHost);
console.log(colors_1.theme.dim('Database:'), databaseName);
console.log(colors_1.theme.dim('User:'), credentials.id);
console.log();
}
// Connect to the database
await client.connect();
return client;
}
class TenantsCommand {
constructor(program, getGlobalOptions) {
const tenants = program
.command('tenants')
.description('Manage tenants in your database')
.addHelpText('after', `
Examples:
# List tenants
${(0, colors_1.formatCommand)('nile tenants list')} List all tenants
${(0, colors_1.formatCommand)('nile tenants list', '--workspace myworkspace')} List tenants in specific workspace
${(0, colors_1.formatCommand)('nile tenants list', '--db mydb')} List tenants in specific database
${(0, colors_1.formatCommand)('nile tenants list', '--format json')} Output in JSON format
${(0, colors_1.formatCommand)('nile tenants list', '--format csv')} Output in CSV format
# Create tenants
${(0, colors_1.formatCommand)('nile tenants create', '--name "My Tenant"')} Create tenant with auto-generated ID
${(0, colors_1.formatCommand)('nile tenants create', '--name "My Tenant" --id custom-id')} Create tenant with custom ID
${(0, colors_1.formatCommand)('nile tenants create', '--name "Customer A"')} Create tenant for a customer
${(0, colors_1.formatCommand)('nile tenants create', '--name "Organization B" --id org-b')} Create tenant with organization ID
# Update tenants
${(0, colors_1.formatCommand)('nile tenants update', '--id tenant-123 --new_name "New Name"')} Update tenant name
${(0, colors_1.formatCommand)('nile tenants update', '--id org-b --new_name "Organization B Ltd"')} Update organization name
${(0, colors_1.formatCommand)('nile tenants update', '--id custom-id --new_name "Updated Name"')} Update custom tenant name
# Delete tenants
${(0, colors_1.formatCommand)('nile tenants delete', '--id tenant-123')} Delete tenant by ID
${(0, colors_1.formatCommand)('nile tenants delete', '--id org-b')} Delete organization tenant
${(0, colors_1.formatCommand)('nile tenants delete', '--id 550e8400-e29b-41d4-a716-446655440000')} Delete tenant by UUID
# Using with different output formats
${(0, colors_1.formatCommand)('nile tenants list', '--format json')} List tenants in JSON format
${(0, colors_1.formatCommand)('nile tenants list', '--format csv')} List tenants in CSV format
${(0, colors_1.formatCommand)('nile tenants create', '--name "New Tenant" --format json')} Create tenant with JSON output
${(0, globalOptions_1.getGlobalOptionsHelp)()}`);
const listCmd = new commander_1.Command('list')
.description('List all tenants in the database')
.addHelpText('after', `
Examples:
${(0, colors_1.formatCommand)('nile tenants list')} List all tenants
${(0, colors_1.formatCommand)('nile tenants list', '--workspace myworkspace')} List tenants in specific workspace
${(0, colors_1.formatCommand)('nile tenants list', '--db mydb')} List tenants in specific database
`)
.action(async () => {
let client;
try {
const options = getGlobalOptions();
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, databaseName } = await getWorkspaceAndDatabase(options);
client = await getPostgresClient(api, workspaceSlug, databaseName, options);
console.log(colors_1.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 cli_table3_1.default({
head: [
colors_1.theme.header('ID'),
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': '│'
}
});
tenants.forEach(tenant => {
tenantsTable.push([
colors_1.theme.primary(tenant.id),
colors_1.theme.info(tenant.name || '(unnamed)')
]);
});
console.log(tenantsTable.toString());
}
catch (error) {
if (axios_1.default.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) {
await (0, errorHandling_1.handleTenantError)(error, 'list tenants', new config_1.ConfigManager(getGlobalOptions()));
}
else {
throw error;
}
}
finally {
if (client) {
await client.end();
}
}
});
const createCmd = new commander_1.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:
${(0, colors_1.formatCommand)('nile tenants create', '--name "My Tenant"')} Create a tenant with auto-generated ID
${(0, colors_1.formatCommand)('nile tenants create', '--name "My Tenant" --id custom-id')} Create tenant with custom ID
`)
.action(async (cmdOptions) => {
let client;
try {
const options = getGlobalOptions();
const configManager = new config_1.ConfigManager(options);
const api = new api_1.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(colors_1.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 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('ID:'), colors_1.theme.primary(tenant.id)], [colors_1.theme.secondary('Name:'), colors_1.theme.info(tenant.name)]);
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.handleTenantError)(error, 'create tenant', new config_1.ConfigManager(getGlobalOptions()));
}
else {
throw error;
}
}
finally {
if (client) {
await client.end();
}
}
});
const deleteCmd = new commander_1.Command('delete')
.description('Delete a tenant')
.requiredOption('--id <id>', 'ID of the tenant to delete')
.addHelpText('after', `
Examples:
${(0, colors_1.formatCommand)('nile tenants delete', '--id tenant-123')} Delete a tenant by ID
${(0, colors_1.formatCommand)('nile tenants delete', '--id 550e8400-e29b-41d4-a716-446655440000')} Delete tenant by UUID
`)
.action(async (cmdOptions) => {
let client;
try {
const options = getGlobalOptions();
const configManager = new config_1.ConfigManager(options);
const token = await configManager.getToken();
const api = new api_1.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(colors_1.theme.dim(`\nDeleting tenant '${tenant.name}' (${tenant.id})...`));
// Delete the tenant
await client.query('DELETE FROM tenants WHERE id = $1', [cmdOptions.id]);
console.log(colors_1.theme.success(`\nTenant '${colors_1.theme.bold(tenant.name)}' deleted successfully.`));
}
catch (error) {
if (axios_1.default.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) {
await (0, errorHandling_1.handleTenantError)(error, 'delete tenant', new config_1.ConfigManager(getGlobalOptions()));
}
else {
throw error;
}
}
finally {
if (client) {
await client.end();
}
}
});
const updateCmd = new commander_1.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:
${(0, colors_1.formatCommand)('nile tenants update', '--id tenant-123 --new_name "New Name"')} Update tenant name
${(0, colors_1.formatCommand)('nile tenants update', '--id 550e8400-e29b-41d4-a716-446655440000 --new_name "New Name"')} Update tenant with UUID
`)
.action(async (cmdOptions) => {
let client;
try {
const options = getGlobalOptions();
const configManager = new config_1.ConfigManager(options);
const api = new api_1.NileAPI({
token: configManager.getToken(),
dbHost: configManager.getDbHost(),
controlPlaneUrl: configManager.getGlobalHost(),
});
const { workspaceSlug, databaseName } = await getWorkspaceAndDatabase(options);
console.log(colors_1.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(colors_1.theme.success(`\nTenant '${colors_1.theme.bold(tenant.id)}' updated successfully.`));
console.log(colors_1.theme.primary('\nUpdated tenant details:'));
console.log(`${colors_1.theme.secondary('ID:')} ${colors_1.theme.primary(tenant.id)}`);
console.log(`${colors_1.theme.secondary('Name:')} ${colors_1.theme.info(tenant.name)}`);
}
catch (error) {
if (axios_1.default.isAxiosError(error) && (error.response?.status === 401 || error.message === 'Token is required')) {
await (0, errorHandling_1.handleTenantError)(error, 'update tenant', new config_1.ConfigManager(getGlobalOptions()));
}
else {
throw error;
}
}
finally {
if (client) {
await client.end();
}
}
});
tenants.addCommand(listCmd);
tenants.addCommand(createCmd);
tenants.addCommand(deleteCmd);
tenants.addCommand(updateCmd);
}
}
exports.TenantsCommand = TenantsCommand;
function createTenantsCommand(getGlobalOptions) {
const program = new commander_1.Command();
new TenantsCommand(program, getGlobalOptions);
return program.commands[0];
}