Permalink
Browse files

Automigrade/update

  • Loading branch information...
1 parent e72db08 commit 4cb7af139d11d165a926f588162487d72d30ec88 @1602 committed Dec 9, 2011
Showing with 292 additions and 5 deletions.
  1. +1 −1 Makefile
  2. +5 −2 lib/abstract-class.js
  3. +120 −0 lib/adapters/mysql.js
  4. +18 −2 lib/schema.js
  5. +4 −0 lib/validatable.js
  6. +14 −0 test/common_test.js
  7. +130 −0 test/migration_test.coffee
View
@@ -1,5 +1,5 @@
test:
- @./support/nodeunit/bin/nodeunit test/common_test.js
+ @ONLY=memory ./support/nodeunit/bin/nodeunit test/*_test.*
.PHONY: test
View
@@ -85,6 +85,10 @@ function AbstractClass(data) {
this.trigger("initialize");
};
+AbstractClass.defineProperty = function (prop, params) {
+ this.schema.defineProperty(this.modelName, prop, params);
+};
+
/**
* @param data [optional]
* @param callback(err, obj)
@@ -412,10 +416,9 @@ AbstractClass.prototype.reset = function () {
AbstractClass.hasMany = function (anotherClass, params) {
var methodName = params.as; // or pluralize(anotherClass.modelName)
var fk = params.foreignKey;
- // console.log(this.modelName, 'has many', anotherClass.modelName, 'as', params.as, 'queried by', params.foreignKey);
// each instance of this class should have method named
// pluralize(anotherClass.modelName)
- // which is actually just anotherClass.all({thisModelNameId: this.id}, cb);
+ // which is actually just anotherClass.all({where: {thisModelNameId: this.id}}, cb);
defineScope(this.prototype, anotherClass, methodName, function () {
var x = {};
x[fk] = this.id;
View
@@ -27,9 +27,14 @@ MySQL.prototype.define = function (descr) {
this._models[descr.model.modelName] = descr;
};
+MySQL.prototype.defineProperty = function (model, prop, params) {
+ this._models[model].properties[prop] = params;
+};
+
MySQL.prototype.query = function (sql, callback) {
var time = Date.now();
var log = this.log;
+ if (typeof callback !== 'function') throw new Error('callback should be a function');
this.client.query(sql, function (err, data) {
log(sql, time);
callback(err, data);
@@ -191,3 +196,118 @@ MySQL.prototype.disconnect = function disconnect() {
this.client.end();
};
+MySQL.prototype.automigrate = function (cb) {
+ var self = this;
+ var wait = 0;
+ Object.keys(this._models).forEach(function (model) {
+ wait += 1;
+ self.dropTable(model, function () {
+ self.createTable(model, function (err) {
+ if (err) console.log(err);
+ done();
+ });
+ });
+ });
+
+ function done() {
+ if (--wait === 0 && cb) {
+ cb();
+ }
+ }
+};
+
+MySQL.prototype.autoupdate = function (cb) {
+ var self = this;
+ var wait = 0;
+ Object.keys(this._models).forEach(function (model) {
+ wait += 1;
+ self.query('SHOW FIELDS FROM ' + model, function (err, fields) {
+ self.alterTable(model, fields, done);
+ });
+ });
+
+ function done(err) {
+ if (err) {
+ console.log(err);
+ }
+ if (--wait === 0 && cb) {
+ cb();
+ }
+ }
+};
+
+MySQL.prototype.alterTable = function (model, actualFields, done) {
+ var self = this;
+ var m = this._models[model];
+ var propNames = Object.keys(m.properties);
+ var sql = [];
+ actualFields.forEach(function (f) {
+ if (f.Field !== 'id') {
+ actualize(f.Field, f);
+ }
+ });
+
+ if (sql.length) {
+ this.query('ALTER TABLE `' + model + '` ' + sql.join(',\n'), done);
+ } else {
+ done();
+ }
+
+ function actualize(propName, oldSettings) {
+ var newSettings = m.properties[propName];
+ if (!newSettings) {
+ sql.push('ADD COLUMN `' + propName + '` ' + self.propertySettingsSQL(model, propName));
+ } else if (changed(newSettings, oldSettings)) {
+ sql.push('CHANGE COLUMN `' + propName + '` `' + propName + '` ' + self.propertySettingsSQL(model, propName));
+ }
+ }
+
+ function changed(newSettings, oldSettings) {
+ if (oldSettings.Null === 'YES' && (newSettings.allowNull === false || newSettings.null === false)) return true;
+ if (oldSettings.Null === 'NO' && !(newSettings.allowNull === false || newSettings.null === false)) return true;
+ if (oldSettings.Type.toUpperCase() !== datatype(newSettings)) return true;
+ return false;
+ }
+};
+
+MySQL.prototype.dropTable = function (model, cb) {
+ this.query('DROP TABLE IF EXISTS ' + model, cb);
+};
+
+MySQL.prototype.createTable = function (model, cb) {
+ this.query('CREATE TABLE ' + model +
+ ' (\n ' + this.propertiesSQL(model) + '\n)', cb);
+};
+
+MySQL.prototype.propertiesSQL = function (model) {
+ var self = this;
+ var sql = ['`id` INT(11) NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY'];
+ Object.keys(this._models[model].properties).forEach(function (prop) {
+ sql.push('`' + prop + '` ' + self.propertySettingsSQL(model, prop));
+ });
+ return sql.join(',\n ');
+
+};
+
+MySQL.prototype.propertySettingsSQL = function (model, prop) {
+ var p = this._models[model].properties[prop];
+ return datatype(p) + ' ' +
+ (p.allowNull === false || p['null'] === false ? 'NOT NULL' : 'NULL') +
+ ''; // (p.index ? ' KEY ix' + model + '_' + prop : '');
+};
+
+function datatype(p) {
+ switch (p.type.name) {
+ case 'String':
+ return 'VARCHAR(' + (p.limit || 255) + ')';
+ case 'Text':
+ return 'TEXT';
+ case 'Number':
+ return 'INT(11)';
+ case 'Date':
+ return 'DATETIME';
+ case 'Boolean':
+ return 'TINYINT(1)';
+ }
+}
+
View
@@ -75,12 +75,28 @@ function Text() {
}
Schema.Text = Text;
+Schema.prototype.defineProperty = function (model, prop, params) {
+ this.definitions[model].properties[prop] = params;
+ if (this.adapter.defineProperty) {
+ this.adapter.defineProperty(model, prop, params);
+ }
+};
+
Schema.prototype.automigrate = function (cb) {
this.freeze();
if (this.adapter.automigrate) {
this.adapter.automigrate(cb);
- } else {
- cb && cb();
+ } else if (cb) {
+ cb();
+ }
+};
+
+Schema.prototype.autoupdate = function (cb) {
+ this.freeze();
+ if (this.adapter.autoupdate) {
+ this.adapter.autoupdate(cb);
+ } else if (cb) {
+ cb();
}
};
View
@@ -127,6 +127,10 @@ Validatable.prototype.isValid = function (callback) {
});
+ if (!async) {
+ validationsDone();
+ }
+
var asyncFail = false;
function done(fail) {
asyncFail = asyncFail || fail;
View
@@ -68,6 +68,14 @@ function testOrm(schema) {
// user.posts.create(data) // build and save
// user.posts.find
+ // User.hasOne('latestPost', {model: Post, foreignKey: 'postId'});
+
+ // User.hasOne(Post, {as: 'latestPost', foreignKey: 'latestPostId'});
+ // creates instance methods:
+ // user.latestPost()
+ // user.latestPost.build(data)
+ // user.latestPost.create(data)
+
Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
// creates instance methods:
// post.author(callback) -- getter when called with function
@@ -309,6 +317,12 @@ function testOrm(schema) {
});
});
+ // it('should handle hasOne relationship', function (test) {
+ // User.create(function (err, u) {
+ // if (err) return console.log(err);
+ // });
+ // });
+
it('should support scopes', function (test) {
var wait = 2;
View
@@ -0,0 +1,130 @@
+juggling = require('../index')
+Schema = juggling.Schema
+Text = Schema.Text
+
+DBNAME = 'migrationtest'
+DBUSER = 'root'
+DBPASS = ''
+
+require('./spec_helper').init module.exports
+
+schema = new Schema 'mysql', database: '', username: DBUSER, password: DBPASS
+schema.log = (q) -> console.log q
+
+query = (sql, cb) ->
+ schema.adapter.query sql, cb
+
+User = schema.define 'User',
+ email: { type: String, null: false, index: true }
+ name: String
+ bio: Text
+ password: String
+ birthDate: Date
+ pendingPeriod: Number
+ createdByAdmin: Boolean
+
+withBlankDatabase = (cb) ->
+ db = schema.settings.database = DBNAME
+ query 'DROP DATABASE IF EXISTS ' + db, (err) ->
+ query 'CREATE DATABASE ' + db, (err) ->
+ query 'USE '+ db, cb
+
+getFields = (model, cb) ->
+ query 'SHOW FIELDS FROM ' + model, (err, res) ->
+ if err
+ cb err
+ else
+ fields = {}
+ res.forEach (field) -> fields[field.Field] = field
+ cb err, fields
+
+it 'should run migration', (test) ->
+ withBlankDatabase (err) ->
+ schema.automigrate ->
+ getFields 'User', (err, fields) ->
+ test.deepEqual fields,
+ id:
+ Field: 'id'
+ Type: 'int(11)'
+ Null: 'NO'
+ Key: 'PRI'
+ Default: null
+ Extra: 'auto_increment'
+ email:
+ Field: 'email'
+ Type: 'varchar(255)'
+ Null: 'NO'
+ Key: ''
+ Default: null
+ Extra: ''
+ name:
+ Field: 'name'
+ Type: 'varchar(255)'
+ Null: 'YES'
+ Key: ''
+ Default: null
+ Extra: ''
+ bio:
+ Field: 'bio'
+ Type: 'text'
+ Null: 'YES'
+ Key: ''
+ Default: null
+ Extra: ''
+ password:
+ Field: 'password'
+ Type: 'varchar(255)'
+ Null: 'YES'
+ Key: ''
+ Default: null
+ Extra: ''
+ birthDate:
+ Field: 'birthDate'
+ Type: 'datetime'
+ Null: 'YES'
+ Key: ''
+ Default: null
+ Extra: ''
+ pendingPeriod:
+ Field: 'pendingPeriod'
+ Type: 'int(11)'
+ Null: 'YES'
+ Key: ''
+ Default: null
+ Extra: ''
+ createdByAdmin:
+ Field: 'createdByAdmin'
+ Type: 'tinyint(1)'
+ Null: 'YES'
+ Key: ''
+ Default: null
+ Extra: ''
+
+ test.done()
+
+it 'should autoupgrade', (test) ->
+ userExists = (cb) ->
+ query 'SELECT * FROM User', (err, res) ->
+ cb(not err and res[0].email == 'test@example.com')
+
+ User.create email: 'test@example.com', (err, user) ->
+ test.ok not err
+ userExists (yep) ->
+ test.ok yep
+ User.defineProperty 'email', type: String
+ User.defineProperty 'name', type: String, limit: 50
+ User.defineProperty 'newProperty', type: Number
+ schema.autoupdate (err) ->
+ getFields 'User', (err, fields) ->
+ test.equal fields.email.Null, 'YES'
+ test.equal fields.name.Type, 'varchar(50)'
+ test.ok fields.newProperty
+ if fields.newProperty
+ test.equal fields.newProperty.Type, 'int(11)'
+ userExists (yep) ->
+ test.ok yep
+ test.done()
+
+it 'should disconnect when done', (test) ->
+ schema.disconnect()
+ test.done()

0 comments on commit 4cb7af1

Please sign in to comment.