diff --git a/lib/abstract-class.js b/lib/abstract-class.js index 9b2d2b42..c8f13c37 100644 --- a/lib/abstract-class.js +++ b/lib/abstract-class.js @@ -167,6 +167,30 @@ AbstractClass.create = function (data, callback) { } }; +AbstractClass.upsert = AbstractClass.updateOrCreate = function upsert(data, callback) { + var Model = this; + if (!data.id) return this.create(data, callback); + if (this.schema.adapter.updateOrCreate) { + this.schema.adapter.updateOrCreate(Model.modelName, data, function (err, data) { + var obj = data ? new Model(data) : null; + if (obj) { + addToCache(Model, obj); + } + callback(err, obj); + }); + } else { + this.find(data.id, function (err, inst) { + if (err) return callback(err); + if (inst) { + inst.updateAttributes(data, callback); + } else { + var obj = new Model(data); + obj.save(data, callback); + } + }); + } +}; + AbstractClass.exists = function exists(id, cb) { if (id) { this.schema.adapter.exists(this.modelName, id, cb); @@ -250,9 +274,7 @@ function substractDirtyAttributes(object, data) { AbstractClass.destroyAll = function destroyAll(cb) { this.schema.adapter.destroyAll(this.modelName, function (err) { - if (!err) { - clearCache(this); - } + clearCache(this); cb(err); }.bind(this)); }; @@ -424,7 +446,7 @@ AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) { } done.call(inst, function () { saveDone.call(inst, function () { - cb(err); + cb(err, inst); }); }); }); diff --git a/lib/adapters/memory.js b/lib/adapters/memory.js index 98ef0d9c..9d8e0ac2 100644 --- a/lib/adapters/memory.js +++ b/lib/adapters/memory.js @@ -17,15 +17,29 @@ Memory.prototype.define = function defineModel(descr) { }; Memory.prototype.create = function create(model, data, callback) { - var id = this.ids[model]++; + var id = data.id || this.ids[model]++; data.id = id; this.cache[model][id] = data; callback(null, id); }; +Memory.prototype.updateOrCreate = function (model, data, callback) { + var mem = this; + this.exists(model, data.id, function (err, exists) { + if (exists) { + mem.save(model, data, callback); + } else { + mem.create(model, data, function (err, id) { + data.id = id; + callback(err, data); + }); + } + }); +}; + Memory.prototype.save = function save(model, data, callback) { this.cache[model][data.id] = data; - callback(); + callback(null, data); }; Memory.prototype.exists = function exists(model, id, callback) { @@ -151,6 +165,7 @@ Memory.prototype.updateAttributes = function updateAttributes(model, id, data, c }; function merge(base, update) { + if (!base) return update; Object.keys(update).forEach(function (key) { base[key] = update[key]; }); diff --git a/lib/adapters/mongodb.js b/lib/adapters/mongodb.js index a93dfa8d..1f601bb3 100644 --- a/lib/adapters/mongodb.js +++ b/lib/adapters/mongodb.js @@ -81,6 +81,29 @@ MongoDB.prototype.find = function find(model, id, callback) { }); }; +MongoDB.prototype.updateOrCreate = function updateOrCreate(model, data, callback) { + var adapter = this; + if (!data.id) return this.create(data, callback); + this.find(model, data.id, function (err, inst) { + if (err) return callback(err); + if (inst) { + adapter.updateAttributes(model, data.id, data, callback); + } else { + delete data.id; + adapter.create(model, data, function (err, id) { + if (err) return callback(err); + if (id) { + data.id = id; + delete data._id; + callback(null, data); + } else{ + callback(null, null); // wtf? + } + }); + } + }); +}; + MongoDB.prototype.destroy = function destroy(model, id, callback) { this.collection(model).remove({_id: new ObjectID(id)}, callback); }; diff --git a/lib/adapters/mysql.js b/lib/adapters/mysql.js index 65606a09..c57b7084 100644 --- a/lib/adapters/mysql.js +++ b/lib/adapters/mysql.js @@ -15,13 +15,24 @@ exports.initialize = function initializeSchema(schema, callback) { port: s.port || 3306, user: s.username, password: s.password, - database: s.database, debug: s.debug }); schema.adapter = new MySQL(schema.client); + schema.adapter.schema = schema; // schema.client.query('SET TIME_ZONE = "+04:00"', callback); - process.nextTick(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) { + callback(); + } else { + throw error; + } + }); + } else callback(); + }); }; function MySQL(client) { @@ -32,10 +43,27 @@ function MySQL(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); }); @@ -57,6 +85,40 @@ MySQL.prototype.create = function (model, data, callback) { }); }; +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; diff --git a/lib/adapters/neo4j.js b/lib/adapters/neo4j.js index 86c35660..02cd858f 100644 --- a/lib/adapters/neo4j.js +++ b/lib/adapters/neo4j.js @@ -207,7 +207,7 @@ Neo4j.prototype.save = function save(model, data, callback) { if (err) return callback(err); self.updateIndexes(model, node, function (err) { if (err) return console.log(err); - callback(null); + callback(null, node.data); }); }); }); diff --git a/lib/adapters/postgres.js b/lib/adapters/postgres.js index 02ea8ad8..bbc3d46f 100644 --- a/lib/adapters/postgres.js +++ b/lib/adapters/postgres.js @@ -55,7 +55,7 @@ PG.prototype.query = function (sql, callback) { * Must invoke callback(err, id) */ PG.prototype.create = function (model, data, callback) { - var fields = this.toFields(model, data,true); + var fields = this.toFields(model, data, true); var sql = 'INSERT INTO ' + this.tableEscaped(model) + ''; if (fields) { sql += ' ' + fields; @@ -69,6 +69,43 @@ PG.prototype.create = function (model, data, callback) { }); }; +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; diff --git a/lib/adapters/sqlite3.js b/lib/adapters/sqlite3.js index 387b2cdb..f0d06055 100644 --- a/lib/adapters/sqlite3.js +++ b/lib/adapters/sqlite3.js @@ -90,6 +90,24 @@ SQLite3.prototype.create = function (model, data, callback) { }); }; +SQLite3.prototype.updateOrCreate = function (model, data, callback) { + data = data || {}; + var questions = []; + var values = Object.keys(data).map(function (key) { + questions.push('?'); + return data[key]; + }); + var sql = 'INSERT OR REPLACE INTO ' + this.tableEscaped(model) + ' (' + Object.keys(data).join(',') + ') VALUES (' + sql += questions.join(','); + sql += ')'; + this.command(sql, values, function (err) { + if (!err && this) { + data.id = this.lastID; + } + callback(err, data); + }); +}; + SQLite3.prototype.toFields = function (model, data) { var fields = []; var props = this._models[model].properties; diff --git a/test/common_test.js b/test/common_test.js index a46bb457..8e5ae72e 100644 --- a/test/common_test.js +++ b/test/common_test.js @@ -37,7 +37,7 @@ Object.keys(schemas).forEach(function (schemaName) { }); schema.log = function (a) { - // console.log(a); + // console.log(a); }; testOrm(schema); if (specificTest[schemaName]) specificTest[schemaName](schema); @@ -716,6 +716,40 @@ function testOrm(schema) { }); }); + if (schema.name !== 'mongoose' && schema.name !== 'neo4j') + it('should update or create record', function (test) { + var newData = { + id: 1, + title: 'New title (really new)', + content: 'Some example content (updated)' + }; + Post.updateOrCreate(newData, function (err, updatedPost) { + if (err) throw err; + test.ok(updatedPost); + if (!updatedPost) throw Error('No post!'); + + test.equal(newData.id, updatedPost.toObject().id); + test.equal(newData.title, updatedPost.toObject().title); + test.equal(newData.content, updatedPost.toObject().content); + + Post.find(updatedPost.id, function (err, post) { + if (err) throw err; + if (!post) throw Error('No post!'); + test.equal(newData.id, post.toObject().id); + test.equal(newData.title, post.toObject().title); + test.equal(newData.content, post.toObject().content); + Post.updateOrCreate({id: 100001, title: 'hey'}, function (err, post) { + if (schema.name !== 'mongodb') test.equal(post.id, 100001); + test.equal(post.title, 'hey'); + Post.find(post.id, function (err, post) { + if (!post) throw Error('No post!'); + test.done(); + }); + }); + }); + }); + }); + it('all tests done', function (test) { test.done(); process.nextTick(allTestsDone);