UNPKG

pg-test-util

Version:

PostgreSQL administrative utilities such as creating and dropping tables, users etc.

266 lines 16.1 kB
"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