From ef7b724375d8c61b2d9abb21ad657100e1c903d2 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Wed, 19 Feb 2014 11:44:16 -0800 Subject: [PATCH 1/9] Initial client-server example --- example/client-server/client.js | 21 ++++++++ example/client-server/models.js | 35 +++++++++++++ example/client-server/server.js | 24 +++++++++ index.js | 1 + lib/connectors/remote.js | 87 +++++++++++++++++++++++++++++++++ 5 files changed, 168 insertions(+) create mode 100644 example/client-server/client.js create mode 100644 example/client-server/models.js create mode 100644 example/client-server/server.js create mode 100644 lib/connectors/remote.js diff --git a/example/client-server/client.js b/example/client-server/client.js new file mode 100644 index 00000000..458785b7 --- /dev/null +++ b/example/client-server/client.js @@ -0,0 +1,21 @@ +var loopback = require('../../'); +var client = loopback(); +var CartItem = require('./models').CartItem; +var remote = loopback.createDataSource({ + connector: loopback.Remote, + root: 'http://localhost:3000', + remotes: client.remotes() +}); + +client.model(CartItem); +CartItem.attachTo(remote); + +// call the remote method +CartItem.sum(1, function(err, total) { + console.log('result:', err || total); +}); + +// call a built in remote method +CartItem.find(function(err, items) { + console.log(items); +}); diff --git a/example/client-server/models.js b/example/client-server/models.js new file mode 100644 index 00000000..a18ab2e2 --- /dev/null +++ b/example/client-server/models.js @@ -0,0 +1,35 @@ +var loopback = require('../../'); + +var CartItem = exports.CartItem = loopback.Model.extend('CartItem', { + tax: {type: Number, default: 0.1}, + price: Number, + item: String, + qty: {type: Number, default: 0}, + cartId: Number +}); + +CartItem.sum = function(cartId, callback) { + this.find({where: {cartId: 1}}, function(err, items) { + var total = items + .map(function(item) { + return item.total(); + }) + .reduce(function(cur, prev) { + return prev + cur; + }, 0); + + callback(null, total); + }); +} + +loopback.remoteMethod( + CartItem.sum, + { + accepts: {arg: 'cartId', type: 'number'}, + returns: {arg: 'total', type: 'number'} + } +); + +CartItem.prototype.total = function() { + return this.price * this.qty * 1 + this.tax; +} diff --git a/example/client-server/server.js b/example/client-server/server.js new file mode 100644 index 00000000..4ab292e7 --- /dev/null +++ b/example/client-server/server.js @@ -0,0 +1,24 @@ +var loopback = require('../../'); +var server = module.exports = loopback(); +var CartItem = require('./models').CartItem; +var memory = loopback.createDataSource({ + connector: loopback.Memory +}); + +server.use(loopback.rest()); +server.model(CartItem); + +CartItem.attachTo(memory); + +// test data +// CartItem.create([ +// {item: 'red hat', qty: 6, price: 19.99, cartId: 1}, +// {item: 'green shirt', qty: 1, price: 14.99, cartId: 1}, +// {item: 'orange pants', qty: 58, price: 9.99, cartId: 1} +// ]); + +// CartItem.sum(1, function(err, total) { +// console.log(total); +// }) + +server.listen(3000); diff --git a/index.js b/index.js index d9099a70..739fa4f5 100644 --- a/index.js +++ b/index.js @@ -12,6 +12,7 @@ var datasourceJuggler = require('loopback-datasource-juggler'); loopback.Connector = require('./lib/connectors/base-connector'); loopback.Memory = require('./lib/connectors/memory'); loopback.Mail = require('./lib/connectors/mail'); +loopback.Remote = require('./lib/connectors/remote'); /** * Types diff --git a/lib/connectors/remote.js b/lib/connectors/remote.js new file mode 100644 index 00000000..6f2f7e15 --- /dev/null +++ b/lib/connectors/remote.js @@ -0,0 +1,87 @@ +/** + * Dependencies. + */ + +var assert = require('assert') + , compat = require('../compat') + , _ = require('underscore'); + +/** + * Export the RemoteConnector class. + */ + +module.exports = RemoteConnector; + +/** + * Create an instance of the connector with the given `settings`. + */ + +function RemoteConnector(settings) { + assert(typeof settings === 'object', 'cannot initiaze RemoteConnector without a settings object'); + this.client = settings.client; + this.root = settings.root; + this.client = settings.client; + this.remotes = this.client.remotes(); + this.adapter = settings.adapter || 'rest'; + assert(this.client, 'RemoteConnector: settings.client is required'); + assert(this.root, 'RemoteConnector: settings.root is required'); + + // handle mixins here + this.DataAccessObject = function() {}; +} + +RemoteConnector.prototype.connect = function() { + this.remotes.connect(this.root, this.adapter); +} + +RemoteConnector.initialize = function(dataSource, callback) { + var connector = dataSource.connector = new RemoteConnector(dataSource.settings); + connector.connect(); + callback(); +} + +RemoteConnector.prototype.define = function(definition) { + var Model = definition.model; + var className = compat.getClassNameForRemoting(Model); + var sharedClass = getSharedClass(this.remotes, className); + + mixinRemoteMethods(this.remotes, Model, sharedClass.methods()); +} + +function getSharedClass(remotes, className) { + return _.find(remotes.classes(), function(sharedClass) { + return sharedClass.name === className; + }); +} + +function mixinRemoteMethods(remotes, Model, methods) { + methods.forEach(function(sharedMethod) { + var original = sharedMethod.fn; + var fn = createProxyFunction(remotes, sharedMethod.stringName); + for(var key in original) { + fn[key] = original[key]; + } + + if(sharedMethod.isStatic) { + Model[sharedMethod.name] = fn; + } else { + Model.prototype[sharedMethod.name] = fn; + } + }); +} + +function createProxyFunction(remotes, stringName) { + return function() { + var args = Array.prototype.slice.call(arguments); + var lastArgIsFunc = typeof args[args.length - 1] === 'function'; + var callback; + if(lastArgIsFunc) { + callback = args.pop(); + } else { + callback = noop; + } + remotes.invoke(stringName, args, callback); + } +} + +function noop() {} From dcdbef861e73b52a2718eb189afb3bbd2c740cc0 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Wed, 19 Feb 2014 17:09:36 -0800 Subject: [PATCH 2/9] Add Remote connector --- example/client-server/models.js | 2 +- example/client-server/server.js | 16 +- lib/connectors/remote.js | 4 +- lib/loopback.js | 2 + lib/models/data-model.js | 319 ++++++++++++++++++++++++++++++++ 5 files changed, 331 insertions(+), 12 deletions(-) create mode 100644 lib/models/data-model.js diff --git a/example/client-server/models.js b/example/client-server/models.js index a18ab2e2..c14485c8 100644 --- a/example/client-server/models.js +++ b/example/client-server/models.js @@ -1,6 +1,6 @@ var loopback = require('../../'); -var CartItem = exports.CartItem = loopback.Model.extend('CartItem', { +var CartItem = exports.CartItem = loopback.DataModel.extend('CartItem', { tax: {type: Number, default: 0.1}, price: Number, item: String, diff --git a/example/client-server/server.js b/example/client-server/server.js index 4ab292e7..7e466a56 100644 --- a/example/client-server/server.js +++ b/example/client-server/server.js @@ -11,14 +11,14 @@ server.model(CartItem); CartItem.attachTo(memory); // test data -// CartItem.create([ -// {item: 'red hat', qty: 6, price: 19.99, cartId: 1}, -// {item: 'green shirt', qty: 1, price: 14.99, cartId: 1}, -// {item: 'orange pants', qty: 58, price: 9.99, cartId: 1} -// ]); +CartItem.create([ + {item: 'red hat', qty: 6, price: 19.99, cartId: 1}, + {item: 'green shirt', qty: 1, price: 14.99, cartId: 1}, + {item: 'orange pants', qty: 58, price: 9.99, cartId: 1} +]); -// CartItem.sum(1, function(err, total) { -// console.log(total); -// }) +CartItem.sum(1, function(err, total) { + console.log(total); +}); server.listen(3000); diff --git a/lib/connectors/remote.js b/lib/connectors/remote.js index 6f2f7e15..da825912 100644 --- a/lib/connectors/remote.js +++ b/lib/connectors/remote.js @@ -20,10 +20,8 @@ function RemoteConnector(settings) { assert(typeof settings === 'object', 'cannot initiaze RemoteConnector without a settings object'); this.client = settings.client; this.root = settings.root; - this.client = settings.client; - this.remotes = this.client.remotes(); + this.remotes = settings.remotes; this.adapter = settings.adapter || 'rest'; - assert(this.client, 'RemoteConnector: settings.client is required'); assert(this.root, 'RemoteConnector: settings.root is required'); // handle mixins here diff --git a/lib/loopback.js b/lib/loopback.js index 6a057a44..bd1f2989 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -310,6 +310,7 @@ loopback.autoAttachModel = function(ModelCtor) { */ loopback.Model = require('./models/model'); +loopback.DataModel = require('./models/data-model'); loopback.Email = require('./models/email'); loopback.User = require('./models/user'); loopback.Application = require('./models/application'); @@ -329,6 +330,7 @@ var dataSourceTypes = { }; loopback.Email.autoAttach = dataSourceTypes.MAIL; +loopback.DataModel.autoAttach = dataSourceTypes.DB; loopback.User.autoAttach = dataSourceTypes.DB; loopback.AccessToken.autoAttach = dataSourceTypes.DB; loopback.Role.autoAttach = dataSourceTypes.DB; diff --git a/lib/models/data-model.js b/lib/models/data-model.js new file mode 100644 index 00000000..ffdd3195 --- /dev/null +++ b/lib/models/data-model.js @@ -0,0 +1,319 @@ +/*! + * Module Dependencies. + */ +var Model = require('./model'); + +/** + * Extends Model with basic query and CRUD support. + * + * @class DataModel + * @param {Object} data + */ + +var DataModel = module.exports = Model.extend('DataModel'); + +/*! + * Configure the remoting attributes for a given function + * @param {Function} fn The function + * @param {Object} options The options + * @private + */ + +function setRemoting(fn, options) { + options = options || {}; + for (var opt in options) { + if (options.hasOwnProperty(opt)) { + fn[opt] = options[opt]; + } + } + fn.shared = true; +} + +/*! + * Convert null callbacks to 404 error objects. + * @param {HttpContext} ctx + * @param {Function} cb + */ + +function convertNullToNotFoundError(ctx, cb) { + if (ctx.result !== null) return cb(); + + var modelName = ctx.method.sharedClass.name; + var id = ctx.getArgByName('id'); + var msg = 'Unkown "' + modelName + '" id "' + id + '".'; + var error = new Error(msg); + error.statusCode = error.status = 404; + cb(error); +} + +/** + * Create new instance of Model class, saved in database + * + * @param data [optional] + * @param callback(err, obj) + * callback called with arguments: + * + * - err (null or Error) + * - instance (null or Model) + */ + +DataModel.create = function (data, callback) { + +}; + +setRemoting(DataModel.create, { + description: 'Create a new instance of the model and persist it into the data source', + accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, + returns: {arg: 'data', type: 'object', root: true}, + http: {verb: 'post', path: '/'} +}); + +/** + * Update or insert a model instance + * @param {Object} data The model instance data + * @param {Function} [callback] The callback function + */ + +DataModel.upsert = DataModel.updateOrCreate = function upsert(data, callback) { + +}; + +// upsert ~ remoting attributes +setRemoting(DataModel.upsert, { + description: 'Update an existing model instance or insert a new one into the data source', + accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, + returns: {arg: 'data', type: 'object', root: true}, + http: {verb: 'put', path: '/'} +}); + +/** + * Find one record, same as `all`, limited by 1 and return object, not collection, + * if not found, create using data provided as second argument + * + * @param {Object} query - search conditions: {where: {test: 'me'}}. + * @param {Object} data - object to create. + * @param {Function} cb - callback called with (err, instance) + */ + +DataModel.findOrCreate = function findOrCreate(query, data, callback) { + +}; + +/** + * Check whether a model instance exists in database + * + * @param {id} id - identifier of object (primary key value) + * @param {Function} cb - callbacl called with (err, exists: Bool) + */ + +DataModel.exists = function exists(id, cb) { + +}; + +// exists ~ remoting attributes +setRemoting(DataModel.exists, { + description: 'Check whether a model instance exists in the data source', + accepts: {arg: 'id', type: 'any', description: 'Model id', required: true}, + returns: {arg: 'exists', type: 'any'}, + http: {verb: 'get', path: '/:id/exists'} +}); + +/** + * Find object by id + * + * @param {*} id - primary key value + * @param {Function} cb - callback called with (err, instance) + */ + +DataModel.findById = function find(id, cb) { + +}; + +// find ~ remoting attributes +setRemoting(DataModel.findById, { + description: 'Find a model instance by id from the data source', + accepts: {arg: 'id', type: 'any', description: 'Model id', required: true}, + returns: {arg: 'data', type: 'any', root: true}, + http: {verb: 'get', path: '/:id'}, + rest: {after: convertNullToNotFoundError} +}); + +/** + * Find all instances of Model, matched by query + * make sure you have marked as `index: true` fields for filter or sort + * + * @param {Object} params (optional) + * + * - where: Object `{ key: val, key2: {gt: 'val2'}}` + * - include: String, Object or Array. See DataModel.include documentation. + * - order: String + * - limit: Number + * - skip: Number + * + * @param {Function} callback (required) called with arguments: + * + * - err (null or Error) + * - Array of instances + */ + +DataModel.find = function find(params, cb) { + +}; + +// all ~ remoting attributes +setRemoting(DataModel.find, { + description: 'Find all instances of the model matched by filter from the data source', + accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, orderBy, offset, and limit'}, + returns: {arg: 'data', type: 'array', root: true}, + http: {verb: 'get', path: '/'} +}); + +/** + * Find one record, same as `all`, limited by 1 and return object, not collection + * + * @param {Object} params - search conditions: {where: {test: 'me'}} + * @param {Function} cb - callback called with (err, instance) + */ + +DataModel.findOne = function findOne(params, cb) { + +}; + +setRemoting(DataModel.findOne, { + description: 'Find first instance of the model matched by filter from the data source', + accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, orderBy, offset, and limit'}, + returns: {arg: 'data', type: 'object', root: true}, + http: {verb: 'get', path: '/findOne'} +}); + +/** + * Destroy all matching records + * @param {Object} [where] An object that defines the criteria + * @param {Function} [cb] - callback called with (err) + */ + +DataModel.remove = +DataModel.deleteAll = +DataModel.destroyAll = function destroyAll(where, cb) { + +}; + +/** + * Destroy a record by id + * @param {*} id The id value + * @param {Function} cb - callback called with (err) + */ + +DataModel.removeById = +DataModel.deleteById = +DataModel.destroyById = function deleteById(id, cb) { + +}; + +// deleteById ~ remoting attributes +setRemoting(DataModel.deleteById, { + description: 'Delete a model instance by id from the data source', + accepts: {arg: 'id', type: 'any', description: 'Model id', required: true}, + http: {verb: 'del', path: '/:id'} +}); + +/** + * Return count of matched records + * + * @param {Object} where - search conditions (optional) + * @param {Function} cb - callback, called with (err, count) + */ + +DataModel.count = function (where, cb) { + +}; + +// count ~ remoting attributes +setRemoting(DataModel.count, { + description: 'Count instances of the model matched by where from the data source', + accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'}, + returns: {arg: 'count', type: 'number'}, + http: {verb: 'get', path: '/count'} +}); + +/** + * Save instance. When instance haven't id, create method called instead. + * Triggers: validate, save, update | create + * @param options {validate: true, throws: false} [optional] + * @param callback(err, obj) + */ + +DataModel.prototype.save = function (options, callback) { + +}; + + +/** + * Determine if the data model is new. + * @returns {Boolean} + */ + +DataModel.prototype.isNewRecord = function () { + +}; + +/** + * Delete object from persistence + * + * @triggers `destroy` hook (async) before and after destroying object + */ + +DataModel.prototype.remove = +DataModel.prototype.delete = +DataModel.prototype.destroy = function (cb) { + +}; + +/** + * Update single attribute + * + * equals to `updateAttributes({name: value}, cb) + * + * @param {String} name - name of property + * @param {Mixed} value - value of property + * @param {Function} callback - callback called with (err, instance) + */ + +DataModel.prototype.updateAttribute = function updateAttribute(name, value, callback) { + +}; + +/** + * Update set of attributes + * + * this method performs validation before updating + * + * @trigger `validation`, `save` and `update` hooks + * @param {Object} data - data to update + * @param {Function} callback - callback called with (err, instance) + */ + +DataModel.prototype.updateAttributes = function updateAttributes(data, cb) { + +}; + +// updateAttributes ~ remoting attributes +setRemoting(DataModel.prototype.updateAttributes, { + description: 'Update attributes for a model instance and persist it into the data source', + accepts: {arg: 'data', type: 'object', http: {source: 'body'}, description: 'An object of model property name/value pairs'}, + returns: {arg: 'data', type: 'object', root: true}, + http: {verb: 'put', path: '/'} +}); + +/** + * Reload object from persistence + * + * @requires `id` member of `object` to be able to call `find` + * @param {Function} callback - called with (err, instance) arguments + */ + +DataModel.prototype.reload = function reload(callback) { + if (stillConnecting(this.getDataSource(), this, arguments)) return; + + this.constructor.findById(getIdValue(this.constructor, this), callback); +}; From 0632f524112810d2e6915230b94e2923e365bb78 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Wed, 19 Feb 2014 17:16:46 -0800 Subject: [PATCH 3/9] Remove reload method body --- lib/models/data-model.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/models/data-model.js b/lib/models/data-model.js index ffdd3195..3aba39d1 100644 --- a/lib/models/data-model.js +++ b/lib/models/data-model.js @@ -313,7 +313,5 @@ setRemoting(DataModel.prototype.updateAttributes, { */ DataModel.prototype.reload = function reload(callback) { - if (stillConnecting(this.getDataSource(), this, arguments)) return; - this.constructor.findById(getIdValue(this.constructor, this), callback); }; From 64b374907a8269e0ea0fc47b8cf8e3d8b1815d88 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Thu, 20 Feb 2014 19:43:50 -0800 Subject: [PATCH 4/9] Move proxy creation from remote connector into base model class --- example/client-server/client.js | 3 +- lib/connectors/remote.js | 45 +++++----------------- lib/models/model.js | 67 +++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 37 deletions(-) diff --git a/example/client-server/client.js b/example/client-server/client.js index 458785b7..4e1e423c 100644 --- a/example/client-server/client.js +++ b/example/client-server/client.js @@ -3,8 +3,7 @@ var client = loopback(); var CartItem = require('./models').CartItem; var remote = loopback.createDataSource({ connector: loopback.Remote, - root: 'http://localhost:3000', - remotes: client.remotes() + root: 'http://localhost:3000' }); client.model(CartItem); diff --git a/lib/connectors/remote.js b/lib/connectors/remote.js index da825912..203a7f87 100644 --- a/lib/connectors/remote.js +++ b/lib/connectors/remote.js @@ -20,7 +20,6 @@ function RemoteConnector(settings) { assert(typeof settings === 'object', 'cannot initiaze RemoteConnector without a settings object'); this.client = settings.client; this.root = settings.root; - this.remotes = settings.remotes; this.adapter = settings.adapter || 'rest'; assert(this.root, 'RemoteConnector: settings.root is required'); @@ -29,9 +28,9 @@ function RemoteConnector(settings) { } RemoteConnector.prototype.connect = function() { - this.remotes.connect(this.root, this.adapter); } + RemoteConnector.initialize = function(dataSource, callback) { var connector = dataSource.connector = new RemoteConnector(dataSource.settings); connector.connect(); @@ -41,9 +40,16 @@ RemoteConnector.initialize = function(dataSource, callback) { RemoteConnector.prototype.define = function(definition) { var Model = definition.model; var className = compat.getClassNameForRemoting(Model); - var sharedClass = getSharedClass(this.remotes, className); + var url = this.root; + var adapter = this.adapter; - mixinRemoteMethods(this.remotes, Model, sharedClass.methods()); + Model.remotes(function(err, remotes) { + var sharedClass = getSharedClass(remotes, className); + remotes.connect(url, adapter); + sharedClass + .methods() + .forEach(Model.createProxyMethod.bind(Model)); + }); } function getSharedClass(remotes, className) { @@ -51,35 +57,4 @@ function getSharedClass(remotes, className) { return sharedClass.name === className; }); } - -function mixinRemoteMethods(remotes, Model, methods) { - methods.forEach(function(sharedMethod) { - var original = sharedMethod.fn; - var fn = createProxyFunction(remotes, sharedMethod.stringName); - for(var key in original) { - fn[key] = original[key]; - } - - if(sharedMethod.isStatic) { - Model[sharedMethod.name] = fn; - } else { - Model.prototype[sharedMethod.name] = fn; - } - }); -} - -function createProxyFunction(remotes, stringName) { - return function() { - var args = Array.prototype.slice.call(arguments); - var lastArgIsFunc = typeof args[args.length - 1] === 'function'; - var callback; - if(lastArgIsFunc) { - callback = args.pop(); - } else { - callback = noop; - } - remotes.invoke(stringName, args, callback); - } -} - function noop() {} diff --git a/lib/models/model.js b/lib/models/model.js index 78df36f0..0170a198 100644 --- a/lib/models/model.js +++ b/lib/models/model.js @@ -200,6 +200,73 @@ Model._getAccessTypeForMethod = function(method) { } } +/** + * Get the `Application` the Model is attached to. + * + * @callback {Function} callback + * @param {Error} err + * @param {Application} app + * @end + */ + +Model.getApp = function(callback) { + var Model = this; + if(this.app) { + callback(null, this.app); + } else { + Model.once('attached', function() { + assert(Model.app); + callback(null, Model.app); + }); + } +} + +/** + * Get the Model's `RemoteObjects`. + * + * @callback {Function} callback + * @param {Error} err + * @param {RemoteObjects} remoteObjects + * @end + */ + +Model.remotes = function(callback) { + this.getApp(function(err, app) { + callback(null, app.remotes()); + }); +} + +/*! + * Create a proxy function for invoking remote methods. + * + * @param {SharedMethod} sharedMethod + */ + +Model.createProxyMethod = function createProxyFunction(remoteMethod) { + var Model = this; + var scope = remoteMethod.isStatic ? Model : Model.prototype; + var original = scope[remoteMethod.name]; + + var fn = scope[remoteMethod.name] = function proxy() { + var args = Array.prototype.slice.call(arguments); + var lastArgIsFunc = typeof args[args.length - 1] === 'function'; + var callback; + if(lastArgIsFunc) { + callback = args.pop(); + } else { + callback = noop; + } + + Model.remotes(function(err, remotes) { + remotes.invoke(remoteMethod.stringName, args, callback); + }); + } + + for(var key in original) { + fn[key] = original[key]; + } +} + // setup the initial model Model.setup(); From ac2206d257b39c04aaa479a8650a107a00b15f90 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 21 Feb 2014 09:10:48 -0800 Subject: [PATCH 5/9] Throw useful errors in DataModel stub methods --- lib/models/data-model.js | 46 ++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/lib/models/data-model.js b/lib/models/data-model.js index 3aba39d1..bb8c60cf 100644 --- a/lib/models/data-model.js +++ b/lib/models/data-model.js @@ -27,6 +27,20 @@ function setRemoting(fn, options) { } } fn.shared = true; + // allow connectors to override the function by marking as delegate + fn._delegate = true; +} + +/*! + * Throw an error telling the user that the method is not available and why. + */ + +function throwNotAttached(modelName, methodName) { + throw new Error( + 'Cannot call ' + modelName + '.'+ methodName + '().' + + ' The ' + methodName + ' method has not been setup.' + + ' The DataModel has not been correctly attached to a DataSource!' + ); } /*! @@ -58,7 +72,7 @@ function convertNullToNotFoundError(ctx, cb) { */ DataModel.create = function (data, callback) { - + throwNotAttached(this.modelName, 'create'); }; setRemoting(DataModel.create, { @@ -75,7 +89,7 @@ setRemoting(DataModel.create, { */ DataModel.upsert = DataModel.updateOrCreate = function upsert(data, callback) { - + throwNotAttached(this.modelName, 'updateOrCreate'); }; // upsert ~ remoting attributes @@ -96,7 +110,7 @@ setRemoting(DataModel.upsert, { */ DataModel.findOrCreate = function findOrCreate(query, data, callback) { - + throwNotAttached(this.modelName, 'findOrCreate'); }; /** @@ -107,7 +121,7 @@ DataModel.findOrCreate = function findOrCreate(query, data, callback) { */ DataModel.exists = function exists(id, cb) { - + throwNotAttached(this.modelName, 'exists'); }; // exists ~ remoting attributes @@ -126,7 +140,7 @@ setRemoting(DataModel.exists, { */ DataModel.findById = function find(id, cb) { - + throwNotAttached(this.modelName, 'find'); }; // find ~ remoting attributes @@ -157,7 +171,7 @@ setRemoting(DataModel.findById, { */ DataModel.find = function find(params, cb) { - + throwNotAttached(this.modelName, 'find'); }; // all ~ remoting attributes @@ -176,7 +190,7 @@ setRemoting(DataModel.find, { */ DataModel.findOne = function findOne(params, cb) { - + throwNotAttached(this.modelName, 'findOne'); }; setRemoting(DataModel.findOne, { @@ -195,7 +209,7 @@ setRemoting(DataModel.findOne, { DataModel.remove = DataModel.deleteAll = DataModel.destroyAll = function destroyAll(where, cb) { - + throwNotAttached(this.modelName, 'destroyAll'); }; /** @@ -207,7 +221,7 @@ DataModel.destroyAll = function destroyAll(where, cb) { DataModel.removeById = DataModel.deleteById = DataModel.destroyById = function deleteById(id, cb) { - + throwNotAttached(this.modelName, 'deleteById'); }; // deleteById ~ remoting attributes @@ -225,7 +239,7 @@ setRemoting(DataModel.deleteById, { */ DataModel.count = function (where, cb) { - + throwNotAttached(this.modelName, 'count'); }; // count ~ remoting attributes @@ -244,7 +258,7 @@ setRemoting(DataModel.count, { */ DataModel.prototype.save = function (options, callback) { - + throwNotAttached(this.constructor.modelName, 'save'); }; @@ -254,7 +268,7 @@ DataModel.prototype.save = function (options, callback) { */ DataModel.prototype.isNewRecord = function () { - + throwNotAttached(this.constructor.modelName, 'isNewRecord'); }; /** @@ -266,7 +280,7 @@ DataModel.prototype.isNewRecord = function () { DataModel.prototype.remove = DataModel.prototype.delete = DataModel.prototype.destroy = function (cb) { - + throwNotAttached(this.constructor.modelName, 'destroy'); }; /** @@ -280,7 +294,7 @@ DataModel.prototype.destroy = function (cb) { */ DataModel.prototype.updateAttribute = function updateAttribute(name, value, callback) { - + throwNotAttached(this.constructor.modelName, 'updateAttribute'); }; /** @@ -294,7 +308,7 @@ DataModel.prototype.updateAttribute = function updateAttribute(name, value, call */ DataModel.prototype.updateAttributes = function updateAttributes(data, cb) { - + throwNotAttached(this.modelName, 'updateAttributes'); }; // updateAttributes ~ remoting attributes @@ -313,5 +327,5 @@ setRemoting(DataModel.prototype.updateAttributes, { */ DataModel.prototype.reload = function reload(callback) { - + throwNotAttached(this.constructor.modelName, 'reload'); }; From 4c185e545315c0945886c7046fad6248ad8f1066 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 21 Feb 2014 15:03:45 -0800 Subject: [PATCH 6/9] Support host / port in Remote connector --- lib/connectors/remote.js | 14 +++++++++++--- lib/models/model.js | 3 +-- test/remote-connector.test.js | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 test/remote-connector.test.js diff --git a/lib/connectors/remote.js b/lib/connectors/remote.js index 203a7f87..065682f6 100644 --- a/lib/connectors/remote.js +++ b/lib/connectors/remote.js @@ -19,9 +19,17 @@ module.exports = RemoteConnector; function RemoteConnector(settings) { assert(typeof settings === 'object', 'cannot initiaze RemoteConnector without a settings object'); this.client = settings.client; - this.root = settings.root; this.adapter = settings.adapter || 'rest'; - assert(this.root, 'RemoteConnector: settings.root is required'); + this.protocol = settings.protocol || 'http' + this.root = settings.root || ''; + this.host = settings.host || 'localhost'; + this.port = settings.port || 3000; + + if(settings.url) { + this.url = settings.url; + } else { + this.url = this.protocol + '://' + this.host + ':' + this.port + this.root; + } // handle mixins here this.DataAccessObject = function() {}; @@ -40,7 +48,7 @@ RemoteConnector.initialize = function(dataSource, callback) { RemoteConnector.prototype.define = function(definition) { var Model = definition.model; var className = compat.getClassNameForRemoting(Model); - var url = this.root; + var url = this.url; var adapter = this.adapter; Model.remotes(function(err, remotes) { diff --git a/lib/models/model.js b/lib/models/model.js index 0170a198..b0c34250 100644 --- a/lib/models/model.js +++ b/lib/models/model.js @@ -253,8 +253,6 @@ Model.createProxyMethod = function createProxyFunction(remoteMethod) { var callback; if(lastArgIsFunc) { callback = args.pop(); - } else { - callback = noop; } Model.remotes(function(err, remotes) { @@ -265,6 +263,7 @@ Model.createProxyMethod = function createProxyFunction(remoteMethod) { for(var key in original) { fn[key] = original[key]; } + fn._delegate = true; } // setup the initial model diff --git a/test/remote-connector.test.js b/test/remote-connector.test.js new file mode 100644 index 00000000..de996b92 --- /dev/null +++ b/test/remote-connector.test.js @@ -0,0 +1,33 @@ +var loopback = require('../'); + +describe('RemoteConnector', function() { + beforeEach(function(done) { + var LocalModel = this.LocalModel = loopback.DataModel.extend('LocalModel'); + var RemoteModel = loopback.DataModel.extend('LocalModel'); + var localApp = loopback(); + var remoteApp = loopback(); + localApp.model(LocalModel); + remoteApp.model(RemoteModel); + remoteApp.use(loopback.rest()); + RemoteModel.attachTo(loopback.memory()); + remoteApp.listen(0, function() { + var ds = loopback.createDataSource({ + host: remoteApp.get('host'), + port: remoteApp.get('port'), + connector: loopback.Remote + }); + + LocalModel.attachTo(ds); + done(); + }); + }); + + it('should alow methods to be called remotely', function (done) { + var data = {foo: 'bar'}; + this.LocalModel.create(data, function(err, result) { + if(err) return done(err); + expect(result).to.deep.equal({id: 1, foo: 'bar'}); + done(); + }); + }); +}); From 875e5c1e194ef08d4e8eb6451ad4fe6e2838b86e Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Thu, 27 Mar 2014 15:47:03 -0700 Subject: [PATCH 7/9] Depend on strong-remoting 1.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 42d80bfe..0e096539 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "debug": "~0.7.4", "express": "~3.4.8", - "strong-remoting": "~1.2.6", + "strong-remoting": "~1.3.1", "inflection": "~1.3.5", "passport": "~0.2.0", "passport-local": "~0.1.6", From b4da270a49028f0faa753be0aa383c56e3c12b1b Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 8 Apr 2014 15:26:02 -0700 Subject: [PATCH 8/9] Add nodemailer to browser ignores --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e096539..fab2d994 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,8 @@ "express": "./lib/browser-express.js", "connect": false, "passport": false, - "passport-local": false + "passport-local": false, + "nodemailer": false }, "license": { "name": "Dual MIT/StrongLoop", From 7c0a470d643bf8dc5a0c5a041647d28cfcf6e593 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Mon, 14 Apr 2014 12:25:41 -0700 Subject: [PATCH 9/9] Add basic Remote connector e2e test --- Gruntfile.js | 91 ++++++++++++++++++++++++++++++-- lib/application.js | 7 ++- package.json | 4 +- test/e2e/remote-connector.e2e.js | 28 ++++++++++ test/fixtures/e2e/app.js | 14 +++++ test/fixtures/e2e/models.js | 4 ++ 6 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 test/e2e/remote-connector.e2e.js create mode 100644 test/fixtures/e2e/app.js create mode 100644 test/fixtures/e2e/models.js diff --git a/Gruntfile.js b/Gruntfile.js index 885f909c..cfd7b873 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -103,11 +103,7 @@ module.exports = function(grunt) { // - PhantomJS // - IE (only Windows) browsers: [ - 'Chrome', - 'Firefox', - 'Opera', - 'Safari', - 'PhantomJS' + 'Chrome' ], // If browser does not capture in given timeout [ms], kill it @@ -136,6 +132,83 @@ module.exports = function(grunt) { // Add browserify to preprocessors preprocessors: {'test/*': ['browserify']} } + }, + e2e: { + options: { + // base path, that will be used to resolve files and exclude + basePath: '', + + // frameworks to use + frameworks: ['mocha', 'browserify'], + + // list of files / patterns to load in the browser + files: [ + 'test/e2e/remote-connector.e2e.js' + ], + + // list of files to exclude + exclude: [ + + ], + + // test results reporter to use + // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' + reporters: ['dots'], + + // web server port + port: 9876, + + // cli runner port + runnerPort: 9100, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: 'warn', + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera + // - Safari (only Mac) + // - PhantomJS + // - IE (only Windows) + browsers: [ + 'Chrome' + ], + + // If browser does not capture in given timeout [ms], kill it + captureTimeout: 60000, + + // Continuous Integration mode + // if true, it capture browsers, run tests and exit + singleRun: false, + + // Browserify config (all optional) + browserify: { + // extensions: ['.coffee'], + ignore: [ + 'nodemailer', + 'passport', + 'passport-local', + 'superagent', + 'supertest' + ], + // transform: ['coffeeify'], + // debug: true, + // noParse: ['jquery'], + watch: true, + }, + + // Add browserify to preprocessors + preprocessors: {'test/e2e/*': ['browserify']} + } } } @@ -148,6 +221,14 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-karma'); + grunt.registerTask('e2e-server', function() { + var done = this.async(); + var app = require('./test/fixtures/e2e/app'); + app.listen(3000, done); + }); + + grunt.registerTask('e2e', ['e2e-server', 'karma:e2e']); + // Default task. grunt.registerTask('default', ['browserify']); diff --git a/lib/application.js b/lib/application.js index 54d509ef..6e71ce7c 100644 --- a/lib/application.js +++ b/lib/application.js @@ -56,7 +56,12 @@ app.remotes = function () { if(this._remotes) { return this._remotes; } else { - var options = this.get('remoting') || {}; + var options = {}; + + if(this.get) { + options = this.get('remoting'); + } + return (this._remotes = RemoteObjects.create(options)); } } diff --git a/package.json b/package.json index d956d662..6fd78af6 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "supertest": "~0.9.0", "chai": "~1.9.0", "loopback-testing": "~0.1.2", - "browserify": "~3.30.2", + "browserify": "~3.41.0", "grunt": "~0.4.2", "grunt-browserify": "~1.3.1", "grunt-contrib-uglify": "~0.3.2", @@ -50,7 +50,7 @@ "karma-html2js-preprocessor": "~0.1.0", "karma-phantomjs-launcher": "~0.1.2", "karma": "~0.10.9", - "karma-browserify": "0.1.0", + "karma-browserify": "~0.2.0", "karma-mocha": "~0.1.1", "grunt-karma": "~0.6.2" }, diff --git a/test/e2e/remote-connector.e2e.js b/test/e2e/remote-connector.e2e.js new file mode 100644 index 00000000..c1258056 --- /dev/null +++ b/test/e2e/remote-connector.e2e.js @@ -0,0 +1,28 @@ +var path = require('path'); +var loopback = require('../../'); +var models = require('../fixtures/e2e/models'); +var TestModel = models.TestModel; +var assert = require('assert'); + +describe('RemoteConnector', function() { + before(function() { + // setup the remote connector + var localApp = loopback(); + var ds = loopback.createDataSource({ + url: 'http://localhost:3000/api', + connector: loopback.Remote + }); + localApp.model(TestModel); + TestModel.attachTo(ds); + }); + + it('should be able to call create', function (done) { + TestModel.create({ + foo: 'bar' + }, function(err, inst) { + if(err) return done(err); + assert(inst.id); + done(); + }); + }); +}); diff --git a/test/fixtures/e2e/app.js b/test/fixtures/e2e/app.js new file mode 100644 index 00000000..337d6145 --- /dev/null +++ b/test/fixtures/e2e/app.js @@ -0,0 +1,14 @@ +var loopback = require('../../../'); +var path = require('path'); +var app = module.exports = loopback(); +var models = require('./models'); +var TestModel = models.TestModel; + +app.use(loopback.cookieParser({secret: app.get('cookieSecret')})); +var apiPath = '/api'; +app.use(apiPath, loopback.rest()); +app.use(loopback.static(path.join(__dirname, 'public'))); +app.use(loopback.urlNotFound()); +app.use(loopback.errorHandler()); +app.model(TestModel); +TestModel.attachTo(loopback.memory()); diff --git a/test/fixtures/e2e/models.js b/test/fixtures/e2e/models.js new file mode 100644 index 00000000..dad14f61 --- /dev/null +++ b/test/fixtures/e2e/models.js @@ -0,0 +1,4 @@ +var loopback = require('../../../'); +var DataModel = loopback.DataModel; + +exports.TestModel = DataModel.extend('TestModel');