knex-db-manager
Version:
Collection of administrative database operations for knex supported databases
328 lines (299 loc) • 8.7 kB
JavaScript
var _ = require('lodash'),
pg = require('pg'),
escape = require('pg-escape'),
Promise = require('bluebird'),
classUtils = require('./class-utils'),
DatabaseManager = require('./DatabaseManager').default;
/**
* @constructor
*/
function PostgresDatabaseManager() {
DatabaseManager.apply(this, arguments);
this._masterClient = null;
this._cachedTableNames = null;
this._cachedIdSequences = null;
}
classUtils.inherits(PostgresDatabaseManager, DatabaseManager);
/**
* @Override
*/
PostgresDatabaseManager.prototype.createDbOwnerIfNotExist = function() {
return this._masterQuery(
"DO $body$ BEGIN CREATE ROLE %I LOGIN PASSWORD %L; EXCEPTION WHEN others THEN RAISE NOTICE 'User exists, not re-creating'; END $body$;",
[this.config.knex.connection.user, this.config.knex.connection.password]
);
};
/**
* @Override
*/
PostgresDatabaseManager.prototype.createDb = function(databaseName) {
databaseName = databaseName || this.config.knex.connection.database;
var collate = this.config.dbManager.collate;
var owner = this.config.knex.connection.user;
var self = this;
var promise = Promise.reject(new Error());
if (_.isEmpty(collate)) {
promise = promise.catch(function() {
return self._masterQuery(
"CREATE DATABASE %I OWNER = '%I' ENCODING = 'UTF-8' TEMPLATE template1",
[databaseName, owner]
);
});
} else {
// Try to create with each collate. Use the first one that works. This is kind of a hack
// but seems to be the only reliable way to make this work with both windows and unix.
_.each(collate, function(locale) {
promise = promise.catch(function() {
return self._masterQuery(
"CREATE DATABASE %I OWNER = '%I' ENCODING = 'UTF-8' LC_COLLATE = %L TEMPLATE template0",
[databaseName, owner, locale]
);
});
});
}
return promise;
};
/**
* Drops database with name if db exists.
*
* @Override
*/
PostgresDatabaseManager.prototype.dropDb = function(databaseName) {
var self = this;
databaseName = databaseName || this.config.knex.connection.database;
return this.closeKnex().then(function() {
return self._masterQuery('DROP DATABASE IF EXISTS %I', [databaseName]);
});
};
/**
* @Override
*/
PostgresDatabaseManager.prototype.copyDb = function(
fromDatabaseName,
toDatabaseName
) {
var self = this;
return this.closeKnex().then(function() {
return self._masterQuery('CREATE DATABASE %I template %I', [
toDatabaseName,
fromDatabaseName,
]);
});
};
/**
* @Override
*/
PostgresDatabaseManager.prototype.truncateDb = function(ignoreTables) {
var knex = this.knexInstance();
var config = this.config;
if (!this._cachedTableNames) {
this._updateTableNameCache(knex, config);
}
return this._cachedTableNames.then(function(tableNames) {
var filteredTableNames = _.filter(tableNames, function(tableName) {
return !_.includes(ignoreTables || [], tableName);
});
if (!_.isEmpty(filteredTableNames)) {
return knex.raw(
'TRUNCATE TABLE "' +
filteredTableNames.join('","') +
'" RESTART IDENTITY'
);
}
});
};
/**
* @Override
*/
PostgresDatabaseManager.prototype.updateIdSequences = function() {
var knex = this.knexInstance();
var config = this.config;
if (!this._cachedIdSequences) {
this._updateIdSequenceCache(knex, config);
}
// Set current value of id sequence for each table.
// If there are no rows in the table, the value will be set to sequence's minimum constraint.
// Otherwise, it will be set to max(id) + 1.
return this._cachedIdSequences.then(function(result) {
var query = _.map(result.rows, function(row) {
return escape(
'SELECT setval(\'"%s"\', GREATEST(coalesce(max(id),0) + 1, \'%s\'), false) FROM "%I"',
row.sequence,
row.min,
row.table
);
});
query = query.join(' UNION ALL ') + ';';
return knex.raw(query);
});
};
/**
* @private
*/
PostgresDatabaseManager.prototype._updateTableNameCache = function(
knex,
config
) {
this._cachedTableNames = knex('pg_tables')
.select('tablename')
.where('schemaname', 'public')
.then(function(tables) {
return _.map(tables, 'tablename');
});
};
/**
* Id sequence cache holds a Promise, that returns following objects:
* {
* table: String, // Table that rest of the values target
* sequence: String, // Sequence for the primary key (which is assumed to be id)
* min: String // Minimum allowed value for the sequence
* }
*
* These values are cached because they are not expected to change often,
* and finding them is slow.
*
* @private
*/
PostgresDatabaseManager.prototype._updateIdSequenceCache = function(
knex,
config
) {
if (!this._cachedTableNames) {
this._updateTableNameCache(knex, config);
}
this._cachedIdSequences = this._cachedTableNames
.then(function(tableNames) {
// Skip tables without id column.
return knex('information_schema.columns')
.select('table_name')
.where('column_name', 'id')
.then(function(tables) {
return _.intersection(_.map(tables, 'table_name'), tableNames);
});
// Find name of the id sequence for each table.
// This is required for searching the minimum constraint for the sequence.
})
.then(function(idTableNames) {
var query = _.map(idTableNames, function(tableName) {
return escape(
"SELECT '%I' AS table, substring(pg_get_serial_sequence('\"%I\"', 'id') from '\"\\?([^\".]+)\"\\?$') AS sequence, substring(pg_get_serial_sequence('\"%I\"', 'id') from '^\"\\?([^\".]+)\"\\?.') AS schema",
tableName,
tableName,
tableName
);
});
query = query.join(' UNION ALL ') + ';';
return knex.raw(query);
// Find min constraint for each of the id sequences.
})
.then(function(result) {
var query = _.map(result.rows, function(row) {
return escape(
"SELECT '%I' AS table, '%s' AS sequence, minimum_value AS min FROM information_schema.sequences where sequence_name = '%s' and sequence_schema = '%s'",
row.table,
row.sequence,
row.sequence,
row.schema
);
});
query = query.join(' UNION ALL ') + ';';
return knex.raw(query);
});
};
/**
* @Override
*/
PostgresDatabaseManager.prototype.close = function() {
var disconnectAll = [this.closeKnex()];
if (this._masterClient) {
disconnectAll.push(
this._masterClient.then(function(client) {
client.end();
})
);
this._masterClient = null;
}
return Promise.all(disconnectAll);
};
/**
* @private
* @returns {Promise}
*/
PostgresDatabaseManager.prototype._masterQuery = function(query, params) {
var self = this;
if (!this._masterClient) {
this._masterClient = this.create_masterClient();
}
return this._masterClient.then(function(client) {
return self.perform_masterQuery(client, query, params);
});
};
/**
* @private
* @returns {Promise}
*/
PostgresDatabaseManager.prototype.create_masterClient = function() {
var self = this;
return new Promise(function(resolve, reject) {
var client = new pg.Client({
connectionString: self._masterConnectionUrl(),
});
client.connect(function(err) {
if (err) {
reject(err);
} else {
resolve(client);
}
});
});
};
/**
* @private
* @returns {Promise}
*/
PostgresDatabaseManager.prototype.perform_masterQuery = function(
client,
query,
params
) {
return new Promise(function(resolve, reject) {
if (params) {
var args = [query].concat(params);
query = escape.apply(global, args);
}
client.query(query, function(err, result) {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
};
/**
* @private
* @returns {String}
*/
PostgresDatabaseManager.prototype._masterConnectionUrl = function() {
var url = 'postgres://';
if (this.config.dbManager.superUser) {
url += this.config.dbManager.superUser;
} else {
throw new Error('DatabaseManager: database config must have `superUser`');
}
if (this.config.dbManager.superPassword) {
url +=
':' +
encodeURIComponent(this.config.dbManager.superPassword).replace(
/!/g,
'%21'
);
}
var port = this.config.knex.connection.port || 5432;
url += '@' + this.config.knex.connection.host + ':' + port + '/postgres';
return url;
};
module.exports = {
default: PostgresDatabaseManager,
PostgresDatabaseManager: PostgresDatabaseManager,
};