Automigrade/update

This commit is contained in:
Anatoliy Chakkaev 2011-12-09 19:23:29 +04:00
parent e72db08ad0
commit 4cb7af139d
7 changed files with 292 additions and 5 deletions

View File

@ -1,5 +1,5 @@
test:
@./support/nodeunit/bin/nodeunit test/common_test.js
@ONLY=memory ./support/nodeunit/bin/nodeunit test/*_test.*
.PHONY: test

View File

@ -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 File

@ -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 File

@ -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 File

@ -127,6 +127,10 @@ Validatable.prototype.isValid = function (callback) {
});
if (!async) {
validationsDone();
}
var asyncFail = false;
function done(fail) {
asyncFail = asyncFail || fail;

View File

@ -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;

130
test/migration_test.coffee Normal file
View File

@ -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()