UNPKG

@vinka/repo

Version:

Database and repo utilities

239 lines (218 loc) 8.41 kB
import { Pool, PoolConfig } from 'pg'; import { Options as SequelizeOptions, Sequelize } from 'sequelize'; import { Migration, Umzug } from 'umzug'; import { DbConfig, Logger, ModelMap, PoolConstructor, SequelizeConstructor, UmzugConstructor, } from './types'; const defaultOptions: SequelizeOptions = { dialect: 'postgres', logging: false, }; export default class SequelizeConnector { public sequelizeClient?: Sequelize; private readonly config: DbConfig; private readonly logger: Logger; private readonly PoolConstructor: PoolConstructor; private readonly SequelizeConstructor: SequelizeConstructor; /** * Constructs a SequelizeConnector. Provides convenience for creating Sequelize clients and performing migrations. * Use createClient to create a Sequelize client. After that you can use this connector's migration methods to * perform migrations on a database. * * @param sequelizeConstructor a Sequelize constructor function. * @param poolConstructor a Postgres Pool constructor function. * @param config the database configuration adapted for Sequelize and Postgres. * @param logger a Logger object. */ constructor( sequelizeConstructor: SequelizeConstructor, poolConstructor: PoolConstructor, config: DbConfig, logger: Logger, ) { this.SequelizeConstructor = sequelizeConstructor; this.PoolConstructor = poolConstructor; this.logger = logger; // take shallow copy this.config = { ...config }; this.config.options = { ...defaultOptions, ...config.options }; } /** * Creates a Sequelize client connected to database specified in config, * creates the database if it does not exist, and sets the created client to * an internal member variable. * * @param [modelMap] the models to associate with the client. If not * provided, the caller needs to initialize models separately. */ public async connect(modelMap?: ModelMap): Promise<Sequelize> { try { await this.testDbExistsOrThrow(this.config); const client = this.createSequelizeClient(this.config, modelMap); this.sequelizeClient = client; return client; } catch (err) { if (err.message.match(new RegExp(`"${this.config.db}" does not exist`))) { this.logger.debug( `failed to connect to database ${this.config.db}, trying to create it`, ); await this.createDatabase(this.config); return this.connect(modelMap); } else { throw err; } } } /** * Creates a new database with name from config.db. Uses database with name * from config.masterDb to create the new database. The master database must * already exist. * * @param config the configuration for the postgres pool used internally. */ public async createDatabase(config: DbConfig): Promise<void> { const pool = await this.createPgPool(config, config.masterDb); try { await pool.query(`CREATE DATABASE "${config.db}"`); this.logger.debug(`created database ${config.db}`); } catch (err) { this.logger.error( `encountered error when creating database ${config.db}, message: ${err.message}`, ); throw err; } finally { await pool.end(); } } /** * Creates postgres connection pool, which can be used to connect to a database. * @param config the configuration for the pool. * @param dbName the name of the database the pool is connected to. Default is taken from config.db. */ public createPgPool(config: DbConfig, dbName: string = config.db): Pool { const options: PoolConfig = { user: config.user, password: config.pass, host: config.options.host, port: config.options.port, database: dbName, max: 1, ssl: config.ssl ? { rejectUnauthorized: false } : false, }; return new this.PoolConstructor(options); } /** * Creates a Sequelize client and associates user defined models with the * client. * * @param config the configuration for the client. * @param [modelMap] the models to associate with the client. If not * provided, the caller needs to initialize models separately. */ public createSequelizeClient(config: DbConfig, modelMap?: ModelMap): Sequelize { const options: SequelizeOptions = { ...config.options, ssl: config.ssl, // Ref.: https://github.com/brianc/node-postgres/issues/2009 // reject unauthorized dialectOptions: { ssl: config.ssl ? { require: config.ssl, rejectUnauthorized: false, } : false, }, }; if (config.options.logging && this.logger.debug) { options.logging = (...args: any) => this.logger.debug(args.join(' ')); } this.logger.debug(`connecting to database with options ${JSON.stringify(options)}`); const sequelize = new this.SequelizeConstructor( config.db, config.user, config.pass, options, ); if (modelMap) { modelMap.init(sequelize); } this.logger.info(`connected to database ${config.db}`); return sequelize; } /** * Migrates a database down with Umzug. * @param umzugConstructor the constructor for Umzug. * @param to 0 (default) to revert all migrations, or the name of the migration to migrate down to. * @param client the sequelize client used for the migration. Optional if createClient has been called. */ public async migrateDown( umzugConstructor: UmzugConstructor, to: 0 | string = 0, client = this.sequelizeClient, ): Promise<Migration[]> { if (client) { return await this.createUmzug(umzugConstructor, client).down({ to }); } else { throw Error( 'trying to migrate before client is initialized. method createClient not called succesfully?', ); } } /** * Migrates a database up with Umzug. * @param umzugConstructor the constructor for Umzug. * @param client the sequelize client used for the migration. Optional if createClient has been called. */ public async migrateUp( umzugConstructor: UmzugConstructor, client = this.sequelizeClient, ): Promise<Migration[]> { if (client) { return await this.createUmzug(umzugConstructor, client).up(); } else { throw Error( 'trying to migrate before client is initialized. method createClient not called succesfully?', ); } } /** * Tests that a client can be created from a Pool with config or throws. * @param config the config to create the pool with. The pool provides the client. */ public async testDbExistsOrThrow(config: DbConfig): Promise<void> { this.logger.debug(`trying to connect to database ${config.db}`); const pool = await this.createPgPool(config); try { (await pool.connect()).release(); } catch (err) { throw err; } finally { pool.end(); } } /** * Creates an Umzug instance. * @param umzugConstructor the Umzug constructor. * @param client the sequelize client used in the Umzug. */ private createUmzug(umzugConstructor: UmzugConstructor, client: Sequelize): Umzug { return new umzugConstructor({ storage: 'sequelize', storageOptions: { sequelize: client, }, logging: (...args: any) => this.logger.debug(args.join(' ')), migrations: { path: 'migrations', pattern: /\.js/, params: [client.getQueryInterface(), this.SequelizeConstructor], }, }); } }