@vinka/repo
Version:
Database and repo utilities
239 lines (218 loc) • 8.41 kB
text/typescript
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],
},
});
}
}