pg-test-util
Version:
PostgreSQL administrative utilities such as creating and dropping tables, users etc.
266 lines • 16.1 kB
JavaScript
"use strict";
var _PgTestUtil_instances, _PgTestUtil_adminClient, _PgTestUtil_clientConfig, _PgTestUtil_safe, _PgTestUtil_baseName, _PgTestUtil_databases, _PgTestUtil_createdDatabaseNames, _PgTestUtil_createdUserPasswords, _PgTestUtil_isCleaningUp, _PgTestUtil_cleanupOnError, _PgTestUtil_getError, _PgTestUtil_generateName, _PgTestUtil_lastCreatedDatabaseName_get;
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const pg_1 = require("pg");
const verror_1 = tslib_1.__importDefault(require("verror"));
const escape = require("pg-escape");
const database_1 = tslib_1.__importDefault(require("./database"));
const get_connection_config_1 = require("./utils/get-connection-config");
/** PgTestUtil class is used to perform PostgreSQL operations related to unit testing such as create database, truncate database and drop database etc. */
class PgTestUtil {
constructor(client, password, { safe = true, baseName = "test_", cleanupOnError = true } = {}) {
_PgTestUtil_instances.add(this);
_PgTestUtil_adminClient.set(this, void 0);
_PgTestUtil_clientConfig.set(this, void 0);
_PgTestUtil_safe.set(this, void 0);
_PgTestUtil_baseName.set(this, void 0);
_PgTestUtil_databases.set(this, {});
_PgTestUtil_createdDatabaseNames.set(this, []);
_PgTestUtil_createdUserPasswords.set(this, {}); // Keys are user name, values are passwords.
_PgTestUtil_isCleaningUp.set(this, false);
_PgTestUtil_cleanupOnError.set(this, void 0);
tslib_1.__classPrivateFieldSet(this, _PgTestUtil_adminClient, client, "f");
tslib_1.__classPrivateFieldSet(this, _PgTestUtil_clientConfig, { ...tslib_1.__classPrivateFieldGet(this, _PgTestUtil_adminClient, "f").connectionParameters, password }, "f");
tslib_1.__classPrivateFieldSet(this, _PgTestUtil_safe, safe, "f");
tslib_1.__classPrivateFieldSet(this, _PgTestUtil_baseName, baseName, "f");
tslib_1.__classPrivateFieldSet(this, _PgTestUtil_cleanupOnError, cleanupOnError, "f");
}
/**
* Create an instance.
*
* @param connection is the `pg.client` or connection parameters for `pg.client`.
* @param options are options.
*/
static async new(connection, options = {}) {
try {
const connectionConfig = (0, get_connection_config_1.getConnectionConfig)(connection);
const adminClient = connection instanceof pg_1.Client ? connection : new pg_1.Client({ database: "postgres", ...connectionConfig });
await adminClient.connect();
return new this(adminClient, typeof connectionConfig.password === "string" ? connectionConfig.password : undefined, options);
}
catch (error) {
throw new verror_1.default(error, "Cannot connect admin client");
}
}
/**
* Executes given SQL in admin clinet and returns result rows.
* Admin client can be used fro administration queries such as creating databases etc.
*
* @typeparam T is type for single row returned by SQL query.
*
* @param sql is sql query.
* @param params are array of parameters to pass query.
* @returns result rows of the SQL query.
*/
async query(sql, params) {
try {
return (await tslib_1.__classPrivateFieldGet(this, _PgTestUtil_adminClient, "f").query(sql, params)).rows;
}
catch (error) {
throw await tslib_1.__classPrivateFieldGet(this, _PgTestUtil_instances, "m", _PgTestUtil_getError).call(this, error, "Cannot execute admin query");
}
}
/**
* Returns `Database` instance object for given database name. Also connects to database if it is not connected.
* If no connection details are provided, default database is returned using same connection parameters as master database.
*
* @param name is database name to get instance for. `defaultDatabaseName` is used by default.
* @returns [[Database]] instance for given database name.
*/
async getDatabase(name = tslib_1.__classPrivateFieldGet(this, _PgTestUtil_instances, "a", _PgTestUtil_lastCreatedDatabaseName_get)) {
if (!tslib_1.__classPrivateFieldGet(this, _PgTestUtil_databases, "f")[name]) {
tslib_1.__classPrivateFieldGet(this, _PgTestUtil_databases, "f")[name] = await database_1.default.new({
clientConfig: { ...tslib_1.__classPrivateFieldGet(this, _PgTestUtil_clientConfig, "f"), database: name },
cleanup: () => this.cleanup(),
drop: () => this.dropDatabase(name),
});
}
await tslib_1.__classPrivateFieldGet(this, _PgTestUtil_databases, "f")[name].connect();
return tslib_1.__classPrivateFieldGet(this, _PgTestUtil_databases, "f")[name];
}
/** Disconnects admin client. */
async disconnect() {
try {
await tslib_1.__classPrivateFieldGet(this, _PgTestUtil_adminClient, "f").end();
}
catch (error) {
throw await tslib_1.__classPrivateFieldGet(this, _PgTestUtil_instances, "m", _PgTestUtil_getError).call(this, error, "Cannot disconnect admin client");
}
}
/**
* Disconnects all clients.
*
* @param options are options.
* @param options.admin whether to disconnect admin client.
*/
async disconnectAll({ admin = true } = {}) {
const promises = Object.values(tslib_1.__classPrivateFieldGet(this, _PgTestUtil_databases, "f")).map((database) => database.disconnect());
if (admin)
promises.push(this.disconnect());
return Promise.all(promises);
}
/** Fetches the list of all databases from server. */
async fetchAllDatabaseNames(onlyCreated) {
const databases = (await this.query("SELECT datname FROM pg_database")).map((row) => row.datname);
return onlyCreated ? databases.filter((database) => tslib_1.__classPrivateFieldGet(this, _PgTestUtil_createdDatabaseNames, "f").includes(database)) : databases;
}
/**
* Creates a database. If name is not provided generates a name using `baseName` from constructor and part of epoch time.
*
* @param options is configuration
* @param options.name is database name
* @param options.encoding is database encoding
* @param options.template is database template to use.
* @param options.sql is SQL query to execute on database after it is created.
* @param options.file is SQL query file to execute on database after it is created.
* @param options.drop is whether to drop database before create command.
* @param options.safe If true, only databases created by this instance is dropped.
* @returns [[Database]] object representing created database.
*/
async createDatabase({ name = tslib_1.__classPrivateFieldGet(this, _PgTestUtil_instances, "m", _PgTestUtil_generateName).call(this), encoding = "UTF8", template = "template0", sql, file, drop, safe = tslib_1.__classPrivateFieldGet(this, _PgTestUtil_safe, "f"), } = {}) {
const createDBSQL = escape("CREATE DATABASE %I WITH ENCODING = %L TEMPLATE = %I;", name, encoding, template);
if (drop)
await this.dropDatabase(name, { safe });
await this.query(createDBSQL);
tslib_1.__classPrivateFieldGet(this, _PgTestUtil_createdDatabaseNames, "f").push(name);
const database = await this.getDatabase(name);
if (file)
await database.queryFile(file);
if (sql)
await database.query(sql);
return database;
}
/**
* Copies a given database with a new name.
*
* @param options is configuration.
* @param options.from is source database name or [[Database]] instance to copy from.
* @param options.to is target database name or [[Database]] instance to copy to.
* @param options.drop is whether to drop target database before copy.
* @param options.safe If true, only databases created by this instance is dropped.
* @returns [[Database]] object.
*/
async copyDatabase({ source = tslib_1.__classPrivateFieldGet(this, _PgTestUtil_instances, "a", _PgTestUtil_lastCreatedDatabaseName_get), target = tslib_1.__classPrivateFieldGet(this, _PgTestUtil_instances, "m", _PgTestUtil_generateName).call(this), drop = false, safe = tslib_1.__classPrivateFieldGet(this, _PgTestUtil_safe, "f"), }) {
const [sourceName, targetName] = [source instanceof database_1.default ? source.name : source, target instanceof database_1.default ? target.name : target];
if (drop)
await this.dropDatabase(targetName, { safe });
const sourceDatabase = await this.getDatabase(sourceName);
await sourceDatabase.disconnect();
await this.query(escape("CREATE DATABASE %I template %I", targetName, sourceName));
tslib_1.__classPrivateFieldGet(this, _PgTestUtil_createdDatabaseNames, "f").push(targetName);
return this.getDatabase(targetName);
}
/**
* Drops given database. To ensure the task, drops all connections to the database beforehand.
* If `dropOnlyCreated` is true and database is not created by this instance, throws error.
*
* @param database is database name or [[Database]] instance to drop.
* @param options are options
* @param options.safe If true, only databases created by this instance is dropped.
*/
async dropDatabase(database = tslib_1.__classPrivateFieldGet(this, _PgTestUtil_instances, "a", _PgTestUtil_lastCreatedDatabaseName_get), { safe = tslib_1.__classPrivateFieldGet(this, _PgTestUtil_safe, "f") } = {}) {
const name = database instanceof database_1.default ? database.name : database;
const createdDatabaseIndex = tslib_1.__classPrivateFieldGet(this, _PgTestUtil_createdDatabaseNames, "f").indexOf(name);
if (createdDatabaseIndex === -1 && safe)
throw await tslib_1.__classPrivateFieldGet(this, _PgTestUtil_instances, "m", _PgTestUtil_getError).call(this, `'${name}' database is not created by this instance. Set "safe" to "false" to force.`);
if (tslib_1.__classPrivateFieldGet(this, _PgTestUtil_databases, "f")[name]) {
const databaseObj = database instanceof database_1.default ? database : await this.getDatabase(name);
await databaseObj.disconnect();
delete tslib_1.__classPrivateFieldGet(this, _PgTestUtil_databases, "f")[name];
}
await this.dropConnections(name);
await this.query(escape("DROP DATABASE IF EXISTS %I;", name));
if (createdDatabaseIndex > -1)
tslib_1.__classPrivateFieldGet(this, _PgTestUtil_createdDatabaseNames, "f").splice(createdDatabaseIndex, 1); // Delete from created databases.
}
async dropConnections(databaseName) {
await this.query(escape("SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname=%L;", databaseName));
}
/**
* Drops all databases created by this instance.
*
* @param options are options.
* @param options.disconnect is whether to disconnect admin client.
*/
async dropAllDatabases({ disconnect = true } = {}) {
await Promise.all(tslib_1.__classPrivateFieldGet(this, _PgTestUtil_createdDatabaseNames, "f").map((dbName) => this.dropDatabase(dbName)));
if (disconnect)
await this.disconnect();
}
/**
* Creates a new database user if it does not exist.
*
* @param user is the name of the user.
* @param password is the password for the user.
*/
async createUser(user, password) {
try {
// DO NOT USE `this.query()`!!! We use special error handling
await tslib_1.__classPrivateFieldGet(this, _PgTestUtil_adminClient, "f").query(escape("DO $body$ BEGIN CREATE ROLE %I LOGIN PASSWORD %L; END $body$;", user, password));
tslib_1.__classPrivateFieldGet(this, _PgTestUtil_createdUserPasswords, "f")[user] = password; // Only add to created users if user does not exist.
}
catch (error) {
// Ignore error if user exists. (Duplicate user error code is 42710). https://www.postgresql.org/docs/current/errcodes-appendix.html
if (error.code !== "42710")
throw await tslib_1.__classPrivateFieldGet(this, _PgTestUtil_instances, "m", _PgTestUtil_getError).call(this, error, `Cannot create user '${user}'`);
}
}
/**
* Fetches database users from database.
*
* @param onlyCreated is whether to fetch users only created by this utility instance.
* @returns array of usernames.
*/
async getUserNames(onlyCreated = false) {
const result = await this.query('SELECT u.usename AS "name" FROM pg_catalog.pg_user u ORDER BY u.usename');
const userNames = result.map((row) => row.name);
return onlyCreated ? userNames.filter((userName) => userName in tslib_1.__classPrivateFieldGet(this, _PgTestUtil_createdUserPasswords, "f")) : userNames;
}
/**
* Drops database user.
*
* @param user is user name to drop.
* @param options are options.
* @param options.safe If true, only users created by this instance is dropped.
*/
async dropUser(user, { safe = tslib_1.__classPrivateFieldGet(this, _PgTestUtil_safe, "f") } = {}) {
if (safe && !(user in tslib_1.__classPrivateFieldGet(this, _PgTestUtil_createdUserPasswords, "f")))
throw await tslib_1.__classPrivateFieldGet(this, _PgTestUtil_instances, "m", _PgTestUtil_getError).call(this, `'${user}' user is not created by this instance. Set "safe" to "false" to force.`);
await this.query(escape("DROP ROLE IF EXISTS %I", user));
delete tslib_1.__classPrivateFieldGet(this, _PgTestUtil_createdUserPasswords, "f")[user];
}
/** Drops all users created by this instance. */
async dropAllUsers() {
await Promise.all(Object.keys(tslib_1.__classPrivateFieldGet(this, _PgTestUtil_createdUserPasswords, "f")).map((user) => this.dropUser(user)));
tslib_1.__classPrivateFieldSet(this, _PgTestUtil_createdUserPasswords, {}, "f");
}
/**
* Drops all items created by this instance.
*
* @param options are options.
* @param options.disconnect is whether to disconnect admin client.
*/
async dropAll({ disconnect = true } = {}) {
await Promise.all([this.dropAllDatabases({ disconnect }), this.dropAllUsers()]);
}
async cleanup() {
if (tslib_1.__classPrivateFieldGet(this, _PgTestUtil_isCleaningUp, "f"))
return;
tslib_1.__classPrivateFieldSet(this, _PgTestUtil_isCleaningUp, true, "f");
await this.dropAll({ disconnect: true });
tslib_1.__classPrivateFieldSet(this, _PgTestUtil_isCleaningUp, false, "f");
}
}
exports.default = PgTestUtil;
_PgTestUtil_adminClient = new WeakMap(), _PgTestUtil_clientConfig = new WeakMap(), _PgTestUtil_safe = new WeakMap(), _PgTestUtil_baseName = new WeakMap(), _PgTestUtil_databases = new WeakMap(), _PgTestUtil_createdDatabaseNames = new WeakMap(), _PgTestUtil_createdUserPasswords = new WeakMap(), _PgTestUtil_isCleaningUp = new WeakMap(), _PgTestUtil_cleanupOnError = new WeakMap(), _PgTestUtil_instances = new WeakSet(), _PgTestUtil_getError = async function _PgTestUtil_getError(cause, message) {
if (tslib_1.__classPrivateFieldGet(this, _PgTestUtil_cleanupOnError, "f"))
await this.cleanup();
return new verror_1.default(cause, message);
}, _PgTestUtil_generateName = function _PgTestUtil_generateName() {
const dbNo = new Date().getTime() - 1518518200000;
return `${tslib_1.__classPrivateFieldGet(this, _PgTestUtil_baseName, "f")}_${dbNo}`;
}, _PgTestUtil_lastCreatedDatabaseName_get = function _PgTestUtil_lastCreatedDatabaseName_get() {
return tslib_1.__classPrivateFieldGet(this, _PgTestUtil_createdDatabaseNames, "f")[tslib_1.__classPrivateFieldGet(this, _PgTestUtil_createdDatabaseNames, "f").length - 1];
};
//# sourceMappingURL=pg-test-util.js.map