src/databases/MySqlConnection.js
/**
* @module MySqlConnection.js
* @author Joe Groseclose <@benderTheCrime>
* @date 8/23/2015
*/
// System Modules
import mysql from 'mysql';
import {cyan, magenta, gray} from 'chalk';
import $LogProvider from 'angie-log';
// Angie Modules
import BaseDBConnection from './BaseDBConnection';
import {
$$InvalidDatabaseConfigError
} from '../util/$ExceptionsProvider';
const p = process,
DEFAULT_HOST = '127.0.0.1',
DEFAULT_PORT = 3306;
$LogProvider.mysqlInfo = $LogProvider.info.bind(null, 'MySQL');
class MySqlConnection extends BaseDBConnection {
constructor(name, database, destructive, dryRun) {
super(database, destructive, dryRun);
let db = this.database;
if (!db.username) {
throw new $$InvalidDatabaseConfigError(db);
} else if (!this.connection) {
this.name = name || this.database.name || this.database.alias;
this.connection = mysql.createConnection({
host: db.host || DEFAULT_HOST,
port: db.port || DEFAULT_PORT,
user: db.username || '',
password: db.password || '',
database: this.name
});
this.connection.on('error', function() {
if (db.options && db.options.hardErrors) {
throw new $$InvalidDatabaseConfigError(db);
}
});
this.connected = false;
}
}
types(model, key) {
const field = model[ key ];
let type = field.type,
maxLength = '';
if (!type) {
return;
}
if (field.maxLength) {
maxLength = `(${field.maxLength})`;
}
switch (type) {
case 'CharField':
return `VARCHAR${maxLength}`;
// TODO support different size integers: TINY, SMALL, MEDIUM
case 'IntegerField':
return `INTEGER${maxLength}`;
case 'KeyField':
return `INTEGER${maxLength}`;
case 'ForeignKeyField':
return `INTEGER${maxLength}, ADD CONSTRAINT fk_${key} FOREIGN KEY(${key}) ` +
`REFERENCES ${field.rel}(id) ON DELETE CASCADE`;
default:
return undefined;
}
}
connect() {
let me = this;
return new Promise(function(resolve) {
if (me.connected === false) {
me.connection.connect(function(e) {
// TODO add this back in?
if (e) {
// throw new $$DatabaseConnectivityError(me.database);
$LogProvider.error(e);
} else {
me.connected = true;
$LogProvider.mysqlInfo('Connection successful');
}
});
}
resolve();
});
}
disconnect() {
this.connection.end();
this.connected = false;
}
run(query, model) {
let me = this,
name = this.name;
return this.connect().then(function() {
return new Promise(function(resolve) {
$LogProvider.mysqlInfo(
`Query: ${cyan(name)}: ${magenta(query)}`
);
return me.connection.query(query, function(e, rows = []) {
if (e) {
$LogProvider.warn(e);
}
resolve([ rows, e ]);
});
});
}).then(function(args) {
return me.$$queryset(model, query, args[0], args[1]);
});
}
all() {
const query = super.all.apply(this, arguments);
return this.run(query, arguments[0].model);
}
create() {
const query = super.create.apply(this, arguments);
return this.run(query, arguments[0].model);
}
delete() {
const query = super.delete.apply(this, arguments);
return this.run(query, arguments[0].model);
}
update() {
const query = super.update.apply(this, arguments);
return this.run(query, arguments[0].model);
}
raw(query, model) {
return this.run(query, model);
}
sync() {
let me = this;
// Don't worry about the error state, handled by connection
return super.sync().then(function() {
let models = me.models(),
proms = [];
for (let model in models) {
// Fetch models and get model name
let instance = models[ model ],
modelName = instance.name || instance.alias ||
me.$$name(model);
// Run a table creation with an ID for each table
proms.push(me.run(
`CREATE TABLE \`${modelName}\` ` +
'(`id` int(11) NOT NULL AUTO_INCREMENT, ' +
'PRIMARY KEY (`id`) ' +
') ENGINE=InnoDB DEFAULT CHARSET=latin1;'
));
}
return Promise.all(proms).then(function() {
return me.migrate();
});
});
}
migrate() {
let me = this;
return super.migrate().then(function() {
return me.run('SHOW TABLES').then((queryset) => queryset);
}).then(function(queryset) {
// const modelMap = [
// for (model of queryset) model[ `Tables_in_${me.name}` ]
// ];
const modelMap = queryset.map((v) => v[ `Tables_in_${me.name}` ]);
let models = me.models(),
proms = [];
for (let key of modelMap) {
let prom;
if (!models.hasOwnProperty(key)) {
// Don't consolidate, we don't want to look at the fields
if (me.destructive) {
prom = me.run(`DROP TABLE \`${key}\`;`);
}
} else {
const model = models[ key ],
modelName = me.$$name(model.name || model.alias),
fields = model.$fields();
prom = me.run(
`SHOW COLUMNS from \`${modelName}\`;`,
modelName
).then(function(queryset) {
let proms = [];
queryset.forEach(function(v) {
if (
fields.indexOf(v.Field) === -1 &&
v.Field !== 'id' &&
me.destructive
) {
let baseQuery = `ALTER TABLE \`${modelName}\` `,
query =
`${baseQuery}DROP COLUMN \`${v.Field}\`;`,
keyQuery;
if (v.Key) {
keyQuery =
`${baseQuery}DROP FOREIGN KEY ` +
`\`fk_${v.Field}\`;`;
}
if (!me.dryRun) {
let prom;
if (keyQuery) {
prom = me.run(keyQuery).then(
() => me.run(query)
);
} else {
prom = me.run(query);
}
proms.push(prom);
} else {
$LogProvider.mysqlInfo(
`Dry Run Query: ${gray(`${keyQuery} ${query}`)}`
);
}
}
});
// TODO you've got to return if many to many here
fields.forEach(function(v) {
if (model[ v ].type === 'ManyToManyField') {
return;
}
if (queryset.map(($v) => $v.Field).indexOf(v) === -1) {
let query,
$default;
if (model[ v ].default) {
$default = model[ v ].default;
if (typeof $default === 'function') {
$default = $default();
}
}
query =
`ALTER TABLE \`${modelName}\` ADD COLUMN \`${v}\` ` +
`${me.types(model, v)}` +
`${model[ v ].nullable ? '' : ' NOT NULL'}` +
`${model[ v ].unique ? ' UNIQUE' : ''}` +
`${$default ? ` DEFAULT '${$default}'` : ''};`;
if (!me.dryRun) {
proms.push(me.run(query));
} else {
$LogProvider.mysqlInfo(
`Dry Run Query: ${gray(query)}`
);
}
}
});
return Promise.all(proms);
});
}
proms.push(prom);
}
return Promise.all(proms);
}).then(function() {
me.disconnect();
$LogProvider.mysqlInfo(
`Successfully Synced & Migrated ${cyan(me.name)}`
);
p.exit(0);
});
}
}
export default MySqlConnection;