UNPKG

pg-boss

Version:

Queueing jobs in Postgres from Node.js like a boss

334 lines (327 loc) 12.2 kB
#!/usr/bin/env node import { parseArgs } from 'node:util'; import { readFileSync, existsSync } from 'node:fs'; import { resolve } from 'node:path'; import Db from "./db.js"; import * as plans from "./plans.js"; import * as migrationStore from "./migrationStore.js"; import packageJson from '../package.json' with { type: 'json' }; const schemaVersion = packageJson.pgboss.schema; function printHelp() { console.log(` pg-boss CLI v${packageJson.version} Usage: pg-boss <command> [options] Commands: migrate Run pending migrations (creates schema if not exists) create Create the pg-boss schema (initial installation) version Show current schema version plans Output SQL plans without executing rollback Rollback the last migration Options: --help, -h Show this help message --config, -c <file> Path to config file (default: pgboss.json) --schema, -s <name> Schema name (default: pgboss) --host <host> Database host --port <port> Database port --database, -d <name> Database name --user, -u <user> Database user --password, -p <pass> Database password --connection-string Full connection string (overrides other connection options) --ssl Enable SSL connection --dry-run Output SQL without executing (for plans command) Environment Variables: PGBOSS_DATABASE_URL Full connection string PGBOSS_HOST Database host PGBOSS_PORT Database port PGBOSS_DATABASE Database name PGBOSS_USER Database user PGBOSS_PASSWORD Database password PGBOSS_SCHEMA Schema name (default: pgboss) Config File (pgboss.json): { "host": "localhost", "port": 5432, "database": "mydb", "user": "postgres", "password": "secret", "schema": "pgboss", "ssl": false } Examples: pg-boss migrate pg-boss migrate --schema my_schema pg-boss create --connection-string postgres://user:pass@localhost/db pg-boss plans migrate --dry-run pg-boss version PGBOSS_DATABASE_URL=postgres://localhost/mydb pg-boss migrate `); } function loadConfigFile(configPath) { const paths = configPath ? [resolve(configPath)] : [ resolve('pgboss.json'), resolve('.pgbossrc'), resolve('.pgbossrc.json') ]; for (const filePath of paths) { if (existsSync(filePath)) { try { const content = readFileSync(filePath, 'utf-8'); const config = JSON.parse(content); console.log(`Loaded config from ${filePath}`); return config; } catch (err) { console.error(`Error reading config file ${filePath}: ${err.message}`); process.exit(1); } } } return {}; } function getConnectionConfig(args) { const fileConfig = loadConfigFile(args.config); const config = { connectionString: args.connectionString || process.env.PGBOSS_DATABASE_URL || fileConfig.connectionString, host: args.host || process.env.PGBOSS_HOST || fileConfig.host, port: args.port ? parseInt(args.port, 10) : (process.env.PGBOSS_PORT ? parseInt(process.env.PGBOSS_PORT, 10) : fileConfig.port), database: args.database || process.env.PGBOSS_DATABASE || fileConfig.database, user: args.user || process.env.PGBOSS_USER || fileConfig.user, password: args.password || process.env.PGBOSS_PASSWORD || fileConfig.password, schema: args.schema || process.env.PGBOSS_SCHEMA || fileConfig.schema || plans.DEFAULT_SCHEMA }; if (args.ssl || fileConfig.ssl) { config.ssl = args.ssl ? { rejectUnauthorized: false } : fileConfig.ssl; } if (!config.connectionString && !config.host && !config.database) { console.error('Error: No database connection configured.'); console.error('Provide connection via --connection-string, environment variables, or config file.'); console.error('Run "pg-boss --help" for more information.'); process.exit(1); } return config; } function parseCliArgs() { const { values, positionals } = parseArgs({ options: { help: { type: 'boolean', short: 'h' }, config: { type: 'string', short: 'c' }, schema: { type: 'string', short: 's' }, host: { type: 'string' }, port: { type: 'string' }, database: { type: 'string', short: 'd' }, user: { type: 'string', short: 'u' }, password: { type: 'string', short: 'p' }, 'connection-string': { type: 'string' }, ssl: { type: 'boolean' }, 'dry-run': { type: 'boolean' } }, allowPositionals: true }); return { help: values.help, config: values.config, schema: values.schema, host: values.host, port: values.port, database: values.database, user: values.user, password: values.password, connectionString: values['connection-string'], ssl: values.ssl, dryRun: values['dry-run'], command: positionals[0], subCommand: positionals[1] }; } async function createDb(config) { const db = new Db(config); await db.open(); return db; } async function getSchemaVersion(db, schema) { try { const result = await db.executeSql(plans.versionTableExists(schema)); if (!result.rows[0].name) { return null; } const versionResult = await db.executeSql(plans.getVersion(schema)); return versionResult.rows.length ? parseInt(versionResult.rows[0].version) : null; } catch { return null; } } async function cmdVersion(args) { const config = getConnectionConfig(args); const schema = config.schema || plans.DEFAULT_SCHEMA; const db = await createDb(config); try { const version = await getSchemaVersion(db, schema); if (version === null) { console.log(`pg-boss is not installed in schema "${schema}"`); } else { console.log(`Current schema version: ${version}`); console.log(`Latest schema version: ${schemaVersion}`); if (version < schemaVersion) { console.log(`Migrations pending: ${schemaVersion - version}`); } else { console.log('Schema is up to date'); } } } finally { await db.close(); } } async function cmdCreate(args) { const config = getConnectionConfig(args); const schema = config.schema || plans.DEFAULT_SCHEMA; if (args.dryRun) { const sql = plans.create(schema, schemaVersion, { createSchema: true }); console.log('-- SQL to create pg-boss schema:'); console.log(sql); return; } const db = await createDb(config); try { const version = await getSchemaVersion(db, schema); if (version !== null) { console.log(`pg-boss is already installed in schema "${schema}" at version ${version}`); return; } console.log(`Creating pg-boss schema "${schema}"...`); const sql = plans.create(schema, schemaVersion, { createSchema: true }); await db.executeSql(sql); console.log(`Successfully created pg-boss schema "${schema}" at version ${schemaVersion}`); } finally { await db.close(); } } async function cmdMigrate(args) { const config = getConnectionConfig(args); const schema = config.schema || plans.DEFAULT_SCHEMA; if (args.dryRun) { const sql = migrationStore.migrate(schema, 0); console.log('-- SQL to migrate pg-boss from version 0 to latest:'); console.log(sql); return; } const db = await createDb(config); try { const version = await getSchemaVersion(db, schema); if (version === null) { console.log(`pg-boss is not installed. Creating schema "${schema}"...`); const sql = plans.create(schema, schemaVersion, { createSchema: true }); await db.executeSql(sql); console.log(`Successfully created pg-boss schema "${schema}" at version ${schemaVersion}`); return; } if (version >= schemaVersion) { console.log(`pg-boss schema "${schema}" is already at version ${version} (latest: ${schemaVersion})`); return; } console.log(`Migrating pg-boss schema "${schema}" from version ${version} to ${schemaVersion}...`); const sql = migrationStore.migrate(schema, version); await db.executeSql(sql); console.log(`Successfully migrated pg-boss schema "${schema}" to version ${schemaVersion}`); } finally { await db.close(); } } async function cmdRollback(args) { const config = getConnectionConfig(args); const schema = config.schema || plans.DEFAULT_SCHEMA; const db = await createDb(config); try { const version = await getSchemaVersion(db, schema); if (version === null) { console.log(`pg-boss is not installed in schema "${schema}"`); return; } if (version <= 1) { console.log('Cannot rollback: already at minimum version'); return; } if (args.dryRun) { const sql = migrationStore.rollback(schema, version); console.log(`-- SQL to rollback pg-boss from version ${version} to ${version - 1}:`); console.log(sql); return; } console.log(`Rolling back pg-boss schema "${schema}" from version ${version} to ${version - 1}...`); const sql = migrationStore.rollback(schema, version); await db.executeSql(sql); console.log(`Successfully rolled back pg-boss schema "${schema}" to version ${version - 1}`); } finally { await db.close(); } } async function cmdPlans(args) { const fileConfig = loadConfigFile(args.config); const schema = args.schema || process.env.PGBOSS_SCHEMA || fileConfig.schema || plans.DEFAULT_SCHEMA; const subCommand = args.subCommand || 'migrate'; switch (subCommand) { case 'create': case 'construct': console.log('-- SQL to create pg-boss schema:'); console.log(plans.create(schema, schemaVersion, { createSchema: true })); break; case 'migrate': console.log('-- SQL to migrate pg-boss (from version 0 to latest):'); console.log(migrationStore.migrate(schema, 0)); break; case 'rollback': console.log(`-- SQL to rollback pg-boss from version ${schemaVersion} to ${schemaVersion - 1}:`); console.log(migrationStore.rollback(schema, schemaVersion)); break; default: console.error(`Unknown plans subcommand: ${subCommand}`); console.error('Available: create, migrate, rollback'); process.exit(1); } } async function main() { const args = parseCliArgs(); if (args.help || !args.command) { printHelp(); process.exit(0); } try { switch (args.command) { case 'version': await cmdVersion(args); break; case 'create': await cmdCreate(args); break; case 'migrate': await cmdMigrate(args); break; case 'rollback': await cmdRollback(args); break; case 'plans': await cmdPlans(args); break; default: console.error(`Unknown command: ${args.command}`); console.error('Run "pg-boss --help" for available commands.'); process.exit(1); } } catch (err) { console.error(`Error: ${err.message}`); if (process.env.DEBUG) { console.error(err.stack); } process.exit(1); } } main();