From 7ace323c58d3997ff7a0406c13c7316b04a894c1 Mon Sep 17 00:00:00 2001 From: Taliesin Sisson Date: Mon, 8 Aug 2011 16:22:27 +0100 Subject: [PATCH 1/9] Initial check in for riak datamapper --- lib/datamapper/riak.js | 429 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 lib/datamapper/riak.js diff --git a/lib/datamapper/riak.js b/lib/datamapper/riak.js new file mode 100644 index 0000000..ee73619 --- /dev/null +++ b/lib/datamapper/riak.js @@ -0,0 +1,429 @@ +var riak_lib = require('riak-js'); +var uuid = require('node-uuid'); +var sys = require("sys"); + +var riak = riak_lib.getClient(); + +exports.configure = function (config) { + config.host = config.host || '127.0.0.1'; + config.port = config.port || 8098; + + riak = riak_lib.getClient(config); +}; + +function castForDatabase(properties, attr, data) { + var type = properties[attr].type; + switch (typeof type == 'function' ? type.name : type) { + case 'json': + return new Buffer(JSON.stringify(data), 'utf-8'); + + case 'Date': + case 'String': + case 'Number': + return new Buffer((data == undef || data == null ? '' : data).toString(), 'utf-8'); + + default: + return data ? data.toString() : ''; + } +} + +function castFromDatabase(properties, attr, data) { + if (!properties[attr]) { + return; + } + var type = properties[attr].type; + switch (typeof type == 'function' ? type.name : type) { + case 'Number': + data = parseInt(data, 10); + break; + case 'Date': + if (data == '') data = null; + data = new Date(data); + break; + case 'String': + data = (data || '').toString(); + break; + case 'Boolean': + data = data == 'true' || data == '1'; + break; + case 'json': + try { + data = JSON.parse(data.toString('utf-8')); + } catch(e) { + console.log(data.toString('binary')); + throw e; + } + break; + default: + data = parseInt(data, 10); + break; + } + return data; +} + +exports.mixPersistMethods = function (Model, description) { // model_name, properties, associations) { + // TODO: underscorize + var model_name = description.className, + model_name_lowercase = model_name.toLowerCase(), + primary_key = description.primaryKey || 'id', + table_name = description.tableName, + properties = description.properties, + associations = description.associations, + scopes = description.scopes; + + var cache = {}; + + Object.defineProperty(Model, 'connection', { + enumerable: false, + value: riak + }); + Object.defineProperty(Model, 'riak', { + enumerable: false, + value: riak_lib + }); + Model.prototype.connection = riak; + + // define primary key + var pk_defined = false; + for (var i in properties) { + if (properties[i].primary) { + pk_defined = true; + } + } + if (!pk_defined) { + properties[primary_key] = {type: String, primary: true}; + } + + // initializer + Model.prototype.initialize = function (params, paramsOnly) { + params = params || {}; + Object.keys(properties).forEach(function (attr) { + var _attr = '_' + attr, + attr_was = attr + '_was'; + + if (paramsOnly && !params.hasOwnProperty(attr)) { + return; + } + + // Hidden property to store currrent value + Object.defineProperty(this, _attr, { + writable: true, + enumerable: false, + configurable: true, + value: params[attr] !== undef ? params[attr] : (this[attr] !== undef ? this[attr] : null) + }); + + // Public setters and getters + Object.defineProperty(this, attr, { + get: function () { + return this[_attr]; + }, + set: function (value) { + this[_attr] = value; + }, + configurable: true, + enumerable: true + }); + + // Getter for initial property + Object.defineProperty(this, attr_was, { + get: function () { + return params[attr]; + }, + configurable: true, + enumerable: false + }); + }.bind(this)); + }; + + /** + * TODO doc + * Create new object in storage + */ + Model.create = function (params) { + var callback = arguments[arguments.length - 1]; + if (arguments.length == 0 || params === callback) { + params = {}; + } + if (typeof callback !== 'function') { + callback = function () { + }; + } + + debug("create new " + model_name_lowercase + ""); + + var self = new Model; + if (pk_defined && !params.hasOwnProperty(primary_key)) { + throw Error('Must specify primary key value for ' + primary_key); + } + + if (!pk_defined && !params.hasOwnProperty(primary_key)) { + params[primary_key] = uuid(); + } + + cache[params[primary_key]] = self; + + self.save(params, callback.bind(self, params[primary_key], self)); + }; + + /** + * TODO test + * Find object in database + * @param {Number} id identifier of record + * @param {Function} callback(err) Function will be called after search + * it takes two arguments: + * - error + * - found object + * * applies to found object + */ + Model.findById = Model.find = function (id, callback) { + if (!id) { + throw new Error(model_name + '.find(): `id` param required'); + } + if (typeof callback !== 'function') { + throw new Error(model_name + '.find(): `callback` param required'); + } + + // check cache + if (cache[id]) { + // ok, we got it, sync with database + cache[id].reload(function () { + callback.call(this, null, this); + }); + return; + } + + riak.get(table_name, id, function(err, data, meta) { + var found = false; + if (!err) { + var obj = {}; + obj[primary_key] = id; + Object.keys(properties).forEach(function (attr) { + found = true; + obj[attr] = castFromDatabase(properties, attr, data[attr]); + }); + var object = new Model(obj); + cache[id] = object; + callback.call(found ? object : null, found ? null : true, found ? object : null); + } + else { + callback.call(null, true); + } + }); + }; + + /** + * TODO document + * Checks whether record with given id exists in database + * @param id - primary id of record + * @param callback - takes two params: err and exists (Boolean) + */ + Model.exists = function (id, callback) { + riak.exists(table_name, id, function (err, data, meta) { + if (typeof callback == 'function') { + callback(err, data); + } + }); + }; + + // Define instance methods + + /** + * Checks is property changed based on current property and initial value + * @param {attr} String - property name + * @return Boolean + */ + Model.prototype.propertyChanged = function (attr) { + return this['_' + attr] !== this[attr + '_was']; + }; + + /** + * TODO test + * Exports all defined properties to JSON + * @return JSON string + */ + Model.prototype.toJSON = function () { + var data = {}; + Object.keys(properties).forEach(function (attr) { + data[attr] = this[attr]; + }.bind(this)); + return JSON.stringify(data); + }; + + /** + * Check whether object is new record + * @return Boolean + */ + Model.prototype.isNewRecord = function () { + return !this[primary_key]; + }; + + Model.prototype.reload = function (callback) { // TODO test, doc, refactor to do not use `new` + if (this.isNewRecord()) { + if (typeof callback == 'function') { + callback.call(this, true); + } + return; + } + + riak.get(table_name, id, function(err, data, meta) { + if (!err) { + throw err; + } + + var obj = {}; + for (var attr in data) { + obj[attr] = castFromDatabase(properties, attr, data[attr]); + } + + this.initialize(obj); + callback.call(this, err); + }.bind(this)); + }; + + /** + * TODO test + * Destroy record (delete from persistence) + * @param callback -- function to call after operation + * takes two params: + * - err + * - succ + */ + Model.prototype.destroy = function (callback) { + riak.remove(table_name, this[primary_key], function(err, data, meta) { + if (!err) { + delete cache[this[primary_key]]; + delete this; + } + + callback(err, data); + }.bind(this)); + }; + + Model.prototype.save = function (data, callback) { + if (typeof data == 'function') { + callback = data; + data = {}; + } + if (callback === undef) { + callback = function () { + }; + } + + if (data === undef) { + data = {}; + } + var currentDate = new Date; + + if (this.isNewRecord()) { + var insertedProperties = {}; + Object.keys(properties).forEach(function (attr) { + if (data[attr] !== undef) { + this[attr] = data[attr]; + } + + insertedProperties[attr] = castForDatabase(properties, attr, this[attr]); + }.bind(this)); + + if (properties.hasOwnProperty('created_at')) { + this.created_at = currentDate; + insertedProperties['created_at'] = castForDatabase(properties, 'created_at', this.created_at); + } + if (properties.hasOwnProperty('updated_at')) { + this.updated_at = currentDate; + insertedProperties['updated_at'] = castForDatabase(properties, 'updated_at', this.updated_at); + } + + riak.save(table_name, this[primary_key], JSON.stringify(insertedProperties), function(err, data, meta) { + callback.call(this, err); + }.bind(this)); + + return; + } + + var updatedProperties = {}; + Object.keys(properties).forEach(function (attr) { + if (data[attr] !== undef) { + this[attr] = data[attr]; + } + if (this.propertyChanged(attr)) { + updatedProperties[attr] = castForDatabase(properties, attr, this[attr]); + } + }.bind(this)); + + if (updatedProperties.length < 1) { + callback.call(this, false); + return; + } + + if (properties.hasOwnProperty('updated_at')) { + this.updated_at = currentDate; + updatedProperties['updated_at'] = castForDatabase(properties, 'updated_at', this.updated_at); + } + + riak.update(table_name, this[primary_key], JSON.stringify(updatedProperties), function(err, data, meta) { + callback.call(this, err); + }.bind(this)); + }; + +// Model.prototype.updateAttribute = function accessor(attr, value, callback) { +// debug(model_name + '[' + this[primary_key] + '].updateAttribute(' + attr + ')'); +// debug(value); +// +// this[attr] = value; +// +// if (typeof callback !== 'function') { +// callback = function () { +// }; +// } +// +// if (this.propertyChanged(attr)) { +// var updatedProperties = {}; +// updatedProperties[attr] = value; +// if (properties.hasOwnProperty('updated_at')) { +// this.updated_at = new Date; +// updatedProperties['updated_at'] = this.updated_at; +// } +// +// riak.update(table_name, id, JSON.stringify(updatedProperties), function(err, data, meta) { +// callback.call(this, err); +// }.bind(this)); +// } else { +// debug('property `' + attr + '` is not modified'); +// callback.call(this, false); +// } +// }; + +// Model.all = function (callback) { +// riak.keys(table_name, function (err, keys) { +// callback.call(err, keys); +// }); +// }; + + Model.allInstances = function(options, callback) { + riak.getAll(table_name, options, function (err, data, meta) { + if (!err) { + data = data || []; + data.forEach(function (row, index) { + data[index] = Model.instantiate(row); + }); + } + + callback.call(err, data); + }); + }; + + Model.instantiate = function (data) { + if (!data.hasOwnProperty(primary_key)) { + throw new Error('Only objects with an `' + primary_key + '` property can be instantiated'); + } + + if (cache[data[primary_key]]) { + cache[data[primary_key]].initialize(data, true); + } else { + cache[data[primary_key]] = new Model(data); + } + return cache[data[primary_key]]; + }; +}; + From 1e8b891ebb5e10fcf2683b9637162b9ac6a82775 Mon Sep 17 00:00:00 2001 From: Taliesin Sisson Date: Mon, 8 Aug 2011 16:24:17 +0100 Subject: [PATCH 2/9] Refactor generator according to ruby mine Start adding schema generation for riak --- lib/generators.js | 112 +++++++++++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 46 deletions(-) diff --git a/lib/generators.js b/lib/generators.js index 4935056..a74e370 100644 --- a/lib/generators.js +++ b/lib/generators.js @@ -45,7 +45,7 @@ function addGenerator(name, callback, meta) { if (meta.alias) { collection[meta.alias] = callback; } -}; +} exports.addGenerator = addGenerator; /** @@ -128,9 +128,9 @@ exports.list = function () { createFile('public/javascripts/application.js', '// place your application-wide javascripts here\n'); createFileByTemplate('npmfile', 'npmfile', replaceViewEngine); createFileByTemplate('public/favicon.ico', 'favicon.ico'); - var extenstion = options.coffee ? '.coffee' : '.js'; + var fileExtension = options.coffee ? '.coffee' : '.js'; var engine = options.coffee ? 'coffee' : 'node'; - createFile('Procfile', 'web: ' + engine + ' server' + extenstion); + createFile('Procfile', 'web: ' + engine + ' server' + fileExtension); createFileByTemplate('package.json', 'package.json', [replaceAppname, replaceViewEngine]); @@ -171,7 +171,10 @@ exports.list = function () { if (driver == 'mongoose') { attrs.push(property + ': { type: ' + type + ' }'); - } else { + } else if (driver == 'riak'){ + attrs.push(property + ': ' + type); + } + else { attrs.push(' property("' + property + '", ' + type + ');'); } result.push({name: property, type: type, plainType: plainType}); @@ -188,7 +191,20 @@ exports.list = function () { appendToFile('db/schema.js', schema); code = ''; - } else { + } + else if (driver == 'riak'){ + schema = '\n\n/**\n * ' + Model + '\n */\n'; + schema += 'var ' + Model + ' = Model.create({\n'; + schema += 'schema: { ' + attrs.join(',\n ') + '\n},\n'; + schema += 'primaryKey: "' + camelize(Model, true) + 'Id",\n'; + schema += 'bucket: "' + camelize(pluralize(Model)) + '"\n'; + schema += '});\n\n'; + schema += 'module.exports["' + Model + '"] = ' + Model + ';'; + appendToFile('db/schema.js', schema); + + code = ''; + } + else { code = 'var ' + Model + ' = describe("' + Model + '", function () {\n' + attrs.join('\n') + '\n});'; } @@ -206,15 +222,17 @@ exports.list = function () { return; } + var fileExtension; + var actions; if (options.coffee) { - var extenstion = '.coffee'; - var actions = ['load \'application\'']; + fileExtension = '.coffee'; + actions = ['load \'application\'']; options.forEach(function (action) { actions.push('action "' + action + '", () -> \n render\n title: "' + controller + '#' + action + '"'); }); } else { - var extenstion = '.js'; - var actions = ['load(\'application\');']; + fileExtension = '.js'; + actions = ['load(\'application\');']; options.forEach(function (action) { actions.push('action("' + action + '", function () {\n render({\n title: "' + controller + '#' + action + '"\n });\n});'); }); @@ -228,7 +246,7 @@ exports.list = function () { createParents(ns, 'app/controllers/'); // controller - var filename = 'app/controllers/' + controller + '_controller' + extenstion; + var filename = 'app/controllers/' + controller + '_controller' + fileExtension; createFile(filename, actions.join('\n\n')); createDir('app/helpers/'); @@ -277,8 +295,9 @@ exports.list = function () { } function crudGenerator(args) { - var model = args[0], - models = pluralize(model).toLowerCase(); + var driver = ormDriver(); + var model = args[0]; + var models = pluralize(model).toLowerCase(); if (!model) { console.log('Usage example: railway g crud post title:string content:string published:boolean'); @@ -289,37 +308,26 @@ exports.list = function () { ns.pop(); var result = modelGenerator.apply(this, Array.prototype.slice.call(arguments)); - var extenstion = options.coffee ? '.coffee' : '.js'; createDir('app/'); createDir('app/controllers/'); createParents(ns, 'app/controllers/'); - createFile('app/controllers/' + models + '_controller' + extenstion, controllerCode(model)); - - function controllerCode(model) { - var code, _model = model.toLowerCase(); - try { - code = fs.readFileSync(__dirname + '/../templates/crud_controller_' + ormDriver() + extenstion); - } catch (e) { - code = fs.readFileSync(__dirname + '/../templates/crud_controller_redis' + extenstion); - } - code = code - .toString('utf8') - .replace(/models/g, pluralize(model).toLowerCase()) - .replace(/model/g, model.toLowerCase()) - .replace(/Model/g, camelize(model, true)) - .replace(/FILTER_PROPERTIES/g, '[' + result.map(function (p) { - return "'" + p.name + "'"; - }).join(', ') + ']'); - return code; - } + var fileExtension = options.coffee ? '.coffee' : '.js'; + createFile('app/controllers/' + models + '_controller' + fileExtension, controllerCode(model, driver, result)); createDir('app/helpers/'); createParents(ns, 'app/helpers/'); + function replaceModel(code) { + return code + .replace(/models/g, models) + .replace(/model/g, model.toLowerCase()) + .replace(/Model/g, camelize(model, true)) + .replace(/VALID_ATTRIBUTES/, result.map(function (attr) { return attr.name + ": ''" }).join(',\n ')); + } + // helper - filename = 'app/helpers/' + models + '_helper.js'; - createFile(filename, 'module.exports = {\n};'); + createFile('app/helpers/' + models + '_helper.js', 'module.exports = {\n};'); // layout createViewByTemplate('app/views/layouts/' + models + '_layout', 'scaffold_layout', replaceModel); @@ -332,7 +340,7 @@ exports.list = function () { createFileByTemplate('test/test_helper.js', 'test_helper.js'); createDir('test/controllers'); createParents(ns, 'test/controllers/'); - if (ormDriver() === 'mongoose') { + if (driver === 'mongoose') { createFileByTemplate('test/controllers/' + models + '_controller_test', 'crud_controller_test_mongoose', replaceModel); } else { createFileByTemplate('test/controllers/' + models + '_controller_test.js', 'crud_controller_test_redis.js', replaceModel); @@ -347,14 +355,6 @@ exports.list = function () { createViewByTemplate('app/views/' + models + '/' + template, 'scaffold_' + template, replaceModel); }); - function replaceModel(code) { - return code - .replace(/models/g, models) - .replace(/model/g, model.toLowerCase()) - .replace(/Model/g, camelize(model, true)) - .replace(/VALID_ATTRIBUTES/, result.map(function (attr) { return attr.name + ": ''" }).join(',\n ')); - } - // route var routesConfig = process.cwd() + '/config/routes.js', routes = fs.readFileSync(routesConfig, 'utf8') @@ -380,6 +380,26 @@ exports.list = function () { * Private helper methods */ + + function controllerCode(model, driver, result) { + var fileExtension = options.coffee ? '.coffee' : '.js'; + var code; + try { + code = fs.readFileSync(__dirname + '/../templates/crud_controller_' + driver + fileExtension); + } catch (e) { + code = fs.readFileSync(__dirname + '/../templates/crud_controller_redis' + fileExtension); + } + code = code + .toString('utf8') + .replace(/models/g, pluralize(model).toLowerCase()) + .replace(/model/g, model.toLowerCase()) + .replace(/Model/g, camelize(model, true)) + .replace(/FILTER_PROPERTIES/g, '[' + result.map(function (p) { + return "'" + p.name + "'"; + }).join(', ') + ']'); + return code; + } + function parseOptions(defaultKeyName) { options = []; options.tpl = app.settings['view engine'] || 'ejs'; @@ -446,9 +466,9 @@ exports.list = function () { function createFileByTemplate(filename, template, prepare) { if (!template.match(/\..+$/)) { - var ext = options.coffee ? '.coffee' : '.js'; - template += ext; - filename += ext; + var fileExtension = options.coffee ? '.coffee' : '.js'; + template += fileExtension; + filename += fileExtension; } var text = fs.readFileSync(__dirname + '/../templates/' + template); if (prepare) { From e3bb4567f9dbac78bc5ac7518f4ae0b4fa4d46fc Mon Sep 17 00:00:00 2001 From: Taliesin Sisson Date: Tue, 9 Aug 2011 12:28:29 +0100 Subject: [PATCH 3/9] Fix spelling mistakes --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 43eb18c..7a0a161 100644 --- a/README.md +++ b/README.md @@ -297,8 +297,8 @@ or it's shortcut railway c -It just simple node-js console with some Railway bingings, e.g. models. Just one note -about working with console. Node.js is asunchronous by his nature, and it's great +It just simple node-js console with some Railway bindings, e.g. models. Just one note +about working with console. Node.js is asynchronous by its nature, and it's great but it made console debugging much more complicated, because you should use callback to fetch result from database, for example. I have added one useful method to simplify async debugging using railway console. It's name `c`, you can pass it @@ -327,7 +327,7 @@ Localization To add another language to app just create yml file in `config/locales`, for example `config/locales/jp.yml`, copy contents of `config/locales/en.yml` to new file and rename root node (`en` to `jp` in that case), also in `lang` section rename -`name` to Japaneese (for example). +`name` to Japanese (for example). Next step - rename email files in `app/views/emails`, copy all files `*.en.html` and `*.en.text` to `*.jp.html` and `*.jp.text` and translate new files. @@ -406,7 +406,7 @@ MIT License THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN From 7df48671c77cd8b2a4928de620e24f1738443d83 Mon Sep 17 00:00:00 2001 From: Taliesin Sisson Date: Tue, 9 Aug 2011 12:29:11 +0100 Subject: [PATCH 4/9] Tested version of riak data mapper --- lib/datamapper/riak.js | 452 +++++++++++++++++++---------------------- 1 file changed, 213 insertions(+), 239 deletions(-) diff --git a/lib/datamapper/riak.js b/lib/datamapper/riak.js index ee73619..792e774 100644 --- a/lib/datamapper/riak.js +++ b/lib/datamapper/riak.js @@ -1,6 +1,7 @@ var riak_lib = require('riak-js'); var uuid = require('node-uuid'); -var sys = require("sys"); +var sys = require("sys"), + undef; var riak = riak_lib.getClient(); @@ -11,61 +12,17 @@ exports.configure = function (config) { riak = riak_lib.getClient(config); }; -function castForDatabase(properties, attr, data) { - var type = properties[attr].type; - switch (typeof type == 'function' ? type.name : type) { - case 'json': - return new Buffer(JSON.stringify(data), 'utf-8'); - - case 'Date': - case 'String': - case 'Number': - return new Buffer((data == undef || data == null ? '' : data).toString(), 'utf-8'); - - default: - return data ? data.toString() : ''; +function debug (m) { + if (exports.debugMode) { + sys.debug(m); } } -function castFromDatabase(properties, attr, data) { - if (!properties[attr]) { - return; - } - var type = properties[attr].type; - switch (typeof type == 'function' ? type.name : type) { - case 'Number': - data = parseInt(data, 10); - break; - case 'Date': - if (data == '') data = null; - data = new Date(data); - break; - case 'String': - data = (data || '').toString(); - break; - case 'Boolean': - data = data == 'true' || data == '1'; - break; - case 'json': - try { - data = JSON.parse(data.toString('utf-8')); - } catch(e) { - console.log(data.toString('binary')); - throw e; - } - break; - default: - data = parseInt(data, 10); - break; - } - return data; -} - exports.mixPersistMethods = function (Model, description) { // model_name, properties, associations) { // TODO: underscorize var model_name = description.className, model_name_lowercase = model_name.toLowerCase(), - primary_key = description.primaryKey || 'id', + primary_key = 'key', table_name = description.tableName, properties = description.properties, associations = description.associations, @@ -105,7 +62,7 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope return; } - // Hidden property to store currrent value + // Hidden property to store current value Object.defineProperty(this, _attr, { writable: true, enumerable: false, @@ -136,96 +93,6 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope }.bind(this)); }; - /** - * TODO doc - * Create new object in storage - */ - Model.create = function (params) { - var callback = arguments[arguments.length - 1]; - if (arguments.length == 0 || params === callback) { - params = {}; - } - if (typeof callback !== 'function') { - callback = function () { - }; - } - - debug("create new " + model_name_lowercase + ""); - - var self = new Model; - if (pk_defined && !params.hasOwnProperty(primary_key)) { - throw Error('Must specify primary key value for ' + primary_key); - } - - if (!pk_defined && !params.hasOwnProperty(primary_key)) { - params[primary_key] = uuid(); - } - - cache[params[primary_key]] = self; - - self.save(params, callback.bind(self, params[primary_key], self)); - }; - - /** - * TODO test - * Find object in database - * @param {Number} id identifier of record - * @param {Function} callback(err) Function will be called after search - * it takes two arguments: - * - error - * - found object - * * applies to found object - */ - Model.findById = Model.find = function (id, callback) { - if (!id) { - throw new Error(model_name + '.find(): `id` param required'); - } - if (typeof callback !== 'function') { - throw new Error(model_name + '.find(): `callback` param required'); - } - - // check cache - if (cache[id]) { - // ok, we got it, sync with database - cache[id].reload(function () { - callback.call(this, null, this); - }); - return; - } - - riak.get(table_name, id, function(err, data, meta) { - var found = false; - if (!err) { - var obj = {}; - obj[primary_key] = id; - Object.keys(properties).forEach(function (attr) { - found = true; - obj[attr] = castFromDatabase(properties, attr, data[attr]); - }); - var object = new Model(obj); - cache[id] = object; - callback.call(found ? object : null, found ? null : true, found ? object : null); - } - else { - callback.call(null, true); - } - }); - }; - - /** - * TODO document - * Checks whether record with given id exists in database - * @param id - primary id of record - * @param callback - takes two params: err and exists (Boolean) - */ - Model.exists = function (id, callback) { - riak.exists(table_name, id, function (err, data, meta) { - if (typeof callback == 'function') { - callback(err, data); - } - }); - }; - // Define instance methods /** @@ -258,49 +125,14 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope return !this[primary_key]; }; - Model.prototype.reload = function (callback) { // TODO test, doc, refactor to do not use `new` - if (this.isNewRecord()) { - if (typeof callback == 'function') { - callback.call(this, true); - } - return; - } - - riak.get(table_name, id, function(err, data, meta) { - if (!err) { - throw err; - } - - var obj = {}; - for (var attr in data) { - obj[attr] = castFromDatabase(properties, attr, data[attr]); - } - - this.initialize(obj); - callback.call(this, err); - }.bind(this)); - }; - - /** + /** * TODO test - * Destroy record (delete from persistence) - * @param callback -- function to call after operation - * takes two params: - * - err - * - succ + * Insert/update model in a table in the database + * @param {Object} data - initial property values + * @param {Function} callback(error, model) */ - Model.prototype.destroy = function (callback) { - riak.remove(table_name, this[primary_key], function(err, data, meta) { - if (!err) { - delete cache[this[primary_key]]; - delete this; - } - - callback(err, data); - }.bind(this)); - }; - Model.prototype.save = function (data, callback) { + if (typeof data == 'function') { callback = data; data = {}; @@ -322,20 +154,23 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope this[attr] = data[attr]; } - insertedProperties[attr] = castForDatabase(properties, attr, this[attr]); + if (attr != primary_key) + { + insertedProperties[attr] = this[attr]; + } }.bind(this)); if (properties.hasOwnProperty('created_at')) { this.created_at = currentDate; - insertedProperties['created_at'] = castForDatabase(properties, 'created_at', this.created_at); + insertedProperties['created_at'] = this.created_at; } if (properties.hasOwnProperty('updated_at')) { this.updated_at = currentDate; - insertedProperties['updated_at'] = castForDatabase(properties, 'updated_at', this.updated_at); + insertedProperties['updated_at'] = this.updated_at; } - riak.save(table_name, this[primary_key], JSON.stringify(insertedProperties), function(err, data, meta) { - callback.call(this, err); + riak.save(table_name, this[primary_key], insertedProperties, function(error, data, meta) { + callback.call(error, this); }.bind(this)); return; @@ -346,84 +181,223 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope if (data[attr] !== undef) { this[attr] = data[attr]; } - if (this.propertyChanged(attr)) { - updatedProperties[attr] = castForDatabase(properties, attr, this[attr]); + if (attr != primary_key && this.propertyChanged(attr)) { + updatedProperties[attr] = this[attr]; } }.bind(this)); if (updatedProperties.length < 1) { - callback.call(this, false); + callback.call(undef, this); return; } if (properties.hasOwnProperty('updated_at')) { this.updated_at = currentDate; - updatedProperties['updated_at'] = castForDatabase(properties, 'updated_at', this.updated_at); + updatedProperties['updated_at'] = this.updated_at; } - riak.update(table_name, this[primary_key], JSON.stringify(updatedProperties), function(err, data, meta) { - callback.call(this, err); + riak.update(table_name, this[primary_key], updatedProperties, function(error, data, meta) { + callback.call(error, this); }.bind(this)); }; -// Model.prototype.updateAttribute = function accessor(attr, value, callback) { -// debug(model_name + '[' + this[primary_key] + '].updateAttribute(' + attr + ')'); -// debug(value); -// -// this[attr] = value; -// -// if (typeof callback !== 'function') { -// callback = function () { -// }; -// } -// -// if (this.propertyChanged(attr)) { -// var updatedProperties = {}; -// updatedProperties[attr] = value; -// if (properties.hasOwnProperty('updated_at')) { -// this.updated_at = new Date; -// updatedProperties['updated_at'] = this.updated_at; -// } -// -// riak.update(table_name, id, JSON.stringify(updatedProperties), function(err, data, meta) { -// callback.call(this, err); -// }.bind(this)); -// } else { -// debug('property `' + attr + '` is not modified'); -// callback.call(this, false); -// } -// }; - -// Model.all = function (callback) { -// riak.keys(table_name, function (err, keys) { -// callback.call(err, keys); -// }); -// }; - - Model.allInstances = function(options, callback) { - riak.getAll(table_name, options, function (err, data, meta) { - if (!err) { - data = data || []; - data.forEach(function (row, index) { - data[index] = Model.instantiate(row); + /** + * TODO test + * Reload model from table in the database if necessary + * @param {Function} callback(error, model) + */ + Model.prototype.reload = function (callback) { + if (this.isNewRecord()) { + if (typeof callback == 'function') { + callback.call(undef, this); + } + return; + } + var key = this[primary_key]; + riak.get(table_name, key, function(error, result, meta) { + if (!error) { + var record = result; + record[primary_key] = key; + + var model = Model.instantiate(record); + callback.call(error, model); + } + else { + callback.call(error, null); + } + }); + }; + + /** + * TODO test + * Returns all keys for a table in the database + * @param {Function} callback(error, keys) + */ + Model.all = function (callback) { + riak.keys(table_name, function (error, keys) { + callback.call(error, keys); + }); + }; + + /** + * TODO test + * Create new model for a table in the database + * @param {Object} data - initial property values + * @param {Function} callback(error, model) + */ + Model.create = function (data, callback) { + if (typeof data == 'function') { + callback = data; + data = {}; + } + if (callback === undef) { + callback = function () {}; + } + + debug("create new " + model_name_lowercase + ""); + + var self = new Model; + if (pk_defined && !data.hasOwnProperty(primary_key)) { + throw Error('Must specify primary key value for ' + primary_key); + } + + if (!pk_defined && !data.hasOwnProperty(primary_key)) { + data[primary_key] = uuid(); + } + + cache[data[primary_key]] = self; + self.save(data, callback); + }; + + /** + * TODO test + * Checks whether model with given key exists for a table in the database + * @param {String} key identifier of record + * @param {Function} callback(error, exists) + */ + Model.exists = function (key, callback) { + riak.exists(table_name, key, function (error, exists, meta) { + if (typeof callback == 'function') { + callback(error, exists); + } + }); + }; + + + /** + * TODO test + * Returns all models for a table in the database + * @param {Function} callback(error, models) + */ + Model.allInstances = function(callback) { + riak.getAll(table_name, function (error, result, meta) { + var models = []; + if (!error) { + result = result || []; + result.forEach(function (row) { + var key = row['meta']['key']; + var record = row['data']; + record[primary_key] = key; + + var model = Model.instantiate(record); + models.push(model); }); } - callback.call(err, data); + callback.call(error, models); }); }; + /** + * TODO test + * Returns model for a table in the database + * @param {String} key identifier of record + * @param {Function} callback(error, model) + */ + Model.findById = function (key, callback) { + if (!key) { + throw new Error(model_name + '.findById(): `key` param required'); + } + if (typeof callback !== 'function') { + throw new Error(model_name + '.findById(): `callback` param required'); + } + + // check cache + if (cache[key]) { + // ok, we got it, sync with database + cache[key].reload(callback); + return; + } + + riak.get(table_name, key, function(error, result, meta) { + if (!error) { + var record = result; + record[primary_key] = key; + + var model = Model.instantiate(record); + callback.call(error, model); + } + else { + callback.call(error, null); + } + }); + }; + + + /** + * TODO test + * Create new model + * @param {Object} data - initial property values + */ Model.instantiate = function (data) { - if (!data.hasOwnProperty(primary_key)) { - throw new Error('Only objects with an `' + primary_key + '` property can be instantiated'); + var key = data[primary_key]; + + if (!key) { + throw new Error(model_name + '.instantiate(): `'+ primary_key + '` param required'); } - if (cache[data[primary_key]]) { - cache[data[primary_key]].initialize(data, true); + if (cache[key]) { + cache[key].initialize(data, false); } else { - cache[data[primary_key]] = new Model(data); + cache[key] = new Model(data); } - return cache[data[primary_key]]; + return cache[key]; }; -}; + /** + * TODO test + * Removes a model from a table in the database + * @param {Function} callback(error, success) + */ + Model.destroy = function (callback) { + riak.remove(table_name, this[primary_key], function(error, success, meta) { + if (!error) { + delete cache[this[primary_key]]; + delete this; + } + + callback(error, success); + }.bind(this)); + }; + + /** + * TODO test + * Removes a model from a table in the database + * @param {String} key identifier of record + * @param {Function} callback(error, success) + */ + Model.destroyById = function (key, callback) { + riak.remove(table_name, key, function(error, success, meta) { + if (!error) { + var model = cache[key]; + + if (model) + { + delete cache[key]; + } + } + + callback(error, success); + }); + }; +}; \ No newline at end of file From 12828eb8e3cd6cbfa972aeb91f9efb0c25cb1d92 Mon Sep 17 00:00:00 2001 From: Taliesin Sisson Date: Tue, 9 Aug 2011 16:53:09 +0100 Subject: [PATCH 5/9] Add database configuration for riak Add controller templates for riak Allow people to specify primary key to use for riak --- lib/datamapper/riak.js | 2 +- lib/generators.js | 14 ----- templates/config/database_riak.json | 25 +++++++++ templates/crud_controller_riak.coffee | 57 ++++++++++++++++++++ templates/crud_controller_riak.js | 76 +++++++++++++++++++++++++++ 5 files changed, 159 insertions(+), 15 deletions(-) create mode 100644 templates/config/database_riak.json create mode 100644 templates/crud_controller_riak.coffee create mode 100644 templates/crud_controller_riak.js diff --git a/lib/datamapper/riak.js b/lib/datamapper/riak.js index 792e774..79f0431 100644 --- a/lib/datamapper/riak.js +++ b/lib/datamapper/riak.js @@ -22,7 +22,7 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope // TODO: underscorize var model_name = description.className, model_name_lowercase = model_name.toLowerCase(), - primary_key = 'key', + primary_key = description.primaryKey || 'id', table_name = description.tableName, properties = description.properties, associations = description.associations, diff --git a/lib/generators.js b/lib/generators.js index a74e370..eaede8d 100644 --- a/lib/generators.js +++ b/lib/generators.js @@ -171,8 +171,6 @@ exports.list = function () { if (driver == 'mongoose') { attrs.push(property + ': { type: ' + type + ' }'); - } else if (driver == 'riak'){ - attrs.push(property + ': ' + type); } else { attrs.push(' property("' + property + '", ' + type + ');'); @@ -192,18 +190,6 @@ exports.list = function () { code = ''; } - else if (driver == 'riak'){ - schema = '\n\n/**\n * ' + Model + '\n */\n'; - schema += 'var ' + Model + ' = Model.create({\n'; - schema += 'schema: { ' + attrs.join(',\n ') + '\n},\n'; - schema += 'primaryKey: "' + camelize(Model, true) + 'Id",\n'; - schema += 'bucket: "' + camelize(pluralize(Model)) + '"\n'; - schema += '});\n\n'; - schema += 'module.exports["' + Model + '"] = ' + Model + ';'; - appendToFile('db/schema.js', schema); - - code = ''; - } else { code = 'var ' + Model + ' = describe("' + Model + '", function () {\n' + attrs.join('\n') + '\n});'; diff --git a/templates/config/database_riak.json b/templates/config/database_riak.json new file mode 100644 index 0000000..71e80e0 --- /dev/null +++ b/templates/config/database_riak.json @@ -0,0 +1,25 @@ +{ "development": + { "driver": "riak" + , "host": "localhost" + , "port": "8098" + , "database": "APPNAME-dev" + } +, "test": + { "driver": "riak" + , "host": "localhost" + , "port": "8098" + , "database": "APPNAME-test" + } +, "staging": + { "driver": "riak" + , "host": "localhost" + , "port": "8098" + , "database": "APPNAME-staging" + } +, "production": + { "driver": "riak" + , "host": "localhost" + , "port": "8098" + , "database": "APPNAME-production" + } +} \ No newline at end of file diff --git a/templates/crud_controller_riak.coffee b/templates/crud_controller_riak.coffee new file mode 100644 index 0000000..61df034 --- /dev/null +++ b/templates/crud_controller_riak.coffee @@ -0,0 +1,57 @@ +before -> + Model.findById req.params['id'], (error, model) => + if error + redirect path_to.models + else + @model = model + next() +, only: ['show', 'edit', 'update', 'destroy'] + +action 'new', -> + @model = new Model + @title = 'New model' + render() + +action 'create', -> + Model.create req.body, (error, model) => + if error + flash 'error', 'Model can not be created' + @model = model + @title = 'New model' + render 'new' + else + flash 'info', 'Model created' + redirect path_to.models + +action 'index', -> + Model.allInstances (error, models) => + @models = models + @title = 'Models index' + render() + +action 'show', -> + @title = 'Model show' + render() + +action 'edit', -> + @title = 'Model edit' + render() + +action 'update', -> + @model.save req.body, (error, model) => + if error + flash 'error', 'Model can not be updated' + @title = 'Edit model details' + render 'edit' + else + flash 'info', 'Model updated' + redirect path_to.model(@model) + +action 'destroy', -> + @model.destroy (error, success) -> + if error + flash 'error', 'Can not destroy model' + else + flash 'info', 'Model successfully removed' + send "'" + path_to.models + "'" + diff --git a/templates/crud_controller_riak.js b/templates/crud_controller_riak.js new file mode 100644 index 0000000..90802c7 --- /dev/null +++ b/templates/crud_controller_riak.js @@ -0,0 +1,76 @@ +before(loadModel, {only: ['show', 'edit', 'update', 'destroy']}); + +action('new', function () { + this.title = 'New model'; + this.model = new Model; + render(); +}); + +action('create', function () { + Model.create(req.body, function (error, model) { + if (error) { + flash('error', 'Model can not be created'); + render('new', { + model: model, + title: 'New model' + }); + } else { + flash('info', 'Model created'); + redirect(path_to.models); + } + }); +}); + +action('index', function () { + this.title = 'Models index'; + Model.allInstances(function (error, models) { + render({ + models: models, + }); + }); +}); + +action('show', function () { + this.title = 'Model show'; + render(); +}); + +action('edit', function () { + this.title = 'Model edit'; + render(); +}); + +action('update', function () { + this.model.save(req.body, function (error, model) { + if (error) { + flash('error', 'Model can not be updated'); + this.title = 'Edit model details'; + render('edit'); + } else { + flash('info', 'Model updated'); + redirect(path_to.model(this.model)); + } + }.bind(this)); +}); + +action('destroy', function () { + this.model.destroy(function (error, success) { + if (error) { + flash('error', 'Can not destroy model'); + } else { + flash('info', 'Model successfully removed'); + } + send("'" + path_to.models + "'"); + }); +}); + +function loadModel () { + Model.findById(req.params['id'], function (error, model) { + if (error) { + redirect(path_to.models); + } else { + this.model = model; + next(); + } + }.bind(this)); +} From 19ad58b645fa72a66b655552e0784ade35913aa7 Mon Sep 17 00:00:00 2001 From: Taliesin Sisson Date: Tue, 9 Aug 2011 20:15:17 +0100 Subject: [PATCH 6/9] Fix problems with callback being called in this context as it was not necessary --- lib/datamapper/riak.js | 112 ++++++++++++++------------ templates/crud_controller_riak.coffee | 2 +- templates/crud_controller_riak.js | 2 +- 3 files changed, 62 insertions(+), 54 deletions(-) diff --git a/lib/datamapper/riak.js b/lib/datamapper/riak.js index 79f0431..8538921 100644 --- a/lib/datamapper/riak.js +++ b/lib/datamapper/riak.js @@ -170,10 +170,8 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope } riak.save(table_name, this[primary_key], insertedProperties, function(error, data, meta) { - callback.call(error, this); + return callback(error, this); }.bind(this)); - - return; } var updatedProperties = {}; @@ -187,8 +185,7 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope }.bind(this)); if (updatedProperties.length < 1) { - callback.call(undef, this); - return; + return callback(undef, this); } if (properties.hasOwnProperty('updated_at')) { @@ -197,7 +194,7 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope } riak.update(table_name, this[primary_key], updatedProperties, function(error, data, meta) { - callback.call(error, this); + return callback(error, this); }.bind(this)); }; @@ -209,22 +206,21 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope Model.prototype.reload = function (callback) { if (this.isNewRecord()) { if (typeof callback == 'function') { - callback.call(undef, this); + return callback(undef, this); } return; } var key = this[primary_key]; riak.get(table_name, key, function(error, result, meta) { - if (!error) { - var record = result; - record[primary_key] = key; - - var model = Model.instantiate(record); - callback.call(error, model); - } - else { - callback.call(error, null); + if (error) { + return callback(error, null); } + + var record = result; + record[primary_key] = key; + + var model = Model.instantiate(record); + return callback(error, model); }); }; @@ -235,7 +231,7 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope */ Model.all = function (callback) { riak.keys(table_name, function (error, keys) { - callback.call(error, keys); + return callback(error, keys); }); }; @@ -278,7 +274,7 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope Model.exists = function (key, callback) { riak.exists(table_name, key, function (error, exists, meta) { if (typeof callback == 'function') { - callback(error, exists); + return callback(error, exists); } }); }; @@ -287,24 +283,33 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope /** * TODO test * Returns all models for a table in the database + * @param {Object} options - initial property values * @param {Function} callback(error, models) */ - Model.allInstances = function(callback) { - riak.getAll(table_name, function (error, result, meta) { - var models = []; - if (!error) { - result = result || []; - result.forEach(function (row) { - var key = row['meta']['key']; - var record = row['data']; - record[primary_key] = key; - - var model = Model.instantiate(record); - models.push(model); - }); + Model.allInstances = function(options, callback) { + if (arguments.length == 1) { + callback = options; + options = {}; + } + + riak.getAll(table_name, options, function (error, result, meta) { + if (error) + { + return callback(error, null); } - callback.call(error, models); + var models = []; + result = result || []; + result.forEach(function (row) { + var key = row['meta']['key']; + var record = row['data']; + record[primary_key] = key; + + var model = Model.instantiate(record); + models.push(model); + }); + + return callback(error, models); }); }; @@ -330,16 +335,15 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope } riak.get(table_name, key, function(error, result, meta) { - if (!error) { - var record = result; - record[primary_key] = key; - - var model = Model.instantiate(record); - callback.call(error, model); - } - else { - callback.call(error, null); + if (error) { + return callback(error, null); } + + var record = result; + record[primary_key] = key; + + var model = Model.instantiate(record); + return callback(error, model); }); }; @@ -371,12 +375,14 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope */ Model.destroy = function (callback) { riak.remove(table_name, this[primary_key], function(error, success, meta) { - if (!error) { - delete cache[this[primary_key]]; - delete this; + if (error) { + return callback(error, false); } - callback(error, success); + delete cache[this[primary_key]]; + delete this; + + return callback(error, success); }.bind(this)); }; @@ -388,16 +394,18 @@ exports.mixPersistMethods = function (Model, description) { // model_name, prope */ Model.destroyById = function (key, callback) { riak.remove(table_name, key, function(error, success, meta) { - if (!error) { - var model = cache[key]; + if (error) { + return callback(error, false); + } - if (model) - { - delete cache[key]; - } + var model = cache[key]; + + if (model) + { + delete cache[key]; } - callback(error, success); + return callback(error, success); }); }; }; \ No newline at end of file diff --git a/templates/crud_controller_riak.coffee b/templates/crud_controller_riak.coffee index 61df034..83f2a78 100644 --- a/templates/crud_controller_riak.coffee +++ b/templates/crud_controller_riak.coffee @@ -1,6 +1,6 @@ before -> Model.findById req.params['id'], (error, model) => - if error + if error or not model redirect path_to.models else @model = model diff --git a/templates/crud_controller_riak.js b/templates/crud_controller_riak.js index 90802c7..b15862b 100644 --- a/templates/crud_controller_riak.js +++ b/templates/crud_controller_riak.js @@ -66,7 +66,7 @@ action('destroy', function () { function loadModel () { Model.findById(req.params['id'], function (error, model) { - if (error) { + if (error || !model) { redirect(path_to.models); } else { this.model = model; From 3694b73084fd9a2f3decdb46b4bdcc7e8bd7cfc7 Mon Sep 17 00:00:00 2001 From: Taliesin Sisson Date: Fri, 12 Aug 2011 07:05:15 +0100 Subject: [PATCH 7/9] Add controller tests --- templates/crud_controller_test_riak.coffee | 111 ++++++++++++++++++ templates/crud_controller_test_riak.js | 126 +++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 templates/crud_controller_test_riak.coffee create mode 100644 templates/crud_controller_test_riak.js diff --git a/templates/crud_controller_test_riak.coffee b/templates/crud_controller_test_riak.coffee new file mode 100644 index 0000000..d00c1a1 --- /dev/null +++ b/templates/crud_controller_test_riak.coffee @@ -0,0 +1,111 @@ +require('../test_helper.js').controller 'models', module.exports + +sinon = require('sinon') + +ValidAttributes = -> + { + VALID_ATTRIBUTES + } + +module.exports['models controller'] = { + 'GET new': (test) -> + test.get '/models/new', -> + test.assign 'title', 'New model' + test.assign 'model' + test.success() + test.render 'new' + test.render 'form.' + app.set('view engine') + test.done() + + 'GET index': (test) -> + test.get '/models', -> + test.success() + test.render 'index' + test.done() + + 'GET edit': (test) -> + find = Model.findById + Model.findById = sinon.spy (id, cb) -> cb null, new Model + + test.get '/models/42/edit', -> + test.ok Model.findById.calledWith('42') + Model.findById = find + test.success() + test.render 'edit' + test.done() + + 'GET show': (test) -> + find = Model.findById + Model.findById = sinon.spy (id, cb) -> cb null, new Model + + test.get '/models/42', (req, res) -> + test.ok Model.findById.calledWith('42') + Model.findById = find + test.success() + test.render('show') + test.done() + + 'POST create': (test) -> + model = new ValidAttributes + create = Model.create + Model.create = sinon.spy (data, callback) -> + test.strictEqual data, model + callback 1 + + test.post '/models', model, () -> + Model.create = create + test.redirect '/models' + test.flash 'info' + test.done() + + 'POST create fail': (test) -> + model = new ValidAttributes + create = Model.create + Model.create = sinon.spy (data, callback) -> + test.strictEqual data, model + callback null + + test.post '/models', model, () -> + Model.create = create + test.success() + test.render('new') + test.flash('error') + test.done() + + 'PUT update': (test) -> + find = Model.findById + Model.findById = sinon.spy (id, callback) -> + test.equal id, 1 + callback null, + id: 1 + save: (cb) -> cb(null) + + test.put '/models/1', new ValidAttributes, () -> + Model.findById = find + test.redirect '/models/1' + test.flash 'info' + test.done() + + 'PUT update fail': (test) -> + find = Model.findById + Model.findById = sinon.spy (id, callback) -> + test.equal id, 1 + callback null, + id: 1 + save: (cb) -> cb new Error + + test.put '/models/1', new ValidAttributes, () -> + Model.findById = find + test.success() + test.render 'edit' + test.flash 'error' + test.done() + + 'DELETE destroy': (test) -> + test.done() + + 'DELETE destroy fail': (test) -> + test.done() + +} + diff --git a/templates/crud_controller_test_riak.js b/templates/crud_controller_test_riak.js new file mode 100644 index 0000000..cb34f2f --- /dev/null +++ b/templates/crud_controller_test_riak.js @@ -0,0 +1,126 @@ +require('../test_helper.js').controller('models', module.exports); + +var sinon = require('sinon'); + +function ValidAttributes () { + return { + VALID_ATTRIBUTES + }; +} + +exports['models controller'] = { + + 'GET new': function (test) { + test.get('/models/new', function () { + test.success(); + test.render('new'); + test.render('form.' + app.set('view engine')); + test.done(); + }); + }, + + 'GET index': function (test) { + test.get('/models', function () { + test.success(); + test.render('index'); + test.done(); + }); + }, + + 'GET edit': function (test) { + var find = Model.findById; + Model.findById = sinon.spy(function (id, callback) { + callback(null, new Model); + }); + test.get('/models/42/edit', function () { + test.ok(Model.findById.calledWith('42')); + Model.findById = find; + test.success(); + test.render('edit'); + test.done(); + }); + }, + + 'GET show': function (test) { + var find = Model.findById; + Model.findById = sinon.spy(function (id, callback) { + callback(null, new Model); + }); + test.get('/models/42', function (req, res) { + test.ok(Model.findById.calledWith('42')); + Model.findById = find; + test.success(); + test.render('show'); + test.done(); + }); + }, + + 'POST create': function (test) { + var model = new ValidAttributes; + var create = Model.create; + Model.create = sinon.spy(function (data, callback) { + test.strictEqual(data, model); + callback(1); + }); + test.post('/models', model, function () { + Model.create = create; + test.redirect('/models'); + test.flash('info'); + test.done(); + }); + }, + + 'POST create fail': function (test) { + var model = new ValidAttributes; + var create = Model.create; + Model.create = sinon.spy(function (data, callback) { + test.strictEqual(data, model); + callback(null); + }); + test.post('/models', model, function () { + Model.create = create; + test.success(); + test.render('new'); + test.flash('error'); + test.done(); + }); + }, + + 'PUT update': function (test) { + var find = Model.findById; + Model.findById = sinon.spy(function (id, callback) { + test.equal(id, 1); + callback(null, {id: 1, save: function (cb) { cb(null); }}); + }); + test.put('/models/1', new ValidAttributes, function () { + Model.findById = find; + test.redirect('/models/1'); + test.flash('info'); + test.done(); + }); + }, + + 'PUT update fail': function (test) { + var find = Model.findById; + Model.findById = sinon.spy(function (id, callback) { + test.equal(id, 1); + callback(null, {id: 1, save: function (cb) { cb(new Error); }}); + }); + test.put('/models/1', new ValidAttributes, function () { + Model.findById = find; + test.success(); + test.render('edit'); + test.flash('error'); + test.done(); + }); + }, + + 'DELETE destroy': function (test) { + test.done(); + }, + + 'DELETE destroy fail': function (test) { + test.done(); + } +}; + From 47d5e21a5e71c1df3133c9aa553c61f792822166 Mon Sep 17 00:00:00 2001 From: Taliesin Sisson Date: Thu, 18 Aug 2011 10:00:50 +0100 Subject: [PATCH 8/9] Add method to test send was done as expected Fixed the tests so they now execute Added tests for delete on controller --- templates/crud_controller_test_riak.coffee | 113 ++++++++++-------- templates/crud_controller_test_riak.js | 132 ++++++++++++++------- templates/test_helper.js | 27 +++-- 3 files changed, 172 insertions(+), 100 deletions(-) diff --git a/templates/crud_controller_test_riak.coffee b/templates/crud_controller_test_riak.coffee index d00c1a1..7035fe9 100644 --- a/templates/crud_controller_test_riak.coffee +++ b/templates/crud_controller_test_riak.coffee @@ -9,7 +9,7 @@ ValidAttributes = -> module.exports['models controller'] = { 'GET new': (test) -> - test.get '/models/new', -> + test.get '/models/new', (req, res) -> test.assign 'title', 'New model' test.assign 'model' test.success() @@ -18,94 +18,113 @@ module.exports['models controller'] = { test.done() 'GET index': (test) -> - test.get '/models', -> + test.get '/models', (req, res) -> test.success() test.render 'index' test.done() 'GET edit': (test) -> - find = Model.findById - Model.findById = sinon.spy (id, cb) -> cb null, new Model + Model.findById = sinon.spy (key, callback) => + test.strictEqual key, '42' + model = new Model ValidAttributes + model.id = '42' + callback null, model - test.get '/models/42/edit', -> - test.ok Model.findById.calledWith('42') - Model.findById = find + test.get '/models/42/edit', (req, res) -> test.success() test.render 'edit' test.done() 'GET show': (test) -> - find = Model.findById - Model.findById = sinon.spy (id, cb) -> cb null, new Model + Model.findById = sinon.spy (key, callback) => + test.strictEqual key, '42' + model = new Model ValidAttributes + model.id = '42' + callback null, model test.get '/models/42', (req, res) -> - test.ok Model.findById.calledWith('42') - Model.findById = find test.success() test.render('show') test.done() 'POST create': (test) -> - model = new ValidAttributes - create = Model.create Model.create = sinon.spy (data, callback) -> - test.strictEqual data, model - callback 1 + test.deepEqual data, new ValidAttributes + model = new Model data + callback null, model - test.post '/models', model, () -> - Model.create = create + test.post '/models', new ValidAttributes, (req, res) -> test.redirect '/models' test.flash 'info' test.done() 'POST create fail': (test) -> - model = new ValidAttributes - create = Model.create Model.create = sinon.spy (data, callback) -> - test.strictEqual data, model - callback null + test.deepEqual data, new ValidAttributes + model = new Model data + callback true, model - test.post '/models', model, () -> - Model.create = create + test.post '/models', new ValidAttributes, (req, res) -> test.success() test.render('new') test.flash('error') test.done() 'PUT update': (test) -> - find = Model.findById - Model.findById = sinon.spy (id, callback) -> - test.equal id, 1 - callback null, - id: 1 - save: (cb) -> cb(null) - - test.put '/models/1', new ValidAttributes, () -> - Model.findById = find - test.redirect '/models/1' + Model.findById = sinon.spy (key, callback) => + test.strictEqual key, '42' + model = new Model ValidAttributes + model.id = '42' + model.save = sinon.spy (data, callback) -> + test.notStrictEqual model.Id, '42' + callback null, model + callback null, model + + test.put '/models/42', new ValidAttributes, (req, res) -> + test.redirect '/models/42' test.flash 'info' test.done() 'PUT update fail': (test) -> - find = Model.findById - Model.findById = sinon.spy (id, callback) -> - test.equal id, 1 - callback null, - id: 1 - save: (cb) -> cb new Error - - test.put '/models/1', new ValidAttributes, () -> - Model.findById = find + Model.findById = sinon.spy (key, callback) -> + test.strictEqual key, '42' + model = new Model ValidAttributes + model.id = '42' + model.save = sinon.spy (data, callback) -> + callback true, model + callback null, model + + test.put '/models/42', new ValidAttributes, (req, res) -> test.success() test.render 'edit' test.flash 'error' test.done() 'DELETE destroy': (test) -> - test.done() + Model.findById = sinon.spy (key, callback) -> + test.strictEqual key, '42' + model = new Model ValidAttributes + model.id = '42' + model.destroy = sinon.spy (callback) -> + callback null, true + callback null, model + + test.del '/models/42', {}, (req, res) -> + test.send('/models') + test.flash 'info' + test.done() 'DELETE destroy fail': (test) -> - test.done() - -} - + Model.findById = sinon.spy (key, callback) -> + test.strictEqual key, '42' + model = new Model ValidAttributes + model.id = '42' + model.destroy = sinon.spy (callback) -> + callback true, false + callback null, model + + test.del '/models/42', {}, (req, res) -> + test.send('/models') + test.flash 'error' + test.done() +} \ No newline at end of file diff --git a/templates/crud_controller_test_riak.js b/templates/crud_controller_test_riak.js index cb34f2f..4d5fa27 100644 --- a/templates/crud_controller_test_riak.js +++ b/templates/crud_controller_test_riak.js @@ -11,7 +11,9 @@ function ValidAttributes () { exports['models controller'] = { 'GET new': function (test) { - test.get('/models/new', function () { + test.get('/models/new', function(req, res) { + test.assign('title', 'New model'); + test.assign('model'); test.success(); test.render('new'); test.render('form.' + app.set('view engine')); @@ -20,7 +22,7 @@ exports['models controller'] = { }, 'GET index': function (test) { - test.get('/models', function () { + test.get('/models', function(req, res) { test.success(); test.render('index'); test.done(); @@ -28,13 +30,14 @@ exports['models controller'] = { }, 'GET edit': function (test) { - var find = Model.findById; - Model.findById = sinon.spy(function (id, callback) { - callback(null, new Model); + Model.findById = sinon.spy(function(key, callback) { + test.strictEqual(key, '42'); + var model = new Model(ValidAttributes); + model.id = '42'; + callback(null, model); }); - test.get('/models/42/edit', function () { - test.ok(Model.findById.calledWith('42')); - Model.findById = find; + + test.get('/models/42/edit', function(req, res) { test.success(); test.render('edit'); test.done(); @@ -42,13 +45,14 @@ exports['models controller'] = { }, 'GET show': function (test) { - var find = Model.findById; - Model.findById = sinon.spy(function (id, callback) { - callback(null, new Model); + Model.findById = sinon.spy(function(key, callback) { + test.strictEqual(key, '42'); + var model = new Model(ValidAttributes); + model.id = '42'; + callback(null, model); }); - test.get('/models/42', function (req, res) { - test.ok(Model.findById.calledWith('42')); - Model.findById = find; + + test.get('/models/42', function(req, res) { test.success(); test.render('show'); test.done(); @@ -56,14 +60,13 @@ exports['models controller'] = { }, 'POST create': function (test) { - var model = new ValidAttributes; - var create = Model.create; - Model.create = sinon.spy(function (data, callback) { - test.strictEqual(data, model); - callback(1); - }); - test.post('/models', model, function () { - Model.create = create; + Model.create = sinon.spy(function(data, callback) { + test.deepEqual(data, new ValidAttributes); + var model = new Model(data); + callback(null, model); + }); + + test.post('/models', model, function(req, res) { test.redirect('/models'); test.flash('info'); test.done(); @@ -71,14 +74,13 @@ exports['models controller'] = { }, 'POST create fail': function (test) { - var model = new ValidAttributes; - var create = Model.create; - Model.create = sinon.spy(function (data, callback) { - test.strictEqual(data, model); - callback(null); - }); - test.post('/models', model, function () { - Model.create = create; + Model.create = sinon.spy(function(data, callback) { + test.deepEqual(data, new ValidAttributes); + var model = new Model(data); + callback(true, model); + }); + + test.post('/models', model, function(req, res) { test.success(); test.render('new'); test.flash('error'); @@ -87,27 +89,37 @@ exports['models controller'] = { }, 'PUT update': function (test) { - var find = Model.findById; - Model.findById = sinon.spy(function (id, callback) { - test.equal(id, 1); - callback(null, {id: 1, save: function (cb) { cb(null); }}); - }); - test.put('/models/1', new ValidAttributes, function () { - Model.findById = find; - test.redirect('/models/1'); + Model.findById = sinon.spy(function(key, callback){ + test.strictEqual(key, '42'); + var model = new Model(ValidAttributes); + model.id = '42'; + model.save = sinon.spy(function(data, callback){ + test.notStrictEqual(model.Id, '42'); + callback(null, model); + }); + callback(null, model); + }); + + test.put('/models/42', new ValidAttributes, function(req, res) { + test.redirect('/models/42'); test.flash('info'); test.done(); }); }, 'PUT update fail': function (test) { - var find = Model.findById; - Model.findById = sinon.spy(function (id, callback) { - test.equal(id, 1); - callback(null, {id: 1, save: function (cb) { cb(new Error); }}); + Model.findById = sinon.spy(function(key, callback){ + test.strictEqual(key, '42'); + var model = new Model(ValidAttributes); + model.id = '42'; + model.save = sinon.spy(function(data, callback){ + test.notStrictEqual(model.Id, '42'); + callback(true, model); + }); + callback(null, model); }); - test.put('/models/1', new ValidAttributes, function () { - Model.findById = find; + + test.put('/models/42', new ValidAttributes, function(req, res) { test.success(); test.render('edit'); test.flash('error'); @@ -116,11 +128,39 @@ exports['models controller'] = { }, 'DELETE destroy': function (test) { - test.done(); + Model.findById = sinon.spy(function(key, callback) { + test.strictEqual(key, '42'); + var model = new Model(ValidAttributes); + model.id = '42'; + model.destroy = sinon.spy(function(callback) { + callback(null, true); + }); + callback(null, model); + }); + + test.del('/models/42', {}, function(req, res) { + test.send('/models'); + test.flash('info'); + test.done(); + }); }, 'DELETE destroy fail': function (test) { - test.done(); + Model.findById = sinon.spy(function(key, callback) { + test.strictEqual(key, '42'); + var model = new Model(ValidAttributes); + model.id = '42'; + model.destroy = sinon.spy(function(callback) { + callback(true, false); + }); + callback(null, model); + }); + + test.del('/models/42', {}, function(req, res) { + test.send('/models'); + test.flash('error'); + test.done(); + }); } }; diff --git a/templates/test_helper.js b/templates/test_helper.js index b0ec9c2..765134c 100644 --- a/templates/test_helper.js +++ b/templates/test_helper.js @@ -30,7 +30,14 @@ exports.controller = function (controllerName, exp) { if (realPath !== path) { assert.fail(realPath, path, message || 'Wrong location', '===', assert.redirect); } - } + }; + + assert.send = function (path, message) { + if (this.res.statusCode !== 200) { + assert.fail(this.res.statusCode, 200, 'Status code is not 200', '===', assert.redirect); + } + this.res.send.calledWithExactly(path); + }; assert.success = function (template, message) { if (this.res.statusCode !== 200) { @@ -84,8 +91,6 @@ exports.controller = function (controllerName, exp) { return test; }; - - app.enable('quiet'); app.enable('models cache'); @@ -110,10 +115,18 @@ exports.controller = function (controllerName, exp) { req.connection = {}; req.url = url; req.method = method; - if (method === 'POST' && typeof callback === 'object') { - req.body = callback; - if (_method) req.body._method = _method; - callback = arguments[2]; + + if (method === 'POST') { + if (typeof callback === 'object') + { + req.body = callback; + callback = arguments[2]; + } + + if (_method) + { + req.body._method = _method; + } } res.end = function () { From 337fe14eedff0704046717b71ef8afcf006206b9 Mon Sep 17 00:00:00 2001 From: Taliesin Sisson Date: Thu, 18 Aug 2011 11:04:30 +0100 Subject: [PATCH 9/9] Generator can use riak controller tests Pass in correct body for post tests in js --- lib/generators.js | 5 +++-- templates/crud_controller_test_riak.js | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/generators.js b/lib/generators.js index eaede8d..db3fbc7 100644 --- a/lib/generators.js +++ b/lib/generators.js @@ -326,8 +326,9 @@ exports.list = function () { createFileByTemplate('test/test_helper.js', 'test_helper.js'); createDir('test/controllers'); createParents(ns, 'test/controllers/'); - if (driver === 'mongoose') { - createFileByTemplate('test/controllers/' + models + '_controller_test', 'crud_controller_test_mongoose', replaceModel); + + if (driver === 'mongoose' || driver === 'riak') { + createFileByTemplate('test/controllers/' + models + '_controller_'+driver, 'crud_controller_test_'+driver, replaceModel); } else { createFileByTemplate('test/controllers/' + models + '_controller_test.js', 'crud_controller_test_redis.js', replaceModel); } diff --git a/templates/crud_controller_test_riak.js b/templates/crud_controller_test_riak.js index 4d5fa27..5009a6c 100644 --- a/templates/crud_controller_test_riak.js +++ b/templates/crud_controller_test_riak.js @@ -66,7 +66,7 @@ exports['models controller'] = { callback(null, model); }); - test.post('/models', model, function(req, res) { + test.post('/models', new ValidAttributes, function(req, res) { test.redirect('/models'); test.flash('info'); test.done(); @@ -80,7 +80,7 @@ exports['models controller'] = { callback(true, model); }); - test.post('/models', model, function(req, res) { + test.post('/models', new ValidAttributes, function(req, res) { test.success(); test.render('new'); test.flash('error');