UNPKG

@golemio/db-migrate-base

Version:
988 lines (860 loc) 25.9 kB
var util = require('util'); var events = require('events'); var log; var type; var Class = require('./class.js'); var Promise = require('bluebird'); var internals = {}; var Base = Class.extend({ init: function(intern) { this._escapeDDL = this._escapeDDL || '"'; this._escapeString = this._escapeString || "'"; internals = intern; this.internals = intern; log = this.internals.mod.log; type = this.internals.mod.type; this.eventEmmiter = new events.EventEmitter(); for (var n in events.EventEmitter.prototype) { this[n] = events.EventEmitter.prototype[n]; } }, close: function() { throw new Error('not implemented'); }, _translateSpecialDefaultValues: function( spec, options, tableName, columnName ) { spec.defaultValue.prep = null; log.warn( 'special default value ' + spec.defaultvalue.special + ' is not supported by your driver. Setting to no defaultvalue instead.' ); }, _prepareSpec: function(columnName, spec, options, tableName) { if (spec.defaultValue) { if (spec.defaultValue.raw) { spec.defaultValue.prep = spec.defaultValue.raw; } else if (spec.defaultValue.special) { this._translateSpecialDefaultValues( spec, options, tableName, columnName ); } } }, mapDataType: function(str) { switch (str) { case type.STRING: return 'VARCHAR'; case type.TEXT: return 'TEXT'; case type.INTEGER: return 'INTEGER'; case type.BIG_INTEGER: return 'BIGINT'; case type.DATE_TIME: return 'INTEGER'; case type.REAL: return 'REAL'; case type.BLOB: return 'BLOB'; case type.TIMESTAMP: return 'TIMESTAMP'; case type.BINARY: return 'BINARY'; case type.BOOLEAN: return 'BOOLEAN'; case type.DECIMAL: return 'DECIMAL'; case type.CHAR: return 'CHAR'; case type.DATE: return 'DATE'; case type.SMALLINT: return 'SMALLINT'; default: var unknownType = str.toUpperCase(); log.warn('Using unknown data type', unknownType); return unknownType; } }, truncate: function(tableName, callback) { return this.runSql( 'TRUNCATE ' + this._escapeDDL + tableName + this._escapeDDL ).nodeify(callback); }, checkDBMS: function(dbms, callback) { if (this.dbms === dbms) return Promise.resolve(dbms).nodeify(callback); else return Promise.reject('dbms does not match'); }, createDatabase: function() { throw new Error('not implemented'); }, showDatabase: function() { throw new Error('not implemented'); }, switchDatabase: function() { throw new Error('not implemented'); }, dropDatabase: function() { throw new Error('not implemented'); }, recurseCallbackArray: function(foreignKeys, callback) { var fkFunc; var promises = []; while ((fkFunc = foreignKeys.pop())) { promises.push(Promise.resolve(fkFunc())); } return Promise.all(promises).nodeify(callback); }, bindForeignKey: function(tableName, columnName, fkOptions) { var self = this; var mapping = {}; if (typeof fkOptions.mapping === 'string') { mapping[columnName] = fkOptions.mapping; } else mapping = fkOptions.mapping; return function(callback) { if (typeof callback === 'function') { self.addForeignKey( tableName, fkOptions.table, fkOptions.name, mapping, fkOptions.rules, callback ); } else { return self.addForeignKey( tableName, fkOptions.table, fkOptions.name, mapping, fkOptions.rules ); } }; }, createColumnDef: function(name, spec, options) { name = this._escapeDDL + name + this._escapeDDL; var type = this.mapDataType(spec.type); var len = spec.length ? util.format('(%s)', spec.length) : ''; var constraint = this.createColumnConstraint(spec, options); return { foreignKey: null, constraints: [name, type, len, constraint].join(' ') }; }, _createList: function(table, opt = {}) { var options = { columns: { id: { type: type.INTEGER, notNull: true, primaryKey: true, autoIncrement: true }, name: { type: type.STRING, length: 255, notNull: true }, run_on: { type: type.DATE_TIME, notNull: true } }, ifNotExists: true }; return this.createTable(table, options); }, _createKV: function(table, opt = { valueJson: false }) { var options = { columns: { key: { type: type.STRING, notNull: true, primaryKey: true, unique: true }, value: { type: type.TEXT, notNull: true }, run_on: { type: type.DATE_TIME, notNull: true } }, ifNotExists: true }; return this.createTable(table, options); }, createMigrationsTable: function(callback) { var options = { columns: { id: { type: type.INTEGER, notNull: true, primaryKey: true, autoIncrement: true }, name: { type: type.STRING, length: 255, notNull: true }, run_on: { type: type.DATE_TIME, notNull: true } }, ifNotExists: true }; this.createTable(this.internals.migrationTable, options, callback); }, createSeedsTable: function(callback) { var options = { columns: { id: { type: type.INTEGER, notNull: true, primaryKey: true, autoIncrement: true }, name: { type: type.STRING, length: 255, notNull: true }, run_on: { type: type.DATE_TIME, notNull: true } }, ifNotExists: true }; this.createTable(this.internals.seedTable, options, callback); }, _handleMultiPrimaryKeys: function(primaryKeyColumns) { return util.format( ', PRIMARY KEY (%s)', this.quoteDDLArr( primaryKeyColumns.map(function(value) { return value.name; }) ).join(', ') ); }, createTable: function(tableName, options, callback) { log.verbose('creating table:', tableName); var columnSpecs = options; var opts = {}; if (options.columns !== undefined) { columnSpecs = options.columns; opts = options; } var ifNotExistsSql = ''; if (opts.ifNotExists) { ifNotExistsSql = 'IF NOT EXISTS'; } var primaryKeyColumns = []; var columnDefOptions = { emitPrimaryKey: false }; for (var columnName in columnSpecs) { var columnSpec = this.normalizeColumnSpec(columnSpecs[columnName]); columnSpecs[columnName] = columnSpec; if (columnSpec.primaryKey) { primaryKeyColumns.push({ spec: columnSpec, name: columnName }); } } var pkSql = ''; if (primaryKeyColumns.length > 1) { pkSql = this._handleMultiPrimaryKeys(primaryKeyColumns); } else if (primaryKeyColumns.length === 1) { primaryKeyColumns[0] = primaryKeyColumns[0].name; columnDefOptions.emitPrimaryKey = true; } var columnDefs = []; var callbacks = []; var extensions = ''; var tableOptions = ''; for (var columnName in columnSpecs) { var columnSpec = columnSpecs[columnName]; this._prepareSpec(columnName, columnSpec, columnDefOptions, tableName); var constraint = this.createColumnDef( columnName, columnSpec, columnDefOptions, tableName ); columnDefs.push(constraint.constraints); // check foreignKey for backward compatiable if (constraint.foreignKey) callbacks.push(constraint.foreignKey); if (constraint.callbacks) { // support multiple callbacks callbacks = callbacks.concat(constraint.callbacks); } } if (typeof this._applyExtensions === 'function') { extensions = this._applyExtensions(options, tableName); } if (typeof this._applyTableOptions === 'function') { tableOptions = this._applyTableOptions(options, tableName); } var sql = util.format( 'CREATE TABLE %s %s (%s%s%s) %s', ifNotExistsSql, this.escapeDDL(tableName), columnDefs.join(', '), extensions, pkSql, tableOptions ); // Create the first (main) schema // The rest is optional and will be used as a search path (alongside the main schema) // EX: bikesharing, public const schemas = this.schema.split(",").map(schema => schema.trim()); const escapedSchemas = schemas.map(schema => this.escapeDDL(schema)); return this.runSql(` DO $$ BEGIN IF NOT EXISTS( SELECT nspname FROM pg_namespace WHERE nspname = '${schemas[0]}' ) THEN EXECUTE 'CREATE SCHEMA ${escapedSchemas[0]}'; END IF; END $$; SET search_path TO ${escapedSchemas.join(",")}; ${sql} `).then( function() { if (this._dbmControl === true) this._counter.signal(); return this.recurseCallbackArray(callbacks); }.bind(this) ) .nodeify(callback); }, dropTable: function(tableName, options, callback) { if (arguments.length < 3 && typeof options === 'function') { callback = options; options = {}; } else { options = options || {}; } var ifExistsSql = ''; if (options.ifExists) { ifExistsSql = 'IF EXISTS'; } var sql = util.format( 'DROP TABLE %s %s', ifExistsSql, this.escapeDDL(tableName) ); return this.runSql(sql).nodeify(callback); }, renameTable: function(tableName, newTableName, callback) { throw new Error('not implemented'); }, addColumn: function(tableName, columnName, columnSpec, callback) { var columnSpec = this.normalizeColumnSpec(columnSpec); this._prepareSpec(columnName, columnSpec, {}, tableName); var def = this.createColumnDef(columnName, columnSpec, {}, tableName); var extensions = ''; var self = this; if (typeof this._applyAddColumnExtension === 'function') { extensions = this._applyAddColumnExtension(def, tableName, columnName); } var sql = util.format( 'ALTER TABLE %s ADD COLUMN %s %s', this.escapeDDL(tableName), def.constraints, extensions ); return this.runSql(sql) .then(function() { if (this._dbmControl === true) this._counter.signal(); var callbacks = def.callbacks || []; if (def.foreignKey) callbacks.push(def.foreignKey); return self.recurseCallbackArray(callbacks); }) .nodeify(callback); }, removeColumn: function(tableName, columnName, callback) { throw new Error('not implemented'); }, renameColumn: function(tableName, oldColumnName, newColumnName, callback) { throw new Error('not implemented'); }, changeColumn: function(tableName, columnName, columnSpec, callback) { throw new Error('not implemented'); }, quoteDDLArr: function(arr) { for (var i = 0; i < arr.length; ++i) { arr[i] = this._escapeDDL + arr[i] + this._escapeDDL; } return arr; }, quoteArr: function(arr) { for (var i = 0; i < arr.length; ++i) { arr[i] = this._escapeString + arr[i] + this._escapeString; } return arr; }, addIndex: function(tableName, indexName, columns, unique, callback) { if (typeof unique === 'function') { callback = unique; unique = false; } if (!Array.isArray(columns)) { columns = [columns]; } var sql = util.format( 'CREATE %s INDEX "%s" ON "%s" (%s)', unique ? 'UNIQUE' : '', indexName, tableName, this.quoteDDLArr(columns).join(', ') ); return this.runSql(sql).nodeify(callback); }, insert: function(tableName, valueArray, callback) { var columnNameArray = {}; if (arguments.length > 3 || Array.isArray(callback)) { columnNameArray = valueArray; valueArray = callback; } else { var names; if (Array.isArray(valueArray)) { names = Object.keys(valueArray[0]); } else { names = Object.keys(valueArray); } for (var i = 0; i < names.length; ++i) { columnNameArray[names[i]] = names[i]; } } if (columnNameArray.length !== valueArray.length) { return Promise.reject( new Error('The number of columns does not match the number of values.') ).nodeify(callback); } var sql = util.format('INSERT INTO %s ', this.escapeDDL(tableName)); var columnNames = '('; var values = 'VALUES '; var values_part = []; for (var index in columnNameArray) { columnNames += this.escapeDDL(columnNameArray[index]); if (Array.isArray(valueArray) && typeof valueArray[0] === 'object') { for (var i = 0; i < valueArray.length; ++i) { values_part[i] = values_part[i] || ''; if (typeof valueArray[i][index] === 'string') { values_part[i] += this.escapeString(valueArray[i][index]); } else { values_part[i] += valueArray[i][index]; } } } else { if (typeof valueArray[index] === 'string') { values_part += this.escapeString(valueArray[index]); } else { values_part += valueArray[index]; } values_part += ','; } columnNames += ','; } if (Array.isArray(valueArray) && typeof valueArray[0] === 'object') { for (var i = 0; i < values_part.length; ++i) { values += '(' + values_part[i].slice(0, -1) + '),'; } values = values.slice(0, -1); } else { values += '(' + values_part.slice(0, -1) + ')'; } sql += columnNames.slice(0, -1) + ') ' + values + ';'; return this.runSql(sql).nodeify(callback); }, update: function(tableName, valueArray, ids, callback) { var columnNameArray = {}; if (arguments.length > 4 && arguments[1].length !== arguments[2].length) { return callback( new Error('The number of columns does not match the number of values.') ); } else if (arguments.length > 4) { columnNameArray = valueArray; valueArray = ids; ids = callback; callback = arguments[4]; } else { var names; if (Array.isArray(valueArray)) { names = Object.keys(valueArray[0]); } else { names = Object.keys(valueArray); } for (var i = 0; i < names.length; ++i) { columnNameArray[names[i]] = names[i]; } } var sql = util.format( 'UPDATE ' + this._escapeDDL + '%s' + this._escapeDDL + ' SET ', tableName ); for (var index in columnNameArray) { sql += columnNameArray[index] + '='; if (typeof valueArray[index] === 'string') { sql += this._escapeString + this.escape(valueArray[index]) + this._escapeString; } else { sql += valueArray[index]; } if (index != columnNameArray.length - 1) { sql += ', '; } } sql = sql.substring(0, sql.length - 2) + ' ' + this.buildWhereClause(ids); return this.runSql(sql).nodeify(callback); }, lookup: function(tableName, column, id, callback) { var sql = 'SELECT ' + this.escapeDDL(column) + ' FROM ' + this.escapeDDL(tableName) + ' ' + this.buildWhereClause(id); return this.runSql(sql).then(function(row) { return row[0]; }); }, removeIndex: function(tableName, indexName, callback) { if (arguments.length === 2 && typeof indexName === 'function') { callback = indexName; indexName = tableName; } else if (arguments.length === 1 && typeof tableName === 'string') { indexName = tableName; } var sql = util.format('DROP INDEX "%s"', indexName); return this.runSql(sql).nodeify(callback); }, addForeignKey: function() { throw new Error('not implemented'); }, removeForeignKey: function() { throw new Error('not implemented'); }, normalizeColumnSpec: function(obj) { if (typeof obj === 'string') { return { type: obj }; } else { return obj; } }, _insertEntry: function(table, name) { return this.runSql( 'INSERT INTO ' + this.escapeDDL(table) + ' (' + this.escapeDDL('name') + ', ' + this.escapeDDL('run_on') + ') VALUES (?, ?)', [name, new Date()] ); }, _insertKV: function(table, key, value) { return this.runSql( `INSERT INTO ${this.escapeDDL(table)} (${this.escapeDDL('key')}, ${this.escapeDDL('value')}, ${this.escapeDDL( 'run_on' )}) VALUES (?, ?, ?)`, [key, value, new Date()] ); }, _updateKV: function(table, key, value) { return this.runSql( `UPDATE ${this.escapeDDL(table)} SET ${this.escapeDDL('value')} = ?, ${this.escapeDDL('run_on')} = ? WHERE ${this.escapeDDL('key')} = ?`, [value, new Date(), key] ); }, _updateKVC: function(table, key, value, c, v) { return this.runSql( `UPDATE ${this.escapeDDL(table)} SET ${this.escapeDDL('value')} = ?, ${this.escapeDDL('run_on')} = ? WHERE ${this.escapeDDL('key')} = ? AND ${this.escapeDDL(c)} = ?`, [value, new Date(), key, v] ); }, addMigrationRecord: function(name, callback) { this.runSql( 'INSERT INTO ' + this.escapeDDL(this.internals.migrationTable) + ' (' + this.escapeDDL('name') + ', ' + this.escapeDDL('run_on') + ') VALUES (?, ?)', [name, new Date()], callback ); }, addSeedRecord: function(name, callback) { this.runSql( 'INSERT INTO ' + this.escapeDDL(this.internals.seedTable) + ' (' + this.escapeDDL('name') + ', ' + this.escapeDDL('run_on') + ') VALUES (?, ?)', [name, new Date()], callback ); }, startMigration: function(cb) { return Promise.resolve().nodeify(cb); }, endMigration: function(cb) { return Promise.resolve().nodeify(cb); }, // sql, params, callback // sql, callback runSql: function() { throw new Error('not implemented'); }, _getList: function(table) { var sql = 'SELECT * FROM ' + this._escapeDDL + table + this._escapeDDL + ` ORDER BY ${this.escapeDDL('run_on')} DESC, ${this.escapeDDL('name')} DESC`; return this.allAsync(sql); }, _getKV: function(table, key) { var sql = 'SELECT * FROM ' + this._escapeDDL + table + this._escapeDDL + ` WHERE ${this.escapeDDL('key')} = ?`; return this.allAsync(sql, [key]).then(([row]) => row); }, /** * Queries the migrations table * * @param callback */ allLoadedMigrations: function(callback) { var sql = 'SELECT * FROM ' + this._escapeDDL + this.internals.migrationTable + this._escapeDDL + ' ORDER BY run_on DESC, name DESC'; return this.all(sql, callback); }, /** * Queries the seeds table * * @param callback */ allLoadedSeeds: function(callback) { var sql = 'SELECT * FROM ' + this._escapeDDL + this.internals.seedTable + this._escapeDDL + ' ORDER BY run_on DESC, name DESC'; return this.all(sql, callback); }, _deleteEntry: function(table, entry) { var sql = 'DELETE FROM ' + this._escapeDDL + table + this._escapeDDL + ' WHERE name = ?'; return this.runSql(sql, [entry]); }, _deleteExpired: function(table, expiry) { var sql = 'DELETE FROM ' + this._escapeDDL + table + this._escapeDDL + ' WHERE run_on <= ?'; return this.runSql(sql, [expiry]); }, _deleteKV: function(table, key) { var sql = 'DELETE FROM ' + this._escapeDDL + table + this._escapeDDL + ` WHERE ${this.escapeDDL('key')} = ?`; return this.runSql(sql, [key]); }, /** * Deletes a migration * * @param migrationName - The name of the migration to be deleted */ deleteMigration: function(migrationName, callback) { var sql = 'DELETE FROM ' + this._escapeDDL + this.internals.migrationTable + this._escapeDDL + ' WHERE name = ?'; this.runSql(sql, [migrationName], callback); }, /** * Removes the specified keys from the database * * @param table - The table in which the to be deleted values are located * @param ids - array or object * id array - arrayof the to be deleted ids * id object - { table: "name of the table to resolve the ids from", * column: [ * { * name: "name of column", //defaults to id if unset * operator: ">", //defaults to = if unset * searchValue: "12", * searchValue: { table: "source", column: [...] }, * //recursion with objects possible * link: "AND" //defaults to AND if unset * } * ] * } * * @return Promise(runSql) */ remove: function(table, ids, callback) { var sql = 'DELETE FROM ' + this._escapeDDL + table + +this._escapeDDL; // var searchClause = ''; return this.runSql(sql + this.buildWhereClause(ids)).nodeify(callback); }, /** * Builds a where clause out of column objects. * * @param ids - array or object * id array - arrayof the to be deleted ids * id object - { table: "name of the table to resolve the ids from", * column: [ * { * name: "name of column", //defaults to id if unset * operator: ">", //defaults to = if unset * searchValue: "12", * searchValue: { table: "source", column: [...] }, * //recursion with objects possible * link: "AND" //defaults to AND if unset * } * ] * } * * @return string */ buildWhereClause: function(ids) { var searchClause = ''; if (Array.isArray(ids) && typeof ids[0] !== 'object' && ids.length > 1) { searchClause += 'WHERE id IN (' + ids.join(this._escapeString + ',' + this._escapeString) + ')'; } else if ( typeof ids === 'string' || ids.length === 1 || typeof ids === 'number' ) { var id = Array.isArray(ids) ? ids[0] : ids; searchClause += 'WHERE id = ' + this._escapeString + id + this._escapeString; } else if (Array.isArray(ids) && typeof ids[0] === 'object') { var preLink = ''; searchClause = ' WHERE '; for (var column in ids) { var columnKeys = Object.keys(ids[column]); if (columnKeys.length === 1) { var _column = { name: columnKeys[0], value: ids[column][columnKeys[0]] }; column = _column; } else { column = ids[column]; } (column.name = column.name || 'id'), (column.operator = column.operator || '='), (column.link = column.link || 'AND'); if (!column.value) { return Promise.reject( 'column ' + column.name + ' was entered without a search value.' ); } searchClause += ' ' + preLink + ' ' + this._escapeDDL + column.name + this._escapeDDL + ' ' + column.operator; if ( typeof searchValue === 'object' && typeof searchValue.table === 'string' && typeof searchValue.columns === 'object' ) { searchClause += ' (SELECT ' + this._escapeDDL + column.selector + this._escapeDDL + ' FROM ' + this._escapeDDL + column.searchValue.table + this._escapeDDL + this.buildWhereClause(column.searchValue.column) + ')'; } else { searchClause += ' (' + this._escapeString + column.value + this._escapeString + ')'; } preLink = column.link; } } else if (typeof ids === 'object') { var key = Object.keys(ids); var preLink = ''; searchClause += 'WHERE '; for (var i = 0; i < key.length; ++i) { searchClause += preLink + this._escapeDDL + key[i] + this._escapeDDL + ' = ' + this._escapeString + ids[key[i]] + this._escapeString; preLink = ' AND '; } } return searchClause; }, /** * Deletes a seed * * @param seedName - The name of the seed to be deleted */ deleteSeed: function(seedName, callback) { var sql = 'DELETE FROM ' + this._escapeDDL + this.internals.seedTable + this._escapeDDL + ' WHERE name = ?'; this.runSql(sql, [seedName], callback); }, all: function(sql, params, callback) { throw new Error('not implemented'); }, escape: function(str) { if (this._escapeString === "'") return str.replace(/'/g, "''"); else return str.replace(/"/g, '"""'); }, escapeString: function(str) { return this._escapeString + this.escape(str) + this._escapeString; }, escapeDDL: function(str) { return this._escapeDDL + str + this._escapeDDL; } }); Promise.promisifyAll(Base); module.exports = Base;