Merge pull request #84 from JosephJNK/master
Added support for migrations to postgres adapter
This commit is contained in:
commit
e1bd896292
File diff suppressed because one or more lines are too long
|
@ -5,6 +5,7 @@ var safeRequire = require('../utils').safeRequire;
|
|||
*/
|
||||
var pg = safeRequire('pg');
|
||||
var BaseSQL = require('../sql');
|
||||
var util = require('util');
|
||||
|
||||
exports.initialize = function initializeSchema(schema, callback) {
|
||||
if (!pg) return;
|
||||
|
@ -187,9 +188,6 @@ PG.prototype.fromDatabase = function (model, data) {
|
|||
var props = this._models[model].properties;
|
||||
Object.keys(data).forEach(function (key) {
|
||||
var val = data[key];
|
||||
if (props[key]) {
|
||||
// if (props[key])
|
||||
}
|
||||
data[key] = val;
|
||||
});
|
||||
return data;
|
||||
|
@ -269,12 +267,24 @@ PG.prototype.toFilter = function (model, filter) {
|
|||
return out;
|
||||
};
|
||||
|
||||
function getTableStatus(model, cb){
|
||||
function decoratedCallback(err, data){
|
||||
data.forEach(function(field){
|
||||
field.Type = mapPostgresDatatypes(field.Type);
|
||||
});
|
||||
cb(err, data);
|
||||
};
|
||||
this.query('SELECT column_name as "Field", udt_name as "Type", is_nullable as "Null", column_default as "Default" FROM information_schema.COLUMNS WHERE table_name = \'' + this.table(model) + '\'', decoratedCallback);
|
||||
};
|
||||
|
||||
PG.prototype.autoupdate = function (cb) {
|
||||
var self = this;
|
||||
var wait = 0;
|
||||
Object.keys(this._models).forEach(function (model) {
|
||||
wait += 1;
|
||||
self.query('SELECT column_name as "Field", udt_name as "Type", is_nullable as "Null", column_default as "Default" FROM information_schema.COLUMNS WHERE table_name = \''+ self.table(model) + '\'', function (err, fields) {
|
||||
var fields;
|
||||
getTableStatus.call(self, model, function(err, fields){
|
||||
if(err) console.log(err);
|
||||
self.alterTable(model, fields, done);
|
||||
});
|
||||
});
|
||||
|
@ -286,64 +296,173 @@ PG.prototype.autoupdate = function (cb) {
|
|||
if (--wait === 0 && cb) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
PG.prototype.isActual = function(cb) {
|
||||
var self = this;
|
||||
var wait = 0;
|
||||
changes = [];
|
||||
Object.keys(this._models).forEach(function (model) {
|
||||
wait += 1;
|
||||
getTableStatus.call(self, model, function(err, fields){
|
||||
changes = changes.concat(getPendingChanges.call(self, model, fields));
|
||||
done(err, changes);
|
||||
});
|
||||
});
|
||||
|
||||
function done(err, fields) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
}
|
||||
if (--wait === 0 && cb) {
|
||||
var actual = (changes.length === 0);
|
||||
cb(null, actual);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
PG.prototype.alterTable = function (model, actualFields, done) {
|
||||
var self = this;
|
||||
var pendingChanges = getPendingChanges.call(self, model, actualFields);
|
||||
applySqlChanges.call(self, model, pendingChanges, done);
|
||||
};
|
||||
|
||||
function getPendingChanges(model, actualFields){
|
||||
var sql = [];
|
||||
var self = this;
|
||||
var m = this._models[model];
|
||||
sql = sql.concat(getColumnsToAdd.call(self, model, actualFields));
|
||||
sql = sql.concat(getPropertiesToModify.call(self, model, actualFields));
|
||||
sql = sql.concat(getColumnsToDrop.call(self, model, actualFields));
|
||||
return sql;
|
||||
};
|
||||
|
||||
function getColumnsToAdd(model, actualFields){
|
||||
var self = this;
|
||||
var m = self._models[model];
|
||||
var propNames = Object.keys(m.properties);
|
||||
var sql = [];
|
||||
|
||||
// change/add new fields
|
||||
propNames.forEach(function (propName) {
|
||||
var found;
|
||||
actualFields.forEach(function (f) {
|
||||
if (f.Field === propName) {
|
||||
found = f;
|
||||
}
|
||||
});
|
||||
|
||||
if (found) {
|
||||
actualize(propName, found);
|
||||
} else {
|
||||
sql.push('ADD COLUMN "' + propName + '" ' + self.propertySettingsSQL(model, propName));
|
||||
var found = searchForPropertyInActual.call(self, propName, actualFields);
|
||||
if(!found && propertyHasNotBeenDeleted.call(self, model, propName)){
|
||||
sql.push(addPropertyToActual.call(self, model, propName));
|
||||
}
|
||||
});
|
||||
return sql;
|
||||
};
|
||||
|
||||
// drop columns
|
||||
function addPropertyToActual(model, propName){
|
||||
var self = this;
|
||||
var p = self._models[model].properties[propName];
|
||||
sqlCommand = 'ADD COLUMN "' + propName + '" ' + datatype(p) + " " + (propertyCanBeNull.call(self, model, propName) ? "" : " NOT NULL");
|
||||
return sqlCommand;
|
||||
};
|
||||
|
||||
function searchForPropertyInActual(propName, actualFields){
|
||||
var found = false;
|
||||
actualFields.forEach(function (f) {
|
||||
var notFound = !~propNames.indexOf(f.Field);
|
||||
if (f.Field === 'id') return;
|
||||
if (notFound || !m.properties[f.Field]) {
|
||||
sql.push('DROP COLUMN "' + f.Field + '"');
|
||||
if (f.Field === propName) {
|
||||
found = f;
|
||||
return;
|
||||
}
|
||||
});
|
||||
return found;
|
||||
};
|
||||
|
||||
function getPropertiesToModify(model, actualFields){
|
||||
var self = this;
|
||||
var sql = [];
|
||||
var m = self._models[model];
|
||||
var propNames = Object.keys(m.properties);
|
||||
var found;
|
||||
propNames.forEach(function (propName) {
|
||||
found = searchForPropertyInActual.call(self, propName, actualFields);
|
||||
if(found && propertyHasNotBeenDeleted.call(self, model, propName)){
|
||||
if (datatypeChanged(propName, found)) {
|
||||
sql.push(modifyDatatypeInActual.call(self, model, propName));
|
||||
}
|
||||
if (nullabilityChanged(propName, found)){
|
||||
sql.push(modifyNullabilityInActual.call(self, model, propName));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (sql.length) {
|
||||
this.query('ALTER TABLE ' + this.tableEscaped(model) + ' ' + sql.join(',\n'), done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
return sql;
|
||||
|
||||
function actualize(propName, oldSettings) {
|
||||
function datatypeChanged(propName, oldSettings){
|
||||
var newSettings = m.properties[propName];
|
||||
if (newSettings && changed(newSettings, oldSettings)) {
|
||||
sql.push('CHANGE COLUMN "' + propName + '" "' + propName + '" ' + self.propertySettingsSQL(model, propName));
|
||||
if(!newSettings) return false;
|
||||
return oldSettings.Type.toLowerCase() !== datatype(newSettings);
|
||||
};
|
||||
|
||||
function nullabilityChanged(propName, oldSettings){
|
||||
var newSettings = m.properties[propName];
|
||||
if(!newSettings) return false;
|
||||
var changed = false;
|
||||
if (oldSettings.Null === 'YES' && (newSettings.allowNull === false || newSettings.null === false)) changed = true;
|
||||
if (oldSettings.Null === 'NO' && !(newSettings.allowNull === false || newSettings.null === false)) changed = true;
|
||||
return changed;
|
||||
};
|
||||
};
|
||||
|
||||
function modifyDatatypeInActual(model, propName) {
|
||||
var self = this;
|
||||
var sqlCommand = 'ALTER COLUMN "' + propName + '" TYPE ' + datatype(self._models[model].properties[propName]);
|
||||
return sqlCommand;
|
||||
};
|
||||
|
||||
function modifyNullabilityInActual(model, propName) {
|
||||
var self = this;
|
||||
var sqlCommand = 'ALTER COLUMN "' + propName + '" ';
|
||||
if(propertyCanBeNull.call(self, model, propName)){
|
||||
sqlCommand = sqlCommand + "DROP ";
|
||||
} else {
|
||||
sqlCommand = sqlCommand + "SET ";
|
||||
}
|
||||
sqlCommand = sqlCommand + "NOT NULL";
|
||||
return sqlCommand;
|
||||
};
|
||||
|
||||
function getColumnsToDrop(model, actualFields){
|
||||
var self = this;
|
||||
var sql = [];
|
||||
actualFields.forEach(function (actualField) {
|
||||
if (actualField.Field === 'id') return;
|
||||
if (actualFieldNotPresentInModel(actualField, model)) {
|
||||
sql.push('DROP COLUMN "' + actualField.Field + '"');
|
||||
}
|
||||
});
|
||||
return sql;
|
||||
|
||||
function actualFieldNotPresentInModel(actualField, model){
|
||||
return !(self._models[model].properties[actualField.Field]);
|
||||
};
|
||||
};
|
||||
|
||||
function applySqlChanges(model, pendingChanges, done){
|
||||
var self = this;
|
||||
if (pendingChanges.length) {
|
||||
var thisQuery = 'ALTER TABLE ' + self.tableEscaped(model);
|
||||
var ranOnce = false;
|
||||
pendingChanges.forEach(function(change){
|
||||
if(ranOnce) thisQuery = thisQuery + ',';
|
||||
thisQuery = thisQuery + ' ' + change;
|
||||
ranOnce = true;
|
||||
});
|
||||
thisQuery = thisQuery + ';';
|
||||
self.query(thisQuery, callback);
|
||||
}
|
||||
|
||||
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;
|
||||
function callback(err, data){
|
||||
if(err) console.log(err);
|
||||
}
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
PG.prototype.propertiesSQL = function (model) {
|
||||
var self = this;
|
||||
var sql = ['"id" SERIAL NOT NULL UNIQUE PRIMARY KEY'];
|
||||
var sql = ['"id" SERIAL PRIMARY KEY'];
|
||||
Object.keys(this._models[model].properties).forEach(function (prop) {
|
||||
sql.push('"' + prop + '" ' + self.propertySettingsSQL(model, prop));
|
||||
});
|
||||
|
@ -351,10 +470,17 @@ PG.prototype.propertiesSQL = function (model) {
|
|||
|
||||
};
|
||||
|
||||
PG.prototype.propertySettingsSQL = function (model, prop) {
|
||||
var p = this._models[model].properties[prop];
|
||||
return datatype(p) + ' ' +
|
||||
(p.allowNull === false || p['null'] === false ? 'NOT NULL' : 'NULL');
|
||||
PG.prototype.propertySettingsSQL = function (model, propName) {
|
||||
var self = this;
|
||||
var p = self._models[model].properties[propName];
|
||||
var result = datatype(p) + ' ';
|
||||
if(!propertyCanBeNull.call(self, model, propName)) result = result + 'NOT NULL ';
|
||||
return result;
|
||||
};
|
||||
|
||||
function propertyCanBeNull(model, propName){
|
||||
var p = this._models[model].properties[propName];
|
||||
return !(p.allowNull === false || p['null'] === false);
|
||||
};
|
||||
|
||||
function escape(val) {
|
||||
|
@ -390,14 +516,32 @@ function escape(val) {
|
|||
function datatype(p) {
|
||||
switch (p.type.name) {
|
||||
case 'String':
|
||||
return 'VARCHAR(' + (p.limit || 255) + ')';
|
||||
return 'varchar';
|
||||
case 'Text':
|
||||
return 'TEXT';
|
||||
return 'text';
|
||||
case 'Number':
|
||||
return 'INTEGER';
|
||||
return 'integer';
|
||||
case 'Date':
|
||||
return 'TIMESTAMP';
|
||||
return 'timestamp';
|
||||
case 'Boolean':
|
||||
return 'BOOLEAN';
|
||||
return 'boolean';
|
||||
default:
|
||||
console.log("Warning: postgres adapter does not explicitly handle type '" + p.type.name +"'");
|
||||
return p.type.toLowerCase();
|
||||
//TODO a default case might not be the safest thing here... postgres has a fair number of extra types though
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function mapPostgresDatatypes(typeName) {
|
||||
//TODO there are a lot of synonymous type names that should go here-- this is just what i've run into so far
|
||||
switch (typeName){
|
||||
case 'int4':
|
||||
return 'integer';
|
||||
default:
|
||||
return typeName;
|
||||
}
|
||||
};
|
||||
|
||||
function propertyHasNotBeenDeleted(model, propName){
|
||||
return !!this._models[model].properties[propName];
|
||||
};
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
juggling = require('../index')
|
||||
Schema = juggling.Schema
|
||||
Text = Schema.Text
|
||||
|
||||
DBNAME = process.env.DBNAME || 'myapp_test' #this db must already exist and will be destroyed
|
||||
DBUSER = process.env.DBUSER || 'root'
|
||||
DBPASS = ''
|
||||
DBENGINE = process.env.DBENGINE || 'postgres'
|
||||
|
||||
require('./spec_helper').init module.exports
|
||||
|
||||
schema = new Schema DBENGINE, database: '', username: DBUSER, password: DBPASS
|
||||
schema.log = (q) -> console.log q
|
||||
|
||||
query = (sql, cb) ->
|
||||
schema.adapter.query sql, cb
|
||||
|
||||
User = schema.define 'User',
|
||||
name: {type: String, default: "guest"}
|
||||
credits: {type: Number, default: 0}
|
||||
|
||||
withBlankDatabase = (cb) ->
|
||||
db = schema.settings.database = DBNAME
|
||||
query 'DROP DATABASE IF EXISTS ' + db, (err) ->
|
||||
query 'CREATE DATABASE ' + db, ->
|
||||
schema.automigrate(cb)
|
||||
|
||||
it 'default values should not interfere with fully specified objects', (test)->
|
||||
withBlankDatabase (err)->
|
||||
test.ok !err, "error while setting up blank database"
|
||||
new User()
|
||||
User.create {name: "Steve", credits: 47}, (err, obj)->
|
||||
console.log "error creating user: #{err}"
|
||||
test.ok !err, "error occurred when saving user with all values specified"
|
||||
test.ok obj.id?, 'saved object has no id'
|
||||
console.log "id: #{obj.id}"
|
||||
test.equals obj.name, "Steve", "User's name didn't save correctly"
|
||||
test.equals obj.credits, 47, "User's credits didn't save correctly"
|
||||
test.done()
|
||||
|
||||
it 'objects should have default values when some fields are unspecified', (test)->
|
||||
User.create {credits: 2}, (err, obj)->
|
||||
console.log "error creating user: #{err}"
|
||||
test.ok !err, "error occurred when saving user with some values unspecified"
|
||||
test.ok obj.id?, 'saved object has no id'
|
||||
test.equals obj.name, "guest", "User's name didn't save correctly"
|
||||
test.equals obj.credits, 2, "User's credits didn't save correctly"
|
||||
User.create {name: "Jeanette Adele McKenzie"}, (err, obj)->
|
||||
console.log "error creating user: #{err}"
|
||||
test.ok !err, "error occurred when saving user with some values unspecified"
|
||||
test.ok obj.id?, 'saved object has no id'
|
||||
test.equals obj.name, "Jeanette Adele McKenzie", "User's name didn't save correctly"
|
||||
test.equals obj.credits, 0, "User's credits didn't save correctly"
|
||||
test.done()
|
||||
|
||||
it 'objects should have default values when all fields are left unspecified', (test)->
|
||||
User.create {}, (err, obj)->
|
||||
console.log "error creating user: #{err}"
|
||||
test.ok !err, "error occurred when saving user with all values specified"
|
||||
test.ok obj.id?, 'saved object has no id'
|
||||
test.equals obj.name, "guest", "User's name didn't save correctly"
|
||||
test.equals obj.credits, 0, "User's credits didn't save correctly"
|
||||
test.done()
|
||||
|
||||
it 'should disconnect when done', (test)->
|
||||
schema.disconnect()
|
||||
test.done()
|
|
@ -0,0 +1,132 @@
|
|||
juggling = require('../index')
|
||||
Schema = juggling.Schema
|
||||
Text = Schema.Text
|
||||
|
||||
DBNAME = process.env.DBNAME || 'myapp_test' #this db must already exist and will be destroyed
|
||||
DBUSER = process.env.DBUSER || 'root'
|
||||
DBPASS = ''
|
||||
DBENGINE = process.env.DBENGINE || 'postgres'
|
||||
|
||||
require('./spec_helper').init module.exports
|
||||
|
||||
schema = new Schema DBENGINE, 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, cb
|
||||
|
||||
getColumnDescriptions = (model, cb)->
|
||||
query "SELECT column_name, data_type, is_nullable, column_default FROM information_schema.COLUMNS WHERE table_name = '#{model}'", (err, res)->
|
||||
if err
|
||||
cb err
|
||||
else
|
||||
fields = {}
|
||||
fields[entry.column_name] = entry for entry in res
|
||||
cb err, fields
|
||||
|
||||
it 'should run migration', (test)->
|
||||
withBlankDatabase (err)->
|
||||
schema.automigrate ->
|
||||
getColumnDescriptions 'User', (err, fields)->
|
||||
test.deepEqual fields,
|
||||
id:
|
||||
column_name: 'id'
|
||||
data_type: 'integer'
|
||||
is_nullable: 'NO'
|
||||
column_default: 'nextval(\'"User_id_seq"\'::regclass)'
|
||||
email:
|
||||
column_name: 'email'
|
||||
data_type: 'character varying'
|
||||
is_nullable: 'NO'
|
||||
column_default: null
|
||||
name:
|
||||
column_name: 'name'
|
||||
data_type: 'character varying'
|
||||
is_nullable: 'YES'
|
||||
column_default: null
|
||||
bio:
|
||||
column_name: 'bio'
|
||||
data_type: 'text'
|
||||
is_nullable: 'YES'
|
||||
column_default: null
|
||||
password:
|
||||
column_name: 'password'
|
||||
data_type: 'character varying'
|
||||
is_nullable: 'YES'
|
||||
column_default: null
|
||||
birthDate:
|
||||
column_name: 'birthDate'
|
||||
data_type: 'timestamp without time zone'
|
||||
is_nullable: 'YES'
|
||||
column_default: null
|
||||
pendingPeriod:
|
||||
column_name: 'pendingPeriod'
|
||||
data_type: 'integer'
|
||||
is_nullable: 'YES'
|
||||
column_default: null
|
||||
createdByAdmin:
|
||||
column_name: 'createdByAdmin'
|
||||
data_type: 'boolean'
|
||||
is_nullable: 'YES'
|
||||
column_default: null
|
||||
test.done()
|
||||
|
||||
it 'should autoupdate', (test) ->
|
||||
getColumnDescriptions 'User', (err, fields)->
|
||||
|
||||
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, "error occurred while creating User: #{err}"
|
||||
userExists (yep) ->
|
||||
test.ok yep, 'userExists returned false'
|
||||
User.defineProperty 'email', type: String
|
||||
User.defineProperty 'createdByAdmin', type: String
|
||||
User.defineProperty 'newProperty', type: Number
|
||||
User.defineProperty 'pendingPeriod', false
|
||||
schema.autoupdate (err) ->
|
||||
getColumnDescriptions 'User', (err, fields) ->
|
||||
# email should now be nullable
|
||||
test.equal fields.email.is_nullable, 'YES', "email's nullability did not change to nullable"
|
||||
# change type of createdByAdmin
|
||||
test.equal fields.createdByAdmin.data_type, 'character varying', "createdByAdmin's data type did not change"
|
||||
# new column should be added
|
||||
test.ok fields.newProperty, 'New column was not added'
|
||||
if fields.newProperty
|
||||
test.equal fields.newProperty.data_type, 'integer', 'New column type is not integer'
|
||||
# pendingPeriod should be dropped
|
||||
test.ok not fields.pendingPeriod, 'pendingPeriod was not dropped'
|
||||
|
||||
# user should still exist
|
||||
userExists (yep) ->
|
||||
test.ok yep, 'user does not still exist after update'
|
||||
test.done()
|
||||
|
||||
it 'should check actuality of schema', (test) ->
|
||||
# drop column
|
||||
User.schema.isActual (err, ok) ->
|
||||
test.ok ok, "User is not actual before schema is modified"
|
||||
User.defineProperty 'email', false
|
||||
User.schema.isActual (err, ok) ->
|
||||
test.ok not ok, "User should not be actual after schema is modified"
|
||||
test.done()
|
||||
|
||||
it 'should disconnect when done', (test)->
|
||||
schema.disconnect()
|
||||
test.done()
|
Loading…
Reference in New Issue