diff --git a/.travis.yml b/.travis.yml index 5269a4cb..a33e0781 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,8 @@ language: node_js node_js: - 0.6 - - 0.8.12 + - 0.8 + - 0.9 services: - - mongodb - - redis-server - neo4j - couchdb -before_install: - - git submodule init && git submodule --quiet update - - ./support/ci/neo4j.sh -before_script: - - "mysql -e 'create database myapp_test;'" - - "psql -c 'create database myapp_test;' -U postgres" - - mongo mydb_test --eval 'db.addUser("travis", "test");' - - curl -X PUT localhost:5984/nano-test \ No newline at end of file diff --git a/README.md b/README.md index 0608a465..98afd493 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ ## About [](http://travis-ci.org/#!/1602/jugglingdb) JugglingDB is cross-db ORM for nodejs, providing **common interface** to access most popular database formats. -Currently supported are: mysql, mongodb, redis, neo4j and js-memory-storage (yep, +Currently supported are: mysql, sqlite3, postgres, couchdb, mongodb, redis, neo4j +and js-memory-storage (yep, self-written engine for test-usage only). You can add your favorite database adapter, checkout one of the existing adapters to learn how, it's super-easy, I guarantee. @@ -15,11 +16,17 @@ existing adapters to learn how, it's super-easy, I guarantee. - Make sure all tests pass (`npm test` command) - Feel free to vote and comment on cards (tickets/issues), if you want to join team -- send me a message with your email. +If you want to create your own jugglingdb adapter, you should publish your +adapter package with name `jugglingdb-ADAPTERNAME`. Creating adapter is simple, +check [jugglingdb-redis](/1602/jugglingdb-redis) for example. JugglingDB core +exports common tests each adapter should pass, you could create your adapter in +TDD style, check that adapter pass all tests defined in `test/common_test.js`. + ## Usage ```javascript var Schema = require('jugglingdb').Schema; -var schema = new Schema('redis2', {port: 6379}); //port number depends on your configuration +var schema = new Schema('redis', {port: 6379}); //port number depends on your configuration // define models var Post = schema.define('Post', { title: { type: String, length: 255 }, diff --git a/lib/adapters/mysql.js b/lib/adapters/mysql.js deleted file mode 100644 index 8c47debf..00000000 --- a/lib/adapters/mysql.js +++ /dev/null @@ -1,536 +0,0 @@ -var safeRequire = require('../utils').safeRequire; - -/** - * Module dependencies - */ -var mysql = safeRequire('mysql'); -var BaseSQL = require('../sql'); - -exports.initialize = function initializeSchema(schema, callback) { - if (!mysql) return; - - var s = schema.settings; - schema.client = mysql.createConnection({ - host: s.host || 'localhost', - port: s.port || 3306, - user: s.username, - password: s.password, - debug: s.debug, - socketPath: s.socketPath - }); - - schema.adapter = new MySQL(schema.client); - schema.adapter.schema = schema; - // schema.client.query('SET TIME_ZONE = "+04:00"', callback); - schema.client.query('USE `' + s.database + '`', function (err) { - if (err && err.message.match(/^unknown database/i)) { - var dbName = s.database; - schema.client.query('CREATE DATABASE ' + dbName, function (error) { - if (!error) { - schema.client.query('USE ' + s.database, callback); - } else { - throw error; - } - }); - } else callback(); - }); -}; - -/** - * MySQL adapter - */ -function MySQL(client) { - this._models = {}; - this.client = client; -} - -require('util').inherits(MySQL, BaseSQL); - -MySQL.prototype.query = function (sql, callback) { - if (!this.schema.connected) { - return this.schema.on('connected', function () { - this.query(sql, callback); - }.bind(this)); - } - var client = this.client; - 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) { - if (err && err.message.match(/^unknown database/i)) { - var dbName = err.message.match(/^unknown database '(.*?)'/i)[1]; - client.query('CREATE DATABASE ' + dbName, function (error) { - if (!error) { - client.query(sql, callback); - } else { - callback(err); - } - }); - return; - } - if (log) log(sql, time); - callback(err, data); - }); -}; - -/** - * Must invoke callback(err, id) - */ -MySQL.prototype.create = function (model, data, callback) { - var fields = this.toFields(model, data); - var sql = 'INSERT INTO ' + this.tableEscaped(model); - if (fields) { - sql += ' SET ' + fields; - } else { - sql += ' VALUES ()'; - } - this.query(sql, function (err, info) { - callback(err, info && info.insertId); - }); -}; - -MySQL.prototype.updateOrCreate = function (model, data, callback) { - var mysql = this; - var fieldsNames = []; - var fieldValues = []; - var combined = []; - var props = this._models[model].properties; - Object.keys(data).forEach(function (key) { - if (props[key] || key === 'id') { - var k = '`' + key + '`'; - var v; - if (key !== 'id') { - v = mysql.toDatabase(props[key], data[key]); - } else { - v = data[key]; - } - fieldsNames.push(k); - fieldValues.push(v); - if (key !== 'id') combined.push(k + ' = ' + v); - } - }); - - var sql = 'INSERT INTO ' + this.tableEscaped(model); - sql += ' (' + fieldsNames.join(', ') + ')'; - sql += ' VALUES (' + fieldValues.join(', ') + ')'; - sql += ' ON DUPLICATE KEY UPDATE ' + combined.join(', '); - - this.query(sql, function (err, info) { - if (!err && info && info.insertId) { - data.id = info.insertId; - } - callback(err, data); - }); -}; - -MySQL.prototype.toFields = function (model, data) { - var fields = []; - var props = this._models[model].properties; - Object.keys(data).forEach(function (key) { - if (props[key]) { - fields.push('`' + key.replace(/\./g, '`.`') + '` = ' + this.toDatabase(props[key], data[key])); - } - }.bind(this)); - return fields.join(','); -}; - -function dateToMysql(val) { - return val.getUTCFullYear() + '-' + - fillZeros(val.getUTCMonth() + 1) + '-' + - fillZeros(val.getUTCDate()) + ' ' + - fillZeros(val.getUTCHours()) + ':' + - fillZeros(val.getUTCMinutes()) + ':' + - fillZeros(val.getUTCSeconds()); - - function fillZeros(v) { - return v < 10 ? '0' + v : v; - } -} - -MySQL.prototype.toDatabase = function (prop, val) { - if (val === null) return 'NULL'; - if (val.constructor.name === 'Object') { - var operator = Object.keys(val)[0] - val = val[operator]; - if (operator === 'between') { - return this.toDatabase(prop, val[0]) + - ' AND ' + - this.toDatabase(prop, val[1]); - } else if (operator == 'inq' || operator == 'nin') { - if (!(val.propertyIsEnumerable('length')) && typeof val === 'object' && typeof val.length === 'number') { //if value is array - for (var i = 0; i < val.length; i++) { - val[i] = this.client.escape(val[i]); - } - return val.join(','); - } else { - return val; - } - } - } - if (!prop) return val; - if (prop.type.name === 'Number') return val; - if (prop.type.name === 'Date') { - if (!val) return 'NULL'; - if (!val.toUTCString) { - val = new Date(val); - } - return '"' + dateToMysql(val) + '"'; - } - if (prop.type.name == "Boolean") return val ? 1 : 0; - return this.client.escape(val.toString()); -}; - -MySQL.prototype.fromDatabase = function (model, data) { - if (!data) return null; - var props = this._models[model].properties; - Object.keys(data).forEach(function (key) { - var val = data[key]; - if (props[key]) { - if (props[key].type.name === 'Date' && val !== null) { - val = new Date(val.toString().replace(/GMT.*$/, 'GMT')); - } - } - data[key] = val; - }); - return data; -}; - -MySQL.prototype.escapeName = function (name) { - return '`' + name.replace(/\./g, '`.`') + '`'; -}; - -MySQL.prototype.all = function all(model, filter, callback) { - - var sql = 'SELECT * FROM ' + this.tableEscaped(model); - var self = this; - var props = this._models[model].properties; - - if (filter) { - - if (filter.where) { - sql += ' ' + buildWhere(filter.where); - } - - if (filter.order) { - sql += ' ' + buildOrderBy(filter.order); - } - - if (filter.limit) { - sql += ' ' + buildLimit(filter.limit, filter.offset || 0); - } - - } - - this.query(sql, function (err, data) { - if (err) { - return callback(err, []); - } - callback(null, data.map(function (obj) { - return self.fromDatabase(model, obj); - })); - }.bind(this)); - - return sql; - - function buildWhere(conds) { - var cs = []; - Object.keys(conds).forEach(function (key) { - var keyEscaped = '`' + key.replace(/\./g, '`.`') + '`' - var val = self.toDatabase(props[key], conds[key]); - if (conds[key] === null) { - cs.push(keyEscaped + ' IS NULL'); - } else if (conds[key].constructor.name === 'Object') { - var condType = Object.keys(conds[key])[0]; - var sqlCond = keyEscaped; - if ((condType == 'inq' || condType == 'nin') && val.length == 0) { - cs.push(condType == 'inq' ? 0 : 1); - return true; - } - switch (condType) { - case 'gt': - sqlCond += ' > '; - break; - case 'gte': - sqlCond += ' >= '; - break; - case 'lt': - sqlCond += ' < '; - break; - case 'lte': - sqlCond += ' <= '; - break; - case 'between': - sqlCond += ' BETWEEN '; - break; - case 'inq': - sqlCond += ' IN '; - break; - case 'nin': - sqlCond += ' NOT IN '; - break; - case 'neq': - sqlCond += ' != '; - break; - } - sqlCond += (condType == 'inq' || condType == 'nin') ? '(' + val + ')' : val; - cs.push(sqlCond); - } else { - cs.push(keyEscaped + ' = ' + val); - } - }); - if (cs.length === 0) { - return ''; - } - return 'WHERE ' + cs.join(' AND '); - } - - function buildOrderBy(order) { - if (typeof order === 'string') order = [order]; - return 'ORDER BY ' + order.join(', '); - } - - function buildLimit(limit, offset) { - return 'LIMIT ' + (offset ? (offset + ', ' + limit) : limit); - } - -}; - -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 ' + self.tableEscaped(model), function (err, fields) { - self.query('SHOW INDEXES FROM ' + self.tableEscaped(model), function (err, indexes) { - if (!err && fields.length) { - self.alterTable(model, fields, indexes, done); - } else { - self.createTable(model, done); - } - }); - }); - }); - - function done(err) { - if (err) { - console.log(err); - } - if (--wait === 0 && cb) { - cb(); - } - } -}; - -MySQL.prototype.isActual = function (cb) { - var ok = false; - 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.query('SHOW INDEXES FROM ' + model, function (err, indexes) { - self.alterTable(model, fields, indexes, done, true); - }); - }); - }); - - function done(err, needAlter) { - if (err) { - console.log(err); - } - ok = ok || needAlter; - if (--wait === 0 && cb) { - cb(null, !ok); - } - } -}; - -MySQL.prototype.alterTable = function (model, actualFields, actualIndexes, done, checkOnly) { - var self = this; - var m = this._models[model]; - var propNames = Object.keys(m.properties).filter(function (name) { - return !!m.properties[name]; - }); - var indexNames = m.settings.indexes ? Object.keys(m.settings.indexes).filter(function (name) { - return !!m.settings.indexes[name]; - }) : []; - var sql = []; - var ai = {}; - - if (actualIndexes) { - actualIndexes.forEach(function (i) { - var name = i.Key_name; - if (!ai[name]) { - ai[name] = { - info: i, - columns: [] - }; - } - ai[name].columns[i.Seq_in_index - 1] = i.Column_name; - }); - } - var aiNames = Object.keys(ai); - - // change/add new fields - propNames.forEach(function (propName) { - if (propName === 'id') return; - 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)); - } - }); - - // drop columns - 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 + '`'); - } - }); - - // remove indexes - aiNames.forEach(function (indexName) { - if (indexName === 'id' || indexName === 'PRIMARY') return; - if (indexNames.indexOf(indexName) === -1 && !m.properties[indexName] || m.properties[indexName] && !m.properties[indexName].index) { - sql.push('DROP INDEX `' + indexName + '`'); - } else { - // first: check single (only type and kind) - if (m.properties[indexName] && !m.properties[indexName].index) { - // TODO - return; - } - // second: check multiple indexes - var orderMatched = true; - if (indexNames.indexOf(indexName) !== -1) { - m.settings.indexes[indexName].columns.split(/,\s*/).forEach(function (columnName, i) { - if (ai[indexName].columns[i] !== columnName) orderMatched = false; - }); - } - if (!orderMatched) { - sql.push('DROP INDEX `' + indexName + '`'); - delete ai[indexName]; - } - } - }); - - // add single-column indexes - propNames.forEach(function (propName) { - var i = m.properties[propName].index; - if (!i) { - return; - } - var found = ai[propName] && ai[propName].info; - if (!found) { - var type = ''; - var kind = ''; - if (i.type) { - type = 'USING ' + i.type; - } - if (i.kind) { - // kind = i.kind; - } - if (kind && type) { - sql.push('ADD ' + kind + ' INDEX `' + propName + '` (`' + propName + '`) ' + type); - } else { - sql.push('ADD ' + kind + ' INDEX `' + propName + '` ' + type + ' (`' + propName + '`) '); - } - } - }); - - // add multi-column indexes - indexNames.forEach(function (indexName) { - var i = m.settings.indexes[indexName]; - var found = ai[indexName] && ai[indexName].info; - if (!found) { - var type = ''; - var kind = ''; - if (i.type) { - type = 'USING ' + i.kind; - } - if (i.kind) { - kind = i.kind; - } - if (kind && type) { - sql.push('ADD ' + kind + ' INDEX `' + indexName + '` (' + i.columns + ') ' + type); - } else { - sql.push('ADD ' + kind + ' INDEX ' + type + ' `' + indexName + '` (' + i.columns + ')'); - } - } - }); - - if (sql.length) { - var query = 'ALTER TABLE ' + self.tableEscaped(model) + ' ' + sql.join(',\n'); - if (checkOnly) { - done(null, true, {statements: sql, query: query}); - } else { - this.query(query, done); - } - } else { - done(); - } - - function actualize(propName, oldSettings) { - var newSettings = m.properties[propName]; - if (newSettings && 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.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) { - if (prop === 'id') return; - 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'); -}; - -function datatype(p) { - var dt = ''; - switch (p.type.name) { - default: - case 'String': - case 'JSON': - dt = 'VARCHAR(' + (p.limit || 255) + ')'; - break; - case 'Text': - dt = 'TEXT'; - break; - case 'Number': - dt = 'INT(' + (p.limit || 11) + ')'; - break; - case 'Date': - dt = 'DATETIME'; - break; - case 'Boolean': - dt = 'TINYINT(1)'; - break; - } - return dt; -} - diff --git a/lib/adapters/postgres.js b/lib/adapters/postgres.js deleted file mode 100644 index 4eefe8ac..00000000 --- a/lib/adapters/postgres.js +++ /dev/null @@ -1,579 +0,0 @@ -var safeRequire = require('../utils').safeRequire; - -/** - * Module dependencies - */ -var pg = safeRequire('pg'); -var BaseSQL = require('../sql'); -var util = require('util'); - -exports.initialize = function initializeSchema(schema, callback) { - if (!pg) return; - - var Client = pg.Client; - var s = schema.settings; - schema.client = new Client(s.url ? s.url : { - host: s.host || 'localhost', - port: s.port || 5432, - user: s.username, - password: s.password, - database: s.database, - debug: s.debug - }); - schema.adapter = new PG(schema.client); - - schema.adapter.connect(callback); -}; - -function PG(client) { - this._models = {}; - this.client = client; -} - -require('util').inherits(PG, BaseSQL); - -PG.prototype.connect = function (callback) { - this.client.connect(function (err) { - if (!err){ - callback(); - }else{ - console.error(err); - throw err; - } - }); -}; - -PG.prototype.query = function (sql, callback) { - var time = Date.now(); - var log = this.log; - this.client.query(sql, function (err, data) { - if (log) log(sql, time); - callback(err, data ? data.rows : null); - }); -}; - -/** - * Must invoke callback(err, id) - */ -PG.prototype.create = function (model, data, callback) { - var fields = this.toFields(model, data, true); - var sql = 'INSERT INTO ' + this.tableEscaped(model) + ''; - if (fields) { - sql += ' ' + fields; - } else { - sql += ' VALUES ()'; - } - sql += ' RETURNING id'; - this.query(sql, function (err, info) { - if (err) return callback(err); - callback(err, info && info[0] && info[0].id); - }); -}; - -PG.prototype.updateOrCreate = function (model, data, callback) { - var pg = this; - var fieldsNames = []; - var fieldValues = []; - var combined = []; - var props = this._models[model].properties; - Object.keys(data).forEach(function (key) { - if (props[key] || key === 'id') { - var k = '"' + key + '"'; - var v; - if (key !== 'id') { - v = pg.toDatabase(props[key], data[key]); - } else { - v = data[key]; - } - fieldsNames.push(k); - fieldValues.push(v); - if (key !== 'id') combined.push(k + ' = ' + v); - } - }); - - var sql = 'UPDATE ' + this.tableEscaped(model); - sql += ' SET ' + combined + ' WHERE id = ' + data.id + ';'; - sql += ' INSERT INTO ' + this.tableEscaped(model); - sql += ' (' + fieldsNames.join(', ') + ')'; - sql += ' SELECT ' + fieldValues.join(', ') - sql += ' WHERE NOT EXISTS (SELECT 1 FROM ' + this.tableEscaped(model); - sql += ' WHERE id = ' + data.id + ') RETURNING id'; - - this.query(sql, function (err, info) { - if (!err && info && info[0] && info[0].id) { - data.id = info[0].id; - } - callback(err, data); - }); -}; - -PG.prototype.toFields = function (model, data, forCreate) { - var fields = []; - var props = this._models[model].properties; - - if(forCreate){ - var columns = []; - Object.keys(data).forEach(function (key) { - if (props[key]) { - if (key === 'id') return; - columns.push('"' + key + '"'); - fields.push(this.toDatabase(props[key], data[key])); - } - }.bind(this)); - return '(' + columns.join(',') + ') VALUES ('+fields.join(',')+')'; - }else{ - Object.keys(data).forEach(function (key) { - if (props[key]) { - fields.push('"' + key + '" = ' + this.toDatabase(props[key], data[key])); - } - }.bind(this)); - return fields.join(','); - } -}; - -function dateToPostgres(val) { - return [ - val.getUTCFullYear(), - fz(val.getUTCMonth() + 1), - fz(val.getUTCDate()) - ].join('-') + ' ' + [ - fz(val.getUTCHours()), - fz(val.getUTCMinutes()), - fz(val.getUTCSeconds()) - ].join(':'); - - function fz(v) { - return v < 10 ? '0' + v : v; - } -} - -PG.prototype.toDatabase = function (prop, val) { - if (val === null) { - // Postgres complains with NULLs in not null columns - // If we have an autoincrement value, return DEFAULT instead - if( prop.autoIncrement ) { - return 'DEFAULT'; - } - else { - return 'NULL'; - } - } - if (val.constructor.name === 'Object') { - var operator = Object.keys(val)[0] - val = val[operator]; - if (operator === 'between') { - return this.toDatabase(prop, val[0]) + ' AND ' + this.toDatabase(prop, val[1]); - } - if (operator === 'inq' || operator === 'nin') { - for (var i = 0; i < val.length; i++) { - val[i] = escape(val[i]); - } - return val.join(','); - } - } - if (prop.type.name === 'Number') { - if (!val && val!=0) { - if( prop.autoIncrement ) { - return 'DEFAULT'; - } - else { - return 'NULL'; - } - } - return val - }; - - if (prop.type.name === 'Date') { - if (!val) { - if( prop.autoIncrement ) { - return 'DEFAULT'; - } - else { - return 'NULL'; - } - } - if (!val.toUTCString) { - val = new Date(val); - } - return escape(dateToPostgres(val)); - } - return escape(val.toString()); - -}; - -PG.prototype.fromDatabase = function (model, data) { - if (!data) return null; - var props = this._models[model].properties; - Object.keys(data).forEach(function (key) { - var val = data[key]; - data[key] = val; - }); - return data; -}; - -PG.prototype.escapeName = function (name) { - return '"' + name.replace(/\./g, '"."') + '"'; -}; - -PG.prototype.all = function all(model, filter, callback) { - this.query('SELECT * FROM ' + this.tableEscaped(model) + ' ' + this.toFilter(model, filter), function (err, data) { - if (err) { - return callback(err, []); - } - callback(err, data); - }.bind(this)); -}; - -PG.prototype.toFilter = function (model, filter) { - if (filter && typeof filter.where === 'function') { - return filter(); - } - if (!filter) return ''; - var props = this._models[model].properties; - var out = ''; - if (filter.where) { - var fields = []; - var conds = filter.where; - Object.keys(conds).forEach(function (key) { - if (filter.where[key] && filter.where[key].constructor.name === 'RegExp') { - return; - } - if (props[key]) { - var filterValue = this.toDatabase(props[key], filter.where[key]); - if (filterValue === 'NULL') { - fields.push('"' + key + '" IS ' + filterValue); - } else if (conds[key].constructor.name === 'Object') { - var condType = Object.keys(conds[key])[0]; - var sqlCond = '"' + key + '"'; - if ((condType == 'inq' || condType == 'nin') && filterValue.length == 0) { - fields.push(condType == 'inq' ? 'FALSE' : 'TRUE'); - return true; - } - switch (condType) { - case 'gt': - sqlCond += ' > '; - break; - case 'gte': - sqlCond += ' >= '; - break; - case 'lt': - sqlCond += ' < '; - break; - case 'lte': - sqlCond += ' <= '; - break; - case 'between': - sqlCond += ' BETWEEN '; - break; - case 'inq': - sqlCond += ' IN '; - break; - case 'nin': - sqlCond += ' NOT IN '; - break; - case 'neq': - sqlCond += ' != '; - break; - } - sqlCond += (condType == 'inq' || condType == 'nin') ? '(' + filterValue + ')' : filterValue; - fields.push(sqlCond); - } else { - fields.push('"' + key + '" = ' + filterValue); - } - } - }.bind(this)); - if (fields.length) { - out += ' WHERE ' + fields.join(' AND '); - } - } - - if (filter.order) { - out += ' ORDER BY ' + filter.order; - } - - if (filter.limit) { - out += ' LIMIT ' + filter.limit + ' OFFSET ' + (filter.offset || '0'); - } - - 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; - var fields; - getTableStatus.call(self, model, function(err, fields){ - if(err) console.log(err); - self.alterTable(model, fields, done); - }); - }); - - function done(err) { - if (err) { - console.log(err); - } - 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; - 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 = []; - propNames.forEach(function (propName) { - if (propName === 'id') return; - var found = searchForPropertyInActual.call(self, propName, actualFields); - if(!found && propertyHasNotBeenDeleted.call(self, model, propName)){ - sql.push(addPropertyToActual.call(self, model, propName)); - } - }); - return sql; -}; - -function addPropertyToActual(model, propName){ - var self = this; - var p = self._models[model].properties[propName]; - var 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) { - 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) { - if (propName === 'id') return; - 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)); - } - } - }); - - return sql; - - function datatypeChanged(propName, oldSettings){ - var newSettings = m.properties[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 callback(err, data){ - if(err) console.log(err); - } - - done(); -}; - -PG.prototype.propertiesSQL = function (model) { - var self = this; - var sql = ['"id" SERIAL PRIMARY KEY']; - Object.keys(this._models[model].properties).forEach(function (prop) { - if (prop === 'id') return; - sql.push('"' + prop + '" ' + self.propertySettingsSQL(model, prop)); - }); - return sql.join(',\n '); - -}; - -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) { - if (val === undefined || val === null) { - return 'NULL'; - } - - switch (typeof val) { - case 'boolean': return (val) ? 'true' : 'false'; - case 'number': return val+''; - } - - if (typeof val === 'object') { - val = (typeof val.toISOString === 'function') - ? val.toISOString() - : val.toString(); - } - - val = val.replace(/[\0\n\r\b\t\\\'\"\x1a]/g, function(s) { - switch(s) { - case "\0": return "\\0"; - case "\n": return "\\n"; - case "\r": return "\\r"; - case "\b": return "\\b"; - case "\t": return "\\t"; - case "\x1a": return "\\Z"; - default: return "\\"+s; - } - }); - return "E'"+val+"'"; -}; - -function datatype(p) { - switch (p.type.name) { - default: - case 'String': - case 'JSON': - return 'varchar'; - case 'Text': - return 'text'; - case 'Number': - return 'integer'; - case 'Date': - return 'timestamp'; - case 'Boolean': - return 'boolean'; - } -}; - -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]; -}; diff --git a/lib/schema.js b/lib/schema.js index 8c2e1905..1957513c 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -73,7 +73,7 @@ function Schema(name, settings) { try { adapter = require('jugglingdb-' + name); } catch (e) { - throw new Error('Adapter ' + name + ' is not defined, try\n npm install ' + name); + throw new Error('Adapter ' + name + ' is not installed, try\n npm install jugglingdb-' + name); } } diff --git a/package.json b/package.json index d66b4a77..800fdf0f 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ }, "main": "index.js", "scripts": { - "test": "EXCEPT=cradle,neo4j,nano nodeunit test/*_test*" + "test": "nodeunit test/*_test*" }, "engines": [ "node >= 0.4.12" @@ -63,17 +63,14 @@ "semicov": "*", "coffee-script": ">= 1.2.0", "nodeunit": ">= 0.6.4", - "redis": "= 0.7.2", - "hiredis": "latest", - "mongoose": "latest", - "mysql": ">= 2.0.0-alpha3", - "pg": "= 0.7.2", - "sqlite3": ">= 2.0.18", "riak-js": ">= 0.4.1", "neo4j": ">= 0.2.5", - "mongodb": ">= 0.9.9", "felix-couchdb": ">= 1.0.3", "cradle": ">= 0.6.3", - "nano": "3.3.x" + + "jugglingdb-redis": "latest", + "jugglingdb-mongodb": "latest", + "jugglingdb-mysql": "latest", + "jugglingdb-sqlite": "latest" } } diff --git a/test/migration_test.coffee b/test/migration_test.coffee deleted file mode 100644 index f4a2d526..00000000 --- a/test/migration_test.coffee +++ /dev/null @@ -1,210 +0,0 @@ -juggling = require('../index') -Schema = juggling.Schema -Text = Schema.Text - -DBNAME = process.env.DBNAME || 'myapp_test' -DBUSER = process.env.DBUSER || 'root' -DBPASS = '' -DBENGINE = process.env.DBENGINE || 'mysql' - -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 -, indexes: - index1: - columns: 'email, createdByAdmin' - -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 - -getIndexes = (model, cb) -> - query 'SHOW INDEXES FROM ' + model, (err, res) -> - if err - console.log err - cb err - else - indexes = {} - res.forEach (index) -> - indexes[index.Key_name] = index if index.Seq_in_index == '1' - cb err, indexes - -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 - User.defineProperty 'pendingPeriod', false - schema.autoupdate (err) -> - getFields 'User', (err, fields) -> - # change nullable for email - test.equal fields.email.Null, 'YES', 'Email is not null' - # change type of name - test.equal fields.name.Type, 'varchar(50)', 'Name is not varchar(50)' - # add new column - test.ok fields.newProperty, 'New column was not added' - if fields.newProperty - test.equal fields.newProperty.Type, 'int(11)', 'New column type is not int(11)' - # drop column - test.ok not fields.pendingPeriod, 'drop column' - - # user still exists - userExists (yep) -> - test.ok yep - test.done() - -it 'should check actuality of schema', (test) -> - # drop column - User.schema.isActual (err, ok) -> - test.ok ok, 'schema is actual' - User.defineProperty 'email', false - User.schema.isActual (err, ok) -> - test.ok not ok, 'schema is not actual' - test.done() - -it 'should add single-column index', (test) -> - User.defineProperty 'email', type: String, index: { kind: 'FULLTEXT', type: 'HASH'} - User.schema.autoupdate (err) -> - return console.log(err) if err - getIndexes 'User', (err, ixs) -> - test.ok ixs.email && ixs.email.Column_name == 'email' - console.log(ixs) - test.equal ixs.email.Index_type, 'BTREE', 'default index type' - test.done() - -it 'should change type of single-column index', (test) -> - User.defineProperty 'email', type: String, index: { type: 'BTREE' } - User.schema.isActual (err, ok) -> - test.ok ok, 'schema is actual' - User.schema.autoupdate (err) -> - return console.log(err) if err - getIndexes 'User', (err, ixs) -> - test.ok ixs.email && ixs.email.Column_name == 'email' - test.equal ixs.email.Index_type, 'BTREE' - test.done() - -it 'should remove single-column index', (test) -> - User.defineProperty 'email', type: String, index: false - User.schema.autoupdate (err) -> - return console.log(err) if err - getIndexes 'User', (err, ixs) -> - test.ok !ixs.email - test.done() - -it 'should update multi-column index when order of columns changed', (test) -> - User.schema.adapter._models.User.settings.indexes.index1.columns = 'createdByAdmin, email' - User.schema.isActual (err, ok) -> - test.ok not ok, 'schema is not actual' - User.schema.autoupdate (err) -> - return console.log(err) if err - getIndexes 'User', (err, ixs) -> - test.equals ixs.index1.Column_name, 'createdByAdmin' - test.done() - - -it 'test', (test) -> - User.defineProperty 'email', type: String, index: true - User.schema.autoupdate (err) -> - User.schema.autoupdate (err) -> - User.schema.autoupdate (err) -> - test.done() - -it 'should disconnect when done', (test) -> - schema.disconnect() - test.done() -