├── example ├── rest-models │ ├── public │ │ ├── client.js │ │ ├── destroy.png │ │ ├── index.html │ │ ├── todos.css │ │ ├── todos.js │ │ └── json2.js │ ├── app.js │ └── remotes.js ├── socket-io │ ├── test.txt │ ├── client.js │ └── server.js ├── documentation │ ├── remotes │ │ ├── index.js │ │ ├── contract.js │ │ ├── simple.js │ │ ├── contract-class.js │ │ └── simple-class.js │ └── index.js ├── simple-types.js ├── simple.js ├── root.js ├── remote-fs.js ├── streams.js ├── shared-class.js └── before-after.js ├── test ├── data │ └── foo.json ├── helpers │ ├── test-server.js │ └── shared-objects-factory.js ├── e2e │ ├── fixtures │ │ ├── remotes.js │ │ └── user.js │ ├── e2e-server.js │ └── smoke.test.js ├── streams.js ├── shared-method.test.js ├── type.test.js ├── http-invocation.test.js ├── jsonrpc.test.js ├── shared-class.test.js ├── rest.browser.test.js └── rest-adapter.test.js ├── .jshintignore ├── .gitignore ├── index.js ├── docs.json ├── LICENSE.md ├── .jshintrc ├── README.md ├── ext └── meta.js ├── package.json ├── lib ├── dynamic.js ├── socket-io-context.js ├── exports-helper.js ├── socket-io-adapter.js ├── jsonrpc-adapter.js ├── http-invocation.js ├── shared-class.js ├── http-context.js ├── shared-method.js ├── remote-objects.js └── rest-adapter.js ├── Gruntfile.js └── CONTRIBUTING.md /example/rest-models/public/client.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/data/foo.json: -------------------------------------------------------------------------------- 1 | {"bar":"baz"} 2 | -------------------------------------------------------------------------------- /example/socket-io/test.txt: -------------------------------------------------------------------------------- 1 | hello, world! -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | example/rest-models/public 2 | node_modules 3 | -------------------------------------------------------------------------------- /test/helpers/test-server.js: -------------------------------------------------------------------------------- 1 | module.exports = function(callback) { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /example/rest-models/public/destroy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drFabio/strong-remoting/master/example/rest-models/public/destroy.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.swp 10 | *.swo 11 | node_modules/ 12 | /coverage/ 13 | dist 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * remotes ~ public api 3 | */ 4 | 5 | module.exports = require('./lib/remote-objects'); 6 | module.exports.SharedClass = require('./lib/shared-class'); 7 | -------------------------------------------------------------------------------- /test/e2e/fixtures/remotes.js: -------------------------------------------------------------------------------- 1 | var RemoteObjects = require('../../../'); 2 | var remotes = module.exports = RemoteObjects.create(); 3 | 4 | remotes.exports.User = require('./user'); 5 | -------------------------------------------------------------------------------- /docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": [ 3 | {"title": "Remote Objects API", "depth": 2}, 4 | "lib/remote-objects.js", 5 | "lib/shared-class.js", 6 | "lib/shared-method.js", 7 | "lib/http-context.js", 8 | "lib/http-invocation.js" 9 | ], 10 | "codeSectionDepth": 3 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | strong-remoting uses a dual license model. 2 | 3 | You may use this library under the terms of the [Artistic 2.0 license][], 4 | or under the terms of the [StrongLoop Subscription Agreement][]. 5 | 6 | [Artistic 2.0 license]: http://opensource.org/licenses/Artistic-2.0 7 | [StrongLoop Subscription Agreement]: http://strongloop.com/license 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "camelcase" : true, 4 | "eqnull" : true, 5 | "indent": 2, 6 | "undef": true, 7 | "quotmark": "single", 8 | "maxlen": 80, 9 | "trailing": true, 10 | "newcap": true, 11 | "nonew": true, 12 | "undef": true, 13 | "laxcomma" : true, 14 | "globals" : { 15 | "it": true, 16 | "describe": true, 17 | "beforeEach": true, 18 | "afterEach": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/rest-models/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * To run this app do this: 3 | * 4 | * $ npm install express jugglingdb 5 | * $ node app.js 6 | */ 7 | 8 | var express = require('express'); 9 | var app = express(); 10 | app.disable('x-powered-by'); 11 | var remotes = require('./remotes'); 12 | 13 | app.use(remotes.handler('rest')); 14 | app.use(express.static('public')); 15 | 16 | app.listen(3000); 17 | -------------------------------------------------------------------------------- /test/e2e/e2e-server.js: -------------------------------------------------------------------------------- 1 | // this server should be started before running tests in the e2e directory 2 | var path = require('path'); 3 | var FIXTURES = path.join(__dirname, 'fixtures'); 4 | var remotes = require(path.join(FIXTURES, 'remotes')); 5 | 6 | require('http') 7 | .createServer(remotes.handler('rest')) 8 | .listen(3000, function() { 9 | console.log('e2e server listening...'); 10 | }); 11 | -------------------------------------------------------------------------------- /example/documentation/remotes/index.js: -------------------------------------------------------------------------------- 1 | var remotes = require('../../../').create(); 2 | 3 | /** 4 | * Example API 5 | */ 6 | remotes.exports.simple = require('./simple'); 7 | remotes.exports.contract = require('./contract'); 8 | remotes.exports.SimpleClass = require('./simple-class').SimpleClass; 9 | remotes.exports.ContractClass = require('./contract-class').ContractClass; 10 | 11 | module.exports = remotes; 12 | -------------------------------------------------------------------------------- /example/socket-io/client.js: -------------------------------------------------------------------------------- 1 | var Remotes = require('../../client/js/client'); 2 | var SocketIOAdapter = require('../../client/js/socket-io-adapter'); 3 | var remotes = Remotes.connect('http://localhost:3000', SocketIOAdapter); 4 | 5 | remotes.invoke('fs.readFile', {path: 'test.txt'}, function (err, data) { 6 | console.log(data.toString()); 7 | }); 8 | 9 | remotes.invoke('ee.on', {event: 'foo'}, function (err, data) { 10 | console.log('foo event ran!', data); // logged multiple times 11 | }); -------------------------------------------------------------------------------- /example/simple-types.js: -------------------------------------------------------------------------------- 1 | // create a set of shared classes 2 | var remotes = require('../').create(); 3 | 4 | // expose a simple object 5 | var user = remotes.exports.user = { 6 | greet: function (fn) { 7 | fn(null, {msg: 'hello, world!'}); 8 | } 9 | }; 10 | 11 | // share the greet method 12 | user.greet.shared = true; 13 | 14 | // expose it over http 15 | require('http') 16 | .createServer(remotes.handler('rest')) 17 | .listen(3000); 18 | 19 | /* 20 | 21 | Test the above with curl or a rest client: 22 | 23 | $ node simple.js 24 | $ curl http://localhost:3000/user/greet 25 | { 26 | "msg": "hello, world!" 27 | } 28 | 29 | */ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # strong-remoting 2 | 3 | Objects (and, therefore, data) in Node applications commonly need to be accessible by other Node processes, browsers, and even mobile clients. Strong remoting: 4 | * Makes local functions remotable, exported over adapters 5 | * Supports multiple transports, including custom transports 6 | * Manages serialization to JSON and deserialization from JSON 7 | * Supports multiple client SDKs, including mobile clients 8 | 9 | ## Installation 10 | 11 | ```sh 12 | $ npm install strong-remoting 13 | ``` 14 | 15 | ## Documentation 16 | 17 | Please see the [official StrongLoop documentation](http://docs.strongloop.com/display/NODE/Strong+Remoting). 18 | -------------------------------------------------------------------------------- /test/e2e/smoke.test.js: -------------------------------------------------------------------------------- 1 | var RemoteObjects = require('../../'); 2 | var expect = require('chai').expect; 3 | var REMOTE_URL = 'http://localhost:3000'; 4 | var remotes = require('./fixtures/remotes'); 5 | 6 | remotes.connect(REMOTE_URL, 'rest'); 7 | 8 | describe('smoke test', function () { 9 | describe('remote.invoke()', function () { 10 | it('invokes a remote static method', function (done) { 11 | remotes.invoke( 12 | 'User.login', 13 | [{username: 'joe', password: 'secret'}], 14 | function(err, session) { 15 | expect(err).to.not.exist; 16 | expect(session.userId).to.equal(123); 17 | done(); 18 | } 19 | ); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /example/documentation/index.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var remotes = require('./remotes'); 3 | var meta = require('../../ext/meta'); 4 | var swagger = require('../../ext/swagger'); 5 | var port = process.argv[2] || 3000; 6 | var handler; 7 | var adapter; 8 | 9 | // The installation order sets which routes are captured by Swagger. 10 | swagger(remotes, { 11 | basePath: 'http://localhost:3000' 12 | }); 13 | meta(remotes); 14 | 15 | http 16 | .createServer(remotes.handler('rest')) 17 | .listen(port, function (err) { 18 | if (err) { 19 | console.error('Failed to start server with: %s', err.stack || err.message || err); 20 | process.exit(1); 21 | } 22 | 23 | console.log('Listening on port %s...', port); 24 | }); 25 | -------------------------------------------------------------------------------- /example/documentation/remotes/contract.js: -------------------------------------------------------------------------------- 1 | var helper = require('../../../').extend(module.exports); 2 | 3 | /** 4 | * Returns a secret message. 5 | */ 6 | helper.method(getSecret, { 7 | http: { verb: 'GET', path: '/customizedGetSecret' }, 8 | returns: { name: 'secret', type: 'string' } 9 | }); 10 | function getSecret(callback) { 11 | callback(null, 'shhh!'); 12 | } 13 | 14 | /** 15 | * Takes a string and returns an updated string. 16 | */ 17 | helper.method(transform, { 18 | http: { verb: 'PUT', path: '/customizedTransform' }, 19 | accepts: [{ name: 'str', type: 'string', required: true }], 20 | returns: { name: 'str', type: 'string' } 21 | }); 22 | function transform(str, callback) { 23 | callback(null, 'transformed: ' + str); 24 | } 25 | -------------------------------------------------------------------------------- /example/simple.js: -------------------------------------------------------------------------------- 1 | // create a set of shared classes 2 | var remotes = require('../').create(); 3 | 4 | // expose a simple object 5 | var user = remotes.exports.user = { 6 | greet: function (fn) { 7 | fn(null, 'hello, world!'); 8 | } 9 | }; 10 | 11 | // share the greet method 12 | user.greet.shared = true; 13 | user.greet.returns = {arg: 'msg'}; 14 | 15 | // expose it over http 16 | require('http') 17 | .createServer(remotes.handler('rest')) 18 | .listen(3000); 19 | 20 | /* 21 | 22 | Test the above with curl or a rest client: 23 | 24 | $ node simple.js 25 | $ curl http://localhost:3000/user/greet 26 | # responds as an object, with the msg attribute 27 | # set to the result of the function 28 | { 29 | "msg": "hello, world!" 30 | } 31 | 32 | */ -------------------------------------------------------------------------------- /example/root.js: -------------------------------------------------------------------------------- 1 | // create a set of shared classes 2 | var remotes = require('../').create(); 3 | 4 | // expose a simple object 5 | var products = remotes.exports.products = { 6 | find: function (fn) { 7 | fn(null, ['tv', 'vcr', 'radio']); 8 | } 9 | }; 10 | 11 | // share the find method 12 | products.find.shared = true; 13 | products.find.returns = {arg: 'products', root: true, type: 'array'}; 14 | 15 | // expose it over http 16 | require('http') 17 | .createServer(remotes.handler('rest')) 18 | .listen(3000); 19 | 20 | /* 21 | 22 | Test the above with curl or a rest client: 23 | 24 | $ node root.js 25 | $ curl http://localhost:3000/products/find 26 | # responds as an array (instead of an object) 27 | [ 28 | "tv", 29 | "vcr", 30 | "radio" 31 | ] 32 | 33 | */ -------------------------------------------------------------------------------- /example/remote-fs.js: -------------------------------------------------------------------------------- 1 | // create a set of shared classes 2 | var remotes = require('../').create(); 3 | 4 | // share some fs module code 5 | var fs = remotes.exports.fs = require('fs'); 6 | 7 | // specifically the createReadStream function 8 | fs.createReadStream.shared = true; 9 | 10 | // describe the arguments 11 | fs.createReadStream.accepts = {arg: 'path', type: 'string'}; 12 | 13 | // describe the stream destination 14 | fs.createReadStream.http = { 15 | // pipe to the response 16 | // for the http transport 17 | pipe: { 18 | dest: 'res' 19 | } 20 | }; 21 | 22 | // over rest / http 23 | require('http') 24 | .createServer(remotes.handler('rest')) 25 | .listen(3000); 26 | 27 | /* 28 | 29 | Test the above with curl or a rest client: 30 | 31 | $ node remote-fs.js 32 | $ curl http://localhost:3000/fs/createReadStream?path=simple.js 33 | 34 | */ -------------------------------------------------------------------------------- /example/documentation/remotes/simple.js: -------------------------------------------------------------------------------- 1 | // This helper adds methods to a module that we assume will be added to the remotes. 2 | // TODO(schoon) - Make this _the_ API, not a "helper". 3 | // TODO(schoon) - Document EVERYTHING 4 | var helper = require('../../../').extend(module.exports); 5 | 6 | /** 7 | * Returns a secret message. 8 | */ 9 | helper.method(getSecret, { 10 | returns: { name: 'secret', type: 'string' } 11 | }); 12 | function getSecret(callback) { 13 | callback(null, 'shhh!'); 14 | } 15 | 16 | /** 17 | * Takes a string and returns an updated string. 18 | */ 19 | helper.method(transform, { 20 | accepts: [{ name: 'str', type: 'string', required: true, description: 'The value to update' }], 21 | returns: { name: 'str', type: 'string' }, 22 | description: 'Takes a string and returns an updated string.' 23 | }); 24 | function transform(str, callback) { 25 | callback(null, 'transformed: ' + str); 26 | } 27 | -------------------------------------------------------------------------------- /example/streams.js: -------------------------------------------------------------------------------- 1 | // faux remote stream 2 | var destination = process.stdout; 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | 6 | // create a set of shared classes 7 | var remotes = require('../').create(); 8 | 9 | // our modules 10 | var fileService = remotes.exports.files = { 11 | upload: function() { 12 | return destination; 13 | }, 14 | download: function() { 15 | return fs.createReadStream(path.join(__dirname, 'streams.js')); 16 | } 17 | } 18 | 19 | fileService.upload.http = { 20 | // pipe to the request 21 | // to the result of the function 22 | pipe: { 23 | source: 'req' 24 | } 25 | }; 26 | fileService.upload.shared = true; 27 | 28 | 29 | fileService.download.http = { 30 | // pipe to the response 31 | // for the http transport 32 | pipe: { 33 | dest: 'res' 34 | } 35 | }; 36 | fileService.download.shared = true; 37 | 38 | // over rest / http 39 | require('http') 40 | .createServer(remotes.handler('rest')) 41 | .listen(3000); 42 | -------------------------------------------------------------------------------- /ext/meta.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Expose the `Meta` plugin. 3 | */ 4 | module.exports = Meta; 5 | 6 | /** 7 | * Module dependencies. 8 | */ 9 | var Remoting = require('../'); 10 | 11 | /** 12 | * Create a remotable Meta module for plugging into `RemoteObjects`. 13 | */ 14 | function Meta(remotes, options) { 15 | // Unfold options. 16 | var name = (options && options.name) || 'meta'; 17 | 18 | // We need a temporary REST adapter to discover our available routes. 19 | var adapter = remotes.handler('rest').adapter; 20 | var extension = {}; 21 | var helper = Remoting.extend(extension); 22 | 23 | helper.method(routes, { returns: { type: 'object', root: true }}); 24 | function routes(callback) { 25 | callback(null, adapter.allRoutes()); 26 | } 27 | 28 | helper.method(classes, { returns: { type: 'object', root: true }}); 29 | function classes(callback) { 30 | callback(null, remotes.classes()); 31 | } 32 | 33 | remotes.exports[name] = extension; 34 | return extension; 35 | } 36 | -------------------------------------------------------------------------------- /example/socket-io/server.js: -------------------------------------------------------------------------------- 1 | // create a set of shared classes 2 | var remotes = require('../../').create(); 3 | 4 | // share some fs module code 5 | var fs = remotes.exports.fs = require('fs'); 6 | 7 | // specifically the readFile function 8 | fs.readFile.shared = true; 9 | 10 | // describe the arguments 11 | fs.readFile.accepts = {arg: 'path', type: 'string'}; 12 | 13 | // describe the result 14 | fs.readFile.returns = {arg: 'data', type: 'buffer'}; 15 | 16 | // event emitter 17 | var EventEmitter = require('events').EventEmitter 18 | var ee = remotes.exports.ee = new EventEmitter(); 19 | 20 | // expose the on method 21 | ee.on.shared = true; 22 | ee.on.accepts = {arg: 'event', type: 'string'}; 23 | ee.on.returns = {arg: 'data', type: 'object'}; 24 | 25 | setInterval(function() { 26 | // emit some data 27 | ee.emit('foo', {some: 'data'}); 28 | }, 1000); 29 | 30 | // expose it over http 31 | var server = 32 | require('http') 33 | .createServer() 34 | .listen(3000); 35 | 36 | remotes.handler('socket-io', server); -------------------------------------------------------------------------------- /example/shared-class.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | 3 | // define a vanilla JavaScript class 4 | function Dog(name) { 5 | this.name = name; 6 | } 7 | 8 | // add a shared constructor 9 | Dog.sharedCtor = function (name, fn) { 10 | fn(null, new Dog(name)); 11 | } 12 | 13 | // define the args for the shared constructor 14 | Dog.sharedCtor.accepts = {arg: 'name', type: 'string', http: {source: 'path'}}; 15 | 16 | // change the default routing 17 | Dog.sharedCtor.http = {path: '/:name'}; 18 | 19 | // define a regular instance method 20 | Dog.prototype.speak = function (fn) { 21 | fn(null, 'roof! my name is ' + this.name); 22 | } 23 | 24 | // mark it as shared 25 | Dog.prototype.speak.returns = {arg: 'result', type: 'string', root: true}; 26 | Dog.prototype.speak.shared = true; 27 | 28 | // create a set of shared classes 29 | var remotes = require('../').create(); 30 | 31 | // expose the Dog class 32 | remotes.exports.dog = Dog; 33 | 34 | var app = express(); 35 | app.use(remotes.handler('rest')); 36 | 37 | app.listen(3000); 38 | 39 | /* 40 | 41 | Test the above with curl or a rest client: 42 | 43 | $ node shared-class.js 44 | $ curl http://localhost:3000/dog/fido/speak 45 | roof! my name is fido 46 | 47 | */ 48 | -------------------------------------------------------------------------------- /test/e2e/fixtures/user.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('test user') 2 | 3 | module.exports = User; 4 | 5 | function User() { 6 | debug('constructed a user: %j', this); 7 | } 8 | 9 | User.sharedCtor = function(id, callback) { 10 | var user = new User(); 11 | user.username = 'joe'; 12 | callback(null, user); 13 | } 14 | User.sharedCtor.shared = true; 15 | User.sharedCtor.accepts = {arg: 'id', type: 'string'}; 16 | User.sharedCtor.http = [ 17 | {path: '/:id', verb: 'get'}, 18 | {path: '/', verb: 'get'} 19 | ]; 20 | 21 | var login = User.login = function(credentials, callback) { 22 | debug('login with credentials: %j', credentials); 23 | setTimeout(function() { 24 | if(!credentials.password) { 25 | return callback(new Error('password required')); 26 | } 27 | callback(null, {userId: 123}); 28 | }, 0); 29 | } 30 | login.shared = true; 31 | login.accepts = {arg: 'credentials', type: 'object'}; 32 | login.returns = {arg: 'session', type: 'object'}; 33 | 34 | var hasUsername = User.prototype.hasUsername = function(username, callback) { 35 | callback(null, username === this.username); 36 | } 37 | 38 | hasUsername.shared = true; 39 | hasUsername.accepts = {arg: 'username', type: 'string'}; 40 | hasUsername.returns = {arg: 'hasUsername', type: 'boolean'}; 41 | -------------------------------------------------------------------------------- /example/documentation/remotes/contract-class.js: -------------------------------------------------------------------------------- 1 | // This example shows using the helper for a type in a "definitive" fashion. 2 | var helper = require('../../../').extend(module.exports); 3 | var clshelper; 4 | 5 | /** 6 | * A simple class that contains a name, this time with a custom HTTP contract. 7 | */ 8 | clshelper = helper.type(ContractClass, { 9 | accepts: [{ name: 'name', type: 'string', required: true }], 10 | http: { path: '/:name' } 11 | }); 12 | function ContractClass(name) { 13 | this.name = name; 14 | } 15 | 16 | /** 17 | * Returns the ContractClass instance's name. 18 | */ 19 | clshelper.method(getName, { 20 | returns: { name: 'name', type: 'string' } 21 | }); 22 | function getName(callback) { 23 | callback(null, this.name); 24 | } 25 | 26 | /** 27 | * Takes in a name, returning a greeting for that name. 28 | */ 29 | clshelper.method(greet, { 30 | accepts: [{ name: 'other', type: 'string', required: true }], 31 | returns: { name: 'greeting', type: 'string' } 32 | }); 33 | function greet(other, callback) { 34 | callback(null, 'Hi, ' + other + '!'); 35 | } 36 | 37 | /** 38 | * Returns the ContractClass prototype's favorite person's name. 39 | */ 40 | helper.method(getFavoritePerson, { 41 | path: 'ContractClass.getFavoritePerson', 42 | returns: { name: 'name', type: 'string' } 43 | }); 44 | function getFavoritePerson(callback) { 45 | callback(null, 'You'); 46 | } 47 | -------------------------------------------------------------------------------- /test/streams.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var RemoteObjects = require('../'); 3 | var express = require('express'); 4 | var request = require('supertest'); 5 | var fs = require('fs'); 6 | 7 | describe('strong-remoting', function () { 8 | var app, remotes, objects; 9 | 10 | beforeEach(function(){ 11 | objects = RemoteObjects.create(); 12 | remotes = objects.exports; 13 | app = express(); 14 | app.disable('x-powered-by'); 15 | 16 | app.use(function (req, res, next) { 17 | objects.handler('rest').apply(objects, arguments); 18 | }); 19 | }); 20 | 21 | function json(method, url) { 22 | return request(app)[method](url) 23 | .set('Accept', 'application/json') 24 | .set('Content-Type', 'application/json') 25 | .expect(200) 26 | .expect('Content-Type', /json/); 27 | } 28 | 29 | it('should stream the file output', function (done) { 30 | remotes.fs = fs; 31 | fs.createReadStream.shared = true; 32 | fs.createReadStream.accepts = [{arg: 'path', type: 'string'}]; 33 | fs.createReadStream.returns = {arg: 'res', type: 'stream'}; 34 | fs.createReadStream.http = { 35 | verb: 'get', 36 | // path: '/fs/createReadStream', 37 | pipe: { 38 | dest: 'res' 39 | } 40 | }; 41 | 42 | json('get', '/fs/createReadStream?path=' + __dirname + '/data/foo.json') 43 | .expect({bar: 'baz'}, done); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /example/documentation/remotes/simple-class.js: -------------------------------------------------------------------------------- 1 | // This example shows using the helper for a type in a "post-definition" style. 2 | var helper = require('../../../').extend(module.exports); 3 | 4 | /** 5 | * A simple class that contains a name. 6 | */ 7 | function SimpleClass(name) { 8 | this.name = name; 9 | } 10 | helper.type(SimpleClass, { 11 | description: 'A simple class example', 12 | accepts: [{ name: 'name', type: 'string', required: true }] 13 | }); 14 | 15 | /** 16 | * Returns the SimpleClass instance's name. 17 | */ 18 | SimpleClass.prototype.getName = function(callback) { 19 | callback(null, this.name); 20 | }; 21 | helper.method(SimpleClass.prototype.getName, { 22 | path: 'SimpleClass.prototype.getName', 23 | description: 'Returns the SimpleClass instance\'s name.', 24 | returns: { name: 'name', type: 'string' } 25 | }); 26 | 27 | /** 28 | * Takes in a name, returning a greeting for that name. 29 | */ 30 | SimpleClass.prototype.greet = function(other, callback) { 31 | callback(null, 'Hi, ' + other + '!'); 32 | }; 33 | helper.method(SimpleClass.prototype.greet, { 34 | path: 'SimpleClass.prototype.greet', 35 | description: 'Takes in a name, returning a greeting for that name.', 36 | accepts: [{ name: 'other', type: 'string', required: true }], 37 | returns: { name: 'greeting', type: 'string' } 38 | }); 39 | 40 | /** 41 | * Returns the SimpleClass prototype's favorite person's name. 42 | */ 43 | SimpleClass.getFavoritePerson = function(callback) { 44 | callback(null, 'You'); 45 | }; 46 | helper.method(SimpleClass.getFavoritePerson, { 47 | path: 'SimpleClass.getFavoritePerson', 48 | description: 'Returns the SimpleClass prototype\'s favorite person\'s name.', 49 | returns: { name: 'name', type: 'string' } 50 | }); 51 | -------------------------------------------------------------------------------- /test/helpers/shared-objects-factory.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2013 StrongLoop, Inc. 2 | * 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included 11 | * in all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | * THE SOFTWARE. 20 | */ 21 | 22 | var extend = require('util')._extend; 23 | 24 | exports.createSharedClass = function createSharedClass(config) { 25 | // create a class that can be remoted 26 | var SharedClass = function(id) { 27 | this.id = id; 28 | }; 29 | extend(SharedClass, config); 30 | 31 | SharedClass.shared = true; 32 | 33 | SharedClass.sharedCtor = function(id, cb) { 34 | cb(null, new SharedClass(id)); 35 | }; 36 | 37 | extend(SharedClass.sharedCtor, { 38 | shared: true, 39 | accepts: [ { arg: 'id', type: 'any', http: { source: 'path' }}], 40 | http: { path: '/:id' }, 41 | returns: { root: true } 42 | }); 43 | 44 | return SharedClass; 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strong-remoting", 3 | "description": "StrongLoop Remoting Module", 4 | "keywords": [ 5 | "StrongLoop", 6 | "LoopBack", 7 | "Remoting", 8 | "REST" 9 | ], 10 | "version": "2.9.0", 11 | "scripts": { 12 | "test": "mocha" 13 | }, 14 | "dependencies": { 15 | "express": "4.x", 16 | "body-parser": "^1.7.0", 17 | "debug": "^2.0.0", 18 | "eventemitter2": "^0.4.14", 19 | "cors": "^2.4.1", 20 | "jayson": "^1.1.1", 21 | "js2xmlparser": "^0.1.3", 22 | "async": "^0.9.0", 23 | "traverse": "^0.6.6", 24 | "request": "^2.42.0", 25 | "browser-request": "^0.3.2", 26 | "qs": "^2.2.3", 27 | "inflection": "^1.4.2", 28 | "xml2js": "^0.4.4" 29 | }, 30 | "devDependencies": { 31 | "supertest": "~0.13.0", 32 | "mocha": "~1.21.4", 33 | "socket.io": "~1.1.0", 34 | "chai": "~1.9.1", 35 | "grunt": "~0.4.5", 36 | "grunt-contrib-uglify": "~0.5.1", 37 | "grunt-contrib-jshint": "~0.10.0", 38 | "browserify": "~5.11.1", 39 | "grunt-browserify": "~3.0.1", 40 | "grunt-contrib-watch": "~0.6.1", 41 | "karma-script-launcher": "~0.1.0", 42 | "karma-chrome-launcher": "~0.1.4", 43 | "karma-firefox-launcher": "~0.1.3", 44 | "karma-html2js-preprocessor": "~0.1.0", 45 | "karma-jasmine": "~0.1.5", 46 | "karma-coffee-preprocessor": "~0.2.1", 47 | "requirejs": "~2.1.14", 48 | "karma-requirejs": "~0.2.2", 49 | "karma-phantomjs-launcher": "~0.1.4", 50 | "karma": "~0.12.23", 51 | "grunt-karma": "~0.9.0", 52 | "karma-browserify": "0.2.1", 53 | "karma-mocha": "~0.1.9" 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "https://github.com/strongloop/strong-remoting" 58 | }, 59 | "browser": { 60 | "express": false, 61 | "body-parser": false, 62 | "request": "browser-request" 63 | }, 64 | "license": { 65 | "name": "Dual MIT/StrongLoop", 66 | "url": "https://github.com/strongloop/strong-remoting/blob/master/LICENSE" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/rest-models/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Backbone.js Todos 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 |

Todos

16 | 17 |
18 | 19 |
20 | 21 | 22 | 23 |
24 | 25 | 29 | 30 |
31 | 32 |
33 | Double-click to edit a todo. 34 |
35 | 36 |
37 | Created by 38 |
39 | Jérôme Gravel-Niquet. 40 |
Rewritten by: TodoMVC. 41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 59 | 60 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /test/shared-method.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var extend = require('util')._extend; 3 | var expect = require('chai').expect; 4 | var SharedMethod = require('../lib/shared-method'); 5 | var factory = require('./helpers/shared-objects-factory.js'); 6 | 7 | describe('SharedMethod', function() { 8 | describe('sharedMethod.isDelegateFor(suspect, [isStatic])', function () { 9 | 10 | // stub function 11 | function myFunction() {}; 12 | 13 | it('checks if the given function is going to be invoked', function () { 14 | var mockSharedClass = {}; 15 | var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass); 16 | assert.equal(sharedMethod.isDelegateFor(myFunction), true); 17 | }); 18 | 19 | it('checks by name if a function is going to be invoked', function () { 20 | var mockSharedClass = { prototype: { myName: myFunction } }; 21 | var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass); 22 | assert.equal(sharedMethod.isDelegateFor('myName', false), true); 23 | assert.equal(sharedMethod.isDelegateFor('myName', true), false); 24 | assert.equal(sharedMethod.isDelegateFor('myName'), true); 25 | }); 26 | 27 | it('checks by name if static function is going to be invoked', function () { 28 | var mockSharedClass = { myName: myFunction }; 29 | var options = { isStatic: true }; 30 | var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass, options); 31 | assert.equal(sharedMethod.isDelegateFor('myName', true), true); 32 | assert.equal(sharedMethod.isDelegateFor('myName', false), false); 33 | }); 34 | 35 | it('checks by alias if static function is going to be invoked', function () { 36 | var mockSharedClass = { myName: myFunction }; 37 | var options = { isStatic: true, aliases: ['myAlias'] }; 38 | var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass, options); 39 | assert.equal(sharedMethod.isDelegateFor('myAlias', true), true); 40 | assert.equal(sharedMethod.isDelegateFor('myAlias', false), false); 41 | }); 42 | 43 | it('checks if the given name is a string', function () { 44 | var mockSharedClass = {}; 45 | var err; 46 | try { 47 | var sharedMethod = new SharedMethod(myFunction, Number, mockSharedClass); 48 | } catch(e) { 49 | err = e; 50 | } 51 | assert(err); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /example/before-after.js: -------------------------------------------------------------------------------- 1 | // create a set of shared classes 2 | var remotes = require('../').create(); 3 | 4 | // expose a simple object 5 | var user = remotes.exports.user = { 6 | greet: function (fn) { 7 | fn(null, 'hello, world!'); 8 | } 9 | }; 10 | 11 | // share the greet method 12 | user.greet.shared = true; 13 | 14 | // expose a simple class 15 | function Dog(name) { 16 | this.name = name; 17 | } 18 | 19 | // define a vanilla JavaScript class 20 | function Dog(name) { 21 | this.name = name; 22 | } 23 | 24 | // add a shared constructor 25 | Dog.sharedCtor = function (name, fn) { 26 | fn(null, new Dog(name)); 27 | } 28 | 29 | // define the args for the shared constructor 30 | Dog.sharedCtor.accepts = {arg: 'name', type: 'string'}; 31 | 32 | // change the default routing 33 | Dog.sharedCtor.http = {path: '/:name'}; 34 | 35 | // define a regular instance method 36 | Dog.prototype.speak = function (fn) { 37 | fn(null, 'roof! my name is ' + this.name); 38 | } 39 | 40 | Dog.prototype.speak.shared = true; 41 | 42 | // expose the dog class 43 | remotes.exports.dog = Dog; 44 | 45 | // do something before greet 46 | remotes.before('user.greet', function (ctx, next) { 47 | if((ctx.req.param('password') || '').toString() !== '1234') { 48 | next(new Error('bad password!')); 49 | } else { 50 | next(); 51 | } 52 | }); 53 | 54 | // do something before any user method 55 | remotes.before('user.*', function (ctx, next) { 56 | console.log('calling a user method'); 57 | next(); 58 | }); 59 | 60 | // do something before a dog instance method 61 | remotes.before('dog.prototype.*', function (ctx, next) { 62 | var dog = this; 63 | console.log('calling a method on', dog.name); 64 | next(); 65 | }); 66 | 67 | // do something after the dog speak method 68 | // note: you cannot cancel a method after 69 | // it has been called 70 | remotes.after('dog.prototype.speak', function (ctx, next) { 71 | console.log('after speak!'); 72 | next(); 73 | }); 74 | 75 | // do something before all methods 76 | remotes.before('**', function (ctx, next, method) { 77 | console.log('calling', method.name); 78 | next(); 79 | }); 80 | 81 | // modify all results 82 | remotes.after('**', function (ctx, next) { 83 | ctx.result += '!!!'; 84 | next(); 85 | }); 86 | 87 | // expose it over http 88 | require('http') 89 | .createServer(remotes.handler('rest')) 90 | .listen(3000); 91 | 92 | /* 93 | 94 | Test the above with curl or a rest client: 95 | 96 | $ node before-after.js 97 | $ curl http://localhost:3000/user/greet 98 | $ curl http://localhost:3000/user/greet?password=1234 99 | $ curl http://localhost:3000/dog/fido/speak 100 | 101 | */ -------------------------------------------------------------------------------- /test/type.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Dynamic = require('../lib/dynamic'); 3 | var RemoteObjects = require('../'); 4 | 5 | describe('types', function () { 6 | var remotes; 7 | beforeEach(function() { 8 | remotes = RemoteObjects.create(); 9 | }); 10 | describe('remotes.defineType(name, fn)', function () { 11 | it('should define a new type converter', function () { 12 | var name = 'MyType'; 13 | remotes.defineType(name, function(val, ctx) { 14 | return val; 15 | }); 16 | assert(Dynamic.getConverter(name)); 17 | }); 18 | }); 19 | 20 | describe('Dynamic(val, [ctx])', function () { 21 | describe('Dynamic.to(typeName)', function () { 22 | it('should convert the dynamic to the given type', function () { 23 | Dynamic.define('beep', function(str) { 24 | return 'boop'; 25 | }); 26 | var dyn = new Dynamic('beep'); 27 | assert.equal(dyn.to('beep'), 'boop'); 28 | }); 29 | }); 30 | describe('Dynamic.canConvert(typeName)', function () { 31 | it('should only return true when a converter exists', function () { 32 | Dynamic.define('MyType', function() {}); 33 | assert(Dynamic.canConvert('MyType')); 34 | assert(!Dynamic.canConvert('FauxType')); 35 | }); 36 | }); 37 | describe('Built in converters', function(){ 38 | it('should convert Boolean values', function() { 39 | shouldConvert(true, true); 40 | shouldConvert(false, false); 41 | shouldConvert(256, true); 42 | shouldConvert(-1, true); 43 | shouldConvert(1, true); 44 | shouldConvert(0, false); 45 | shouldConvert('true', true); 46 | shouldConvert('false', false); 47 | shouldConvert('0', false); 48 | shouldConvert('1', true); 49 | shouldConvert('-1', true); 50 | shouldConvert('256', true); 51 | shouldConvert('null', false); 52 | shouldConvert('undefined', false); 53 | shouldConvert('', false); 54 | 55 | function shouldConvert(val, expected) { 56 | var dyn = new Dynamic(val); 57 | assert.equal(dyn.to('boolean'), expected); 58 | } 59 | }); 60 | it('should convert Number values', function() { 61 | shouldConvert('-1', -1); 62 | shouldConvert('0', 0); 63 | shouldConvert('1', 1); 64 | shouldConvert('0.1', 0.1); 65 | shouldConvert(1, 1); 66 | shouldConvert(true, 1); 67 | shouldConvert(false, 0); 68 | shouldConvert({}, 'NaN'); 69 | shouldConvert([], 0); 70 | 71 | function shouldConvert(val, expected) { 72 | var dyn = new Dynamic(val); 73 | 74 | if(expected === 'NaN') { 75 | return assert(Number.isNaN(dyn.to('number'))); 76 | } 77 | 78 | assert.equal(dyn.to('number'), expected); 79 | } 80 | }); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/http-invocation.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var HttpInvocation = require('../lib/http-invocation'); 3 | var SharedMethod = require('../lib/shared-method'); 4 | var extend = require('util')._extend; 5 | var expect = require('chai').expect; 6 | 7 | describe('HttpInvocation', function() { 8 | describe('namedArgs', function() { 9 | 10 | function expectNamedArgs(accepts, inputArgs, expectedNamedArgs) { 11 | var method = givenSharedStaticMethod({ 12 | accepts: accepts 13 | }); 14 | var inv = new HttpInvocation(method, null, inputArgs); 15 | expect(inv.namedArgs).to.deep.equal(expectedNamedArgs); 16 | } 17 | 18 | it('should correctly name a single arg', function() { 19 | expectNamedArgs( 20 | [{arg: 'a', type: 'number'}], 21 | [1], 22 | {a: 1} 23 | ); 24 | }); 25 | 26 | it('should correctly name multiple args', function() { 27 | expectNamedArgs( 28 | [{arg: 'a', type: 'number'}, {arg: 'str', type: 'string'}], 29 | [1, 'foo'], 30 | {a: 1, str: 'foo'} 31 | ); 32 | }); 33 | 34 | it('should correctly name multiple args when a partial set is provided', function() { 35 | expectNamedArgs( 36 | [{arg: 'a', type: 'number'}, {arg: 'str', type: 'string'}], 37 | [1], 38 | {a: 1} 39 | ); 40 | }); 41 | 42 | describe('HttpContext.isAcceptable()', function() { 43 | it('should accept an acceptable argument', function() { 44 | var acceptable = HttpInvocation.isAcceptable(2, { 45 | arg: 'foo', 46 | type: 'number' 47 | }); 48 | expect(acceptable).to.equal(true); 49 | }); 50 | 51 | it('should always accept args when type is any', function() { 52 | var acceptable = HttpInvocation.isAcceptable(2, { 53 | arg: 'bar', 54 | type: 'any' 55 | }); 56 | expect(acceptable).to.equal(true); 57 | }); 58 | 59 | it('should always accept args when type is complex', function() { 60 | var acceptable = HttpInvocation.isAcceptable({}, { 61 | arg: 'bar', 62 | type: 'MyComplexType' 63 | }); 64 | expect(acceptable).to.equal(true); 65 | }); 66 | 67 | it('should accept null arg when type is complex', function() { 68 | var acceptable = HttpInvocation.isAcceptable(null, { 69 | arg: 'bar', 70 | type: 'MyComplexType' 71 | }); 72 | expect(acceptable).to.equal(true); 73 | }); 74 | }); 75 | }); 76 | }); 77 | 78 | function givenSharedStaticMethod(fn, config) { 79 | if (typeof fn === 'object' && config === undefined) { 80 | config = fn; 81 | fn = null; 82 | } 83 | fn = fn || function(cb) { cb(); }; 84 | 85 | var testClass = { testMethod: fn }; 86 | config = extend({ shared: true }, config); 87 | extend(testClass.testMethod, config); 88 | return SharedMethod.fromFunction(fn, 'testStaticMethodName', null, true); 89 | } 90 | -------------------------------------------------------------------------------- /lib/dynamic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Expose `Dynamic`. 3 | */ 4 | 5 | module.exports = Dynamic; 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var debug = require('debug')('strong-remoting:dynamic') 12 | , assert = require('assert'); 13 | 14 | /** 15 | * Create a dynamic value from the given value. 16 | * 17 | * @param {*} val The value object 18 | * @param {Context} ctx The Remote Context 19 | */ 20 | 21 | function Dynamic(val, ctx) { 22 | this.val = val; 23 | this.ctx = ctx; 24 | } 25 | 26 | /*! 27 | * Object containing converter functions. 28 | */ 29 | 30 | Dynamic.converters = []; 31 | 32 | /** 33 | * Define a named type conversion. The conversion is used when a 34 | * `SharedMethod` argument defines a type with the given `name`. 35 | * 36 | * ```js 37 | * Dynamic.define('MyType', function(val, ctx) { 38 | * // use the val and ctx objects to return the concrete value 39 | * return new MyType(val); 40 | * }); 41 | * ``` 42 | * 43 | * @param {String} name The type name 44 | * @param {Function} converter 45 | */ 46 | 47 | Dynamic.define = function(name, converter) { 48 | converter.typeName = name; 49 | this.converters.unshift(converter); 50 | } 51 | 52 | /** 53 | * Is the given type supported. 54 | * 55 | * @param {String} type 56 | * @returns {Boolean} 57 | */ 58 | 59 | Dynamic.canConvert = function(type) { 60 | return !!this.getConverter(type); 61 | } 62 | 63 | /** 64 | * Get converter by type name. 65 | * 66 | * @param {String} type 67 | * @returns {Function} 68 | */ 69 | 70 | Dynamic.getConverter = function(type) { 71 | var converters = this.converters; 72 | var converter; 73 | for(var i = 0; i < converters.length; i++) { 74 | converter = converters[i]; 75 | if(converter.typeName === type) { 76 | return converter; 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * Convert the dynamic value to the given type. 83 | * 84 | * @param {String} type 85 | * @returns {*} The concrete value 86 | */ 87 | 88 | Dynamic.prototype.to = function(type) { 89 | var converter = this.constructor.getConverter(type); 90 | assert(converter, 'No Type converter defined for ' + type); 91 | return converter(this.val, this.ctx); 92 | } 93 | 94 | /** 95 | * Built in type converters... 96 | */ 97 | 98 | Dynamic.define('boolean', function convertBoolean(val) { 99 | switch(typeof val) { 100 | case 'string': 101 | switch(val) { 102 | case 'false': 103 | case 'undefined': 104 | case 'null': 105 | case '0': 106 | case '': 107 | return false; 108 | break; 109 | default: 110 | return true; 111 | break; 112 | } 113 | break; 114 | case 'number': 115 | return val !== 0; 116 | break; 117 | default: 118 | return Boolean(val); 119 | break; 120 | } 121 | }); 122 | 123 | Dynamic.define('number', function convertNumber(val) { 124 | if(val === 0) return val; 125 | if(!val) return val; 126 | return Number(val); 127 | }); 128 | -------------------------------------------------------------------------------- /example/rest-models/remotes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | var jugglingdb = require('jugglingdb'); 6 | 7 | /** 8 | * Create a set of remote classes and export them. 9 | */ 10 | 11 | var remotes = module.exports = require('../../').create(); 12 | 13 | /** 14 | * Define some models. Remotely export them. 15 | */ 16 | 17 | var Schema = require('jugglingdb').Schema; 18 | var schema = new Schema('memory'); 19 | 20 | var Todo = remotes.exports.post = schema.define('Post', { 21 | title: { type: String, length: 255 }, 22 | done: { type: Boolean }, 23 | date: { type: Date, default: function () { return new Date;} }, 24 | changed: { type: Number, default: Date.now } 25 | }); 26 | 27 | var User = remotes.exports.user = schema.define('User', { 28 | name: String, 29 | bio: Schema.Text, 30 | approved: Boolean, 31 | joinedAt: Date, 32 | age: Number 33 | }); 34 | 35 | 36 | // // setup relationships 37 | // User.hasMany(Post, {as: 'posts', foreignKey: 'userId'}); 38 | // creates instance methods: 39 | // user.posts(conds) 40 | // user.posts.build(data) // like new Post({userId: user.id}); 41 | // user.posts.create(data) // build and save 42 | 43 | // Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}); 44 | // creates instance methods: 45 | // post.author(callback) -- getter when called with function 46 | // post.author() -- sync getter when called without params 47 | // post.author(user) -- setter when called with object 48 | 49 | // setup remote attributes 50 | setup(Todo); 51 | setup(User); 52 | 53 | // create some test data 54 | User.create({name: 'joe', age: 20}); 55 | User.create({name: 'bob', age: 30}); 56 | User.create({name: 'jim', age: 40}); 57 | User.create({name: 'jan', age: 50}); 58 | 59 | Todo.create({title: 'hello', done: false}); 60 | Todo.create({title: 'foo', done: false}); 61 | Todo.create({title: 'lorem', done: true}); 62 | 63 | // setup custom routes 64 | User.http = {path: '/u'}; 65 | Todo.http = {path: '/t'}; 66 | 67 | // annotate with remotes settings 68 | function setup(Model) { 69 | Model.sharedCtor = function (id, fn) { 70 | Model.find(id, fn); 71 | } 72 | Model.sharedCtor.shared = true; 73 | Model.sharedCtor.accepts = {arg: 'id', type: 'string'}; 74 | Model.sharedCtor.http = [ 75 | {path: '/:id', verb: 'get'}, 76 | {path: '/', verb: 'get'} 77 | ]; 78 | 79 | Model.prototype.save.shared = true; 80 | Model.prototype.save.http = [ 81 | {verb: 'post', path: '/'}, 82 | {verb: 'put', path: '/'} 83 | ]; 84 | Model.all.shared = true; 85 | Model.all.http = [ 86 | {verb: 'get', path: '/'} 87 | ]; 88 | Model.all.accepts = [ 89 | { 90 | arg: 'query', 91 | type: 'object' 92 | // http: function (ctx) { 93 | // var q = ctx.req.url.split('?')[1]; 94 | // 95 | // if(q) { 96 | // q = decodeURIComponent(q); 97 | // } 98 | // 99 | // return JSON.parse(q); 100 | // } 101 | } 102 | ]; 103 | } 104 | -------------------------------------------------------------------------------- /lib/socket-io-context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Expose `SocketIOContext`. 3 | */ 4 | 5 | module.exports = SocketIOContext; 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var EventEmitter = require('events').EventEmitter 12 | , debug = require('debug')('strong-remoting:socket-io-context') 13 | , util = require('util') 14 | , inherits = util.inherits 15 | , assert = require('assert'); 16 | 17 | /** 18 | * Create a new `SocketIOContext` with the given `options`. 19 | * 20 | * @param {Object} options 21 | * @return {SocketIOContext} 22 | */ 23 | 24 | function SocketIOContext(req, ctorArgs, args) { 25 | this.req = req; 26 | this.ctorArgs = ctorArgs; 27 | this.args = args; 28 | } 29 | 30 | /** 31 | * Inherit from `EventEmitter`. 32 | */ 33 | 34 | inherits(SocketIOContext, EventEmitter); 35 | 36 | /** 37 | * Get an arg by name using the given options. 38 | * 39 | * @param {String} name 40 | * @param {Object} options **optional** 41 | */ 42 | 43 | SocketIOContext.prototype.getArgByName = function (name, options) { 44 | return this.args[name]; 45 | } 46 | 47 | /** 48 | * Set an arg by name using the given options. 49 | * 50 | * @param {String} name 51 | * @param {Object} options **optional** 52 | */ 53 | 54 | SocketIOContext.prototype.setArgByName = function (name, options) { 55 | throw 'not implemented' 56 | } 57 | 58 | /** 59 | * Set part or all of the result by name using the given options. 60 | * 61 | * @param {String} name 62 | * @param {Object} options **optional** 63 | */ 64 | 65 | SocketIOContext.prototype.setResultByName = function (name, options) { 66 | 67 | } 68 | 69 | /** 70 | * Get part or all of the result by name using the given options. 71 | * 72 | * @param {String} name 73 | * @param {Object} options **optional** 74 | */ 75 | 76 | SocketIOContext.prototype.getResultByName = function (name, options) { 77 | 78 | } 79 | 80 | /** 81 | * Invoke the given shared method using the provided scope against the current context. 82 | */ 83 | 84 | SocketIOContext.prototype.invoke = function (scope, method, fn) { 85 | var args = method.isSharedCtor ? this.ctorArgs : this.args; 86 | var accepts = method.accepts; 87 | var returns = method.returns; 88 | var errors = method.errors; 89 | var scope; 90 | var result; 91 | 92 | // invoke the shared method 93 | method.invoke(scope, args, function (err) { 94 | if(method.name === 'on' && method.ctor instanceof EventEmitter) { 95 | arguments[1] = arguments[0]; 96 | err = null; 97 | } 98 | 99 | if(err) { 100 | return fn(err); 101 | } 102 | 103 | var resultArgs = arguments; 104 | 105 | // map the arguments using the returns description 106 | if(returns.length > 1) { 107 | // multiple 108 | result = {}; 109 | 110 | returns.forEach(function (o, i) { 111 | // map the name of the arg in the returns desc 112 | // to the same arg in the callback 113 | result[o.name || o.arg] = resultArgs[i + 1]; 114 | }); 115 | } else { 116 | // single or no result... 117 | result = resultArgs[1]; 118 | } 119 | 120 | fn(null, result); 121 | }); 122 | } 123 | -------------------------------------------------------------------------------- /lib/exports-helper.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Expose `ExportsHelper`. 3 | */ 4 | 5 | module.exports = ExportsHelper; 6 | 7 | /*! 8 | * Module dependencies. 9 | */ 10 | var debug = require('debug')('strong-remoting:exports-helper'); 11 | 12 | /*! 13 | * Constants 14 | */ 15 | var PASSTHROUGH_OPTIONS = ['http', 'description', 'notes']; 16 | 17 | /** 18 | * @class A wrapper to make manipulating the exports object easier. 19 | * 20 | * @constructor 21 | * Create a new `ExportsHelper` with the given `options`. 22 | */ 23 | 24 | function ExportsHelper(obj) { 25 | if (!(this instanceof ExportsHelper)) { 26 | return new ExportsHelper(obj); 27 | } 28 | 29 | this._obj = obj; 30 | } 31 | 32 | /** 33 | * Sets a value at any path within the exports object. 34 | */ 35 | ExportsHelper.prototype.setPath = setPath; 36 | function setPath(path, value) { 37 | var self = this; 38 | var obj = self._obj; 39 | var split = path.split('.'); 40 | var name = split.pop(); 41 | 42 | split.forEach(function (key) { 43 | if (!obj[key]) { 44 | obj[key] = {}; 45 | } 46 | 47 | obj = obj[key]; 48 | }); 49 | 50 | debug('Setting %s to %s', path, value); 51 | obj[name] = value; 52 | 53 | return self; 54 | } 55 | 56 | /** 57 | * Exports a constructor ("type") with the provided options. 58 | */ 59 | ExportsHelper.prototype.addType = type; 60 | ExportsHelper.prototype.type = type; 61 | function type(fn, options) { 62 | var self = this; 63 | var path = options.path || options.name || fn.name || null; 64 | var sharedCtor = options.sharedCtor || null; 65 | var accepts = options.accepts || null; 66 | 67 | if (!path) { 68 | // TODO: Error. 69 | return self; 70 | } 71 | 72 | if (!sharedCtor) { 73 | // TODO(schoon) - This shouldn't be thought of (or named) as a "shared 74 | // constructor". Instead, this is the lazy find/create sl-remoting uses when 75 | // a prototype method is called. `getInstance`? `findOrCreate`? `load`? 76 | sharedCtor = function () { 77 | var _args = [].slice.call(arguments); 78 | _args.pop()(null, fn.apply(null, _args)); 79 | }; 80 | } 81 | 82 | if (!sharedCtor.accepts) { 83 | sharedCtor.accepts = accepts; 84 | } 85 | 86 | // This is required because sharedCtors are called just like any other 87 | // remotable method. However, you always expect the instance and nothing else. 88 | if (!sharedCtor.returns) { 89 | sharedCtor.returns = { type: 'object', root: true }; 90 | } 91 | 92 | PASSTHROUGH_OPTIONS.forEach(function (key) { 93 | if (options[key]) { 94 | sharedCtor[key] = options[key]; 95 | } 96 | }); 97 | 98 | self.setPath(path, fn); 99 | fn.shared = true; 100 | fn.sharedCtor = sharedCtor; 101 | 102 | return new ExportsHelper(fn.prototype); 103 | } 104 | 105 | /** 106 | * Exports a Function with the provided options. 107 | */ 108 | ExportsHelper.prototype.addMethod = method; 109 | ExportsHelper.prototype.method = method; 110 | function method(fn, options) { 111 | var self = this; 112 | var path = options.path || options.name || fn.name || null; 113 | var accepts = options.accepts || null; 114 | var returns = options.returns || null; 115 | var errors = options.errors || null; 116 | 117 | if (!path) { 118 | // TODO: Error. 119 | return self; 120 | } 121 | 122 | self.setPath(path, fn); 123 | fn.shared = true; 124 | fn.accepts = accepts; 125 | fn.returns = returns; 126 | fn.errors = errors; 127 | 128 | PASSTHROUGH_OPTIONS.forEach(function (key) { 129 | if (options[key]) { 130 | fn[key] = options[key]; 131 | } 132 | }); 133 | 134 | return self; 135 | } 136 | -------------------------------------------------------------------------------- /lib/socket-io-adapter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Expose `SocketIOAdapter`. 3 | */ 4 | 5 | module.exports = SocketIOAdapter; 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var EventEmitter = require('events').EventEmitter 12 | , debug = require('debug')('strong-remoting:socket-io-adapter') 13 | , util = require('util') 14 | , inherits = util.inherits 15 | , assert = require('assert') 16 | , express = require('express') 17 | , SocketIOContext = require('./socket-io-context'); 18 | 19 | /** 20 | * Create a new `RestAdapter` with the given `options`. 21 | * 22 | * @param {Object} options 23 | * @return {RestAdapter} 24 | */ 25 | 26 | function SocketIOAdapter(remotes, server) { 27 | EventEmitter.apply(this, arguments); 28 | 29 | // throw an error if args are not supplied 30 | // assert(typeof options === 'object', 'RestAdapter requires an options object'); 31 | 32 | this.remotes = remotes; 33 | this.server = server; 34 | this.Context = SocketIOContext; 35 | } 36 | 37 | /** 38 | * Inherit from `EventEmitter`. 39 | */ 40 | 41 | inherits(SocketIOAdapter, EventEmitter); 42 | 43 | /*! 44 | * Simplified APIs 45 | */ 46 | 47 | SocketIOAdapter.create = function (remotes) { 48 | // add simplified construction / sugar here 49 | return new SocketIOAdapter(remotes); 50 | } 51 | 52 | SocketIOAdapter.prototype.createHandler = function () { 53 | var adapter = this; 54 | var remotes = this.remotes; 55 | var Context = this.Context; 56 | var classes = this.remotes.classes(); 57 | var io = require('socket.io').listen(this.server); 58 | 59 | io.sockets.on('connection', function (socket) { 60 | socket.on('invoke', function (methodString, ctorArgs, args, id) { 61 | var method = remotes.findMethod(methodString); 62 | 63 | if(method) { 64 | // create context NEED ARGS 65 | var ctx = new Context(socket.request, ctorArgs, args); 66 | 67 | adapter.invoke(ctx, method, args, function (err, result) { 68 | socket.emit('result', { 69 | data: result, 70 | id: id, 71 | methodString: methodString, 72 | __types__: method.returns 73 | }); 74 | }); 75 | } else { 76 | socket.emit('result', { 77 | err: 'method does not exist', 78 | id: id, 79 | methodString: methodString 80 | }); 81 | } 82 | }); 83 | }); 84 | } 85 | 86 | SocketIOAdapter.prototype.invoke = function (ctx, method, args, callback) { 87 | var remotes = this.remotes; 88 | 89 | if(method.isStatic) { 90 | remotes.execHooks('before', method, method.ctor, ctx, function (err) { 91 | if(err) return callback(err); 92 | 93 | // invoke the static method on the actual constructor 94 | ctx.invoke(method.ctor, method, function (err, result) { 95 | if(err) return callback(err); 96 | ctx.result = result; 97 | remotes.execHooks('after', method, method.ctor, ctx, function (err) { 98 | // send the result 99 | callback(err, ctx.result); 100 | }); 101 | }); 102 | }); 103 | } else { 104 | // invoke the shared constructor to get an instance 105 | ctx.invoke(method, method.sharedCtor, function (err, inst) { 106 | if(err) return callback(err); 107 | remotes.execHooks('before', method, inst, ctx, function (err) { 108 | if(err) { 109 | callback(err); 110 | } else { 111 | // invoke the instance method 112 | ctx.invoke(inst, method, function (err, result) { 113 | if(err) return callback(err); 114 | 115 | ctx.result = result; 116 | remotes.execHooks('after', method, inst, ctx, function (err) { 117 | // send the result 118 | callback(err, ctx.result); 119 | }); 120 | }); 121 | } 122 | }); 123 | }); 124 | } 125 | } -------------------------------------------------------------------------------- /test/jsonrpc.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var RemoteObjects = require('../'); 3 | var express = require('express'); 4 | var request = require('supertest'); 5 | var SharedClass = require('../lib/shared-class'); 6 | 7 | describe('strong-remoting-jsonrpc', function () { 8 | var app; 9 | var server; 10 | var objects; 11 | var remotes; 12 | 13 | // setup 14 | beforeEach(function () { 15 | if (server) server.close(); 16 | objects = RemoteObjects.create({json: {limit: '1kb'}}); 17 | remotes = objects.exports; 18 | app = express(); 19 | }); 20 | 21 | function jsonrpc(url, method, parameters) { 22 | return request(app)['post'](url) 23 | .set('Accept', 'application/json') 24 | .set('Content-Type', 'application/json') 25 | .send({"jsonrpc": "2.0", "method": method, "params": parameters, "id": 1}) 26 | .expect(200) 27 | .expect('Content-Type', /json/); 28 | } 29 | 30 | describe('handlers', function () { 31 | describe('jsonrpc', function () { 32 | beforeEach(function () { 33 | app.use(function (req, res, next) { 34 | // create the handler for each request 35 | objects.handler('jsonrpc').apply(objects, arguments); 36 | }); 37 | function greet(msg, fn) { 38 | fn(null, msg); 39 | } 40 | 41 | // Create a shared method directly on the function object 42 | remotes.user = { 43 | greet: greet 44 | }; 45 | greet.shared = true; 46 | 47 | // Create a shared method directly on the function object for named parameters tests 48 | function sum(numA,numB,cb){ 49 | cb(null,numA+numB); 50 | }; 51 | remotes.mathematic={ 52 | sum:sum 53 | }; 54 | sum.accepts=[ 55 | {'arg':'numA','type':'number'}, 56 | {'arg':'numB','type':'number'}, 57 | ]; 58 | sum.returns={ 59 | 'arg':'sum', 60 | 'type':'number' 61 | } 62 | sum.shared=true; 63 | 64 | 65 | // Create a shared method using SharedClass/SharedMethod 66 | function Product() { 67 | } 68 | 69 | Product.getPrice = function(cb) { 70 | process.nextTick(function() { 71 | return cb(null, 100); 72 | }); 73 | }; 74 | 75 | var productClass = new SharedClass('product', Product, {}); 76 | productClass.defineMethod('getPrice', {isStatic: true}); 77 | objects.addClass(productClass); 78 | }); 79 | 80 | it('should support calling object methods', function (done) { 81 | jsonrpc('/user/jsonrpc', 'greet', ['JS']) 82 | .expect({"jsonrpc": "2.0", "id": 1, "result": "JS"}, done); 83 | 84 | }); 85 | it('Should successfully call a method with named parameters',function(done){ 86 | jsonrpc('/mathematic/jsonrpc', 'sum', {'numB':9,'numA':2}) 87 | .expect({"jsonrpc": "2.0", "id": 1, "result": 11}, done); 88 | }); 89 | it('should support a remote method using shared method', function (done) { 90 | jsonrpc('/product/jsonrpc', 'getPrice', []) 91 | .expect({"jsonrpc": "2.0", "id": 1, "result": 100}, done); 92 | }); 93 | 94 | it('should report error for non-existent methods', function (done) { 95 | jsonrpc('/user/jsonrpc', 'greet1', ['JS']) 96 | .expect({ 97 | "jsonrpc": "2.0", 98 | "id": 1, 99 | "error": { 100 | "code": -32601, 101 | "message": "Method not found" 102 | } 103 | }, done); 104 | }); 105 | 106 | // The 1kb limit is set by RemoteObjects.create({json: {limit: '1kb'}}); 107 | it('should reject json payload larger than 1kb', function (done) { 108 | // Build an object that is larger than 1kb 109 | var name = ""; 110 | for (var i = 0; i < 2048; i++) { 111 | name += "11111111111"; 112 | } 113 | 114 | jsonrpc('/user/jsonrpc', 'greet', [name]) 115 | .expect(413, done); 116 | }); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt) { 3 | 4 | // Project configuration. 5 | grunt.initConfig({ 6 | // Metadata. 7 | pkg: grunt.file.readJSON('package.json'), 8 | banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' + 9 | '<%= grunt.template.today("yyyy-mm-dd") %>\n' + 10 | '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' + 11 | '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + 12 | ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n', 13 | // Task configuration. 14 | uglify: { 15 | options: { 16 | banner: '<%= banner %>' 17 | }, 18 | dist: { 19 | files: { 20 | 'dist/strong-remoting.min.js': ['dist/strong-remoting.js'] 21 | } 22 | } 23 | }, 24 | jshint: { 25 | options: { 26 | jshintrc: true 27 | }, 28 | gruntfile: { 29 | src: 'Gruntfile.js' 30 | }, 31 | lib_test: { 32 | src: ['lib/**/*.js', 'test/**/*.js'] 33 | } 34 | }, 35 | watch: { 36 | gruntfile: { 37 | files: '<%= jshint.gruntfile.src %>', 38 | tasks: ['jshint:gruntfile'] 39 | }, 40 | lib_test: { 41 | files: '<%= jshint.lib_test.src %>', 42 | tasks: ['jshint:lib_test'] 43 | } 44 | }, 45 | browserify: { 46 | dist: { 47 | files: { 48 | 'dist/strong-remoting.js': ['index.js'], 49 | }, 50 | options: { 51 | ignore: ['nodemailer', 'passport'], 52 | standalone: 'strong-remoting' 53 | } 54 | } 55 | }, 56 | karma: { 57 | unit: { 58 | options: { 59 | // base path, that will be used to resolve files and exclude 60 | basePath: '', 61 | 62 | // frameworks to use 63 | frameworks: ['mocha', 'browserify'], 64 | 65 | // list of files / patterns to load in the browser 66 | files: [ 67 | 'test/e2e/fixtures/*.js', 68 | 'test/e2e/smoke.test.js' 69 | ], 70 | 71 | // list of files to exclude 72 | exclude: [ 73 | 74 | ], 75 | 76 | // test results reporter to use 77 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 78 | reporters: ['dots'], 79 | 80 | // web server port 81 | port: 9876, 82 | 83 | // cli runner port 84 | runnerPort: 9100, 85 | 86 | // enable / disable colors in the output (reporters and logs) 87 | colors: true, 88 | 89 | // level of logging 90 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 91 | logLevel: 'warn', 92 | 93 | // enable / disable watching file and executing tests whenever any file changes 94 | autoWatch: true, 95 | 96 | // Start these browsers, currently available: 97 | // - Chrome 98 | // - ChromeCanary 99 | // - Firefox 100 | // - Opera 101 | // - Safari (only Mac) 102 | // - PhantomJS 103 | // - IE (only Windows) 104 | browsers: [ 105 | 'Chrome' 106 | ], 107 | 108 | // If browser does not capture in given timeout [ms], kill it 109 | captureTimeout: 60000, 110 | 111 | // Continuous Integration mode 112 | // if true, it capture browsers, run tests and exit 113 | singleRun: false, 114 | 115 | // Browserify config (all optional) 116 | browserify: { 117 | // extensions: ['.coffee'], 118 | ignore: [ 119 | 'superagent', 120 | 'supertest' 121 | ], 122 | // transform: ['coffeeify'], 123 | // debug: true, 124 | // noParse: ['jquery'], 125 | watch: true, 126 | }, 127 | 128 | // Add browserify to preprocessors 129 | preprocessors: { 130 | 'test/e2e/**': ['browserify'], 131 | 'lib/*.js': ['browserify'] 132 | } 133 | } 134 | } 135 | } 136 | 137 | }); 138 | 139 | grunt.registerTask('e2e-server', 'Run the e2e server', function() { 140 | require('test/e2e/e2e-server.js'); 141 | }); 142 | 143 | 144 | // These plugins provide necessary tasks. 145 | grunt.loadNpmTasks('grunt-browserify'); 146 | grunt.loadNpmTasks('grunt-contrib-uglify'); 147 | grunt.loadNpmTasks('grunt-contrib-jshint'); 148 | grunt.loadNpmTasks('grunt-contrib-watch'); 149 | grunt.loadNpmTasks('grunt-karma'); 150 | 151 | // Default task. 152 | grunt.registerTask('default', ['browserify']); 153 | 154 | // browser / e2e testing... 155 | grunt.registerTask('e2e', ['e2e-server', 'karma']); 156 | 157 | }; 158 | -------------------------------------------------------------------------------- /example/rest-models/public/todos.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | line-height: 1.4em; 10 | background: #eeeeee; 11 | color: #333333; 12 | width: 520px; 13 | margin: 0 auto; 14 | -webkit-font-smoothing: antialiased; 15 | } 16 | 17 | #todoapp { 18 | background: #fff; 19 | padding: 20px; 20 | margin-bottom: 40px; 21 | -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0; 22 | -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0; 23 | -ms-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0; 24 | -o-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0; 25 | box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0; 26 | -webkit-border-radius: 0 0 5px 5px; 27 | -moz-border-radius: 0 0 5px 5px; 28 | -ms-border-radius: 0 0 5px 5px; 29 | -o-border-radius: 0 0 5px 5px; 30 | border-radius: 0 0 5px 5px; 31 | } 32 | 33 | #todoapp h1 { 34 | font-size: 36px; 35 | font-weight: bold; 36 | text-align: center; 37 | padding: 0 0 10px 0; 38 | } 39 | 40 | #todoapp input[type="text"] { 41 | width: 466px; 42 | font-size: 24px; 43 | font-family: inherit; 44 | line-height: 1.4em; 45 | border: 0; 46 | outline: none; 47 | padding: 6px; 48 | border: 1px solid #999999; 49 | -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; 50 | -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; 51 | -ms-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; 52 | -o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; 53 | box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; 54 | } 55 | 56 | #todoapp input::-webkit-input-placeholder { 57 | font-style: italic; 58 | } 59 | 60 | #main { 61 | display: none; 62 | } 63 | 64 | #todo-list { 65 | margin: 10px 0; 66 | padding: 0; 67 | list-style: none; 68 | } 69 | 70 | #todo-list li { 71 | padding: 18px 20px 18px 0; 72 | position: relative; 73 | font-size: 24px; 74 | border-bottom: 1px solid #cccccc; 75 | } 76 | 77 | #todo-list li:last-child { 78 | border-bottom: none; 79 | } 80 | 81 | #todo-list li.done label { 82 | color: #777777; 83 | text-decoration: line-through; 84 | } 85 | 86 | #todo-list .destroy { 87 | position: absolute; 88 | right: 5px; 89 | top: 20px; 90 | display: none; 91 | cursor: pointer; 92 | width: 20px; 93 | height: 20px; 94 | background: url(destroy.png) no-repeat; 95 | } 96 | 97 | #todo-list li:hover .destroy { 98 | display: block; 99 | } 100 | 101 | #todo-list .destroy:hover { 102 | background-position: 0 -20px; 103 | } 104 | 105 | #todo-list li.editing { 106 | border-bottom: none; 107 | margin-top: -1px; 108 | padding: 0; 109 | } 110 | 111 | #todo-list li.editing:last-child { 112 | margin-bottom: -1px; 113 | } 114 | 115 | #todo-list li.editing .edit { 116 | display: block; 117 | width: 444px; 118 | padding: 13px 15px 14px 20px; 119 | margin: 0; 120 | } 121 | 122 | #todo-list li.editing .view { 123 | display: none; 124 | } 125 | 126 | #todo-list li .view label { 127 | word-break: break-word; 128 | } 129 | 130 | #todo-list li .edit { 131 | display: none; 132 | } 133 | 134 | #todoapp footer { 135 | display: none; 136 | margin: 0 -20px -20px -20px; 137 | overflow: hidden; 138 | color: #555555; 139 | background: #f4fce8; 140 | border-top: 1px solid #ededed; 141 | padding: 0 20px; 142 | line-height: 37px; 143 | -webkit-border-radius: 0 0 5px 5px; 144 | -moz-border-radius: 0 0 5px 5px; 145 | -ms-border-radius: 0 0 5px 5px; 146 | -o-border-radius: 0 0 5px 5px; 147 | border-radius: 0 0 5px 5px; 148 | } 149 | 150 | #clear-completed { 151 | float: right; 152 | line-height: 20px; 153 | text-decoration: none; 154 | background: rgba(0, 0, 0, 0.1); 155 | color: #555555; 156 | font-size: 11px; 157 | margin-top: 8px; 158 | margin-bottom: 8px; 159 | padding: 0 10px 1px; 160 | cursor: pointer; 161 | -webkit-border-radius: 12px; 162 | -moz-border-radius: 12px; 163 | -ms-border-radius: 12px; 164 | -o-border-radius: 12px; 165 | border-radius: 12px; 166 | -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; 167 | -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; 168 | -ms-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; 169 | -o-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; 170 | box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; 171 | } 172 | 173 | #clear-completed:hover { 174 | background: rgba(0, 0, 0, 0.15); 175 | -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0; 176 | -moz-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0; 177 | -ms-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0; 178 | -o-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0; 179 | box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0; 180 | } 181 | 182 | #clear-completed:active { 183 | position: relative; 184 | top: 1px; 185 | } 186 | 187 | #todo-count span { 188 | font-weight: bold; 189 | } 190 | 191 | #instructions { 192 | margin: 10px auto; 193 | color: #777777; 194 | text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0; 195 | text-align: center; 196 | } 197 | 198 | #instructions a { 199 | color: #336699; 200 | } 201 | 202 | #credits { 203 | margin: 30px auto; 204 | color: #999; 205 | text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0; 206 | text-align: center; 207 | } 208 | 209 | #credits a { 210 | color: #888; 211 | } 212 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing ### 2 | 3 | Thank you for your interest in `strong-remoting`, an open source project 4 | administered by StrongLoop. 5 | 6 | Contributing to `strong-remoting` is easy. In a few simple steps: 7 | 8 | * Ensure that your effort is aligned with the project's roadmap by 9 | talking to the maintainers, especially if you are going to spend a 10 | lot of time on it. 11 | 12 | * Make something better or fix a bug. 13 | 14 | * Adhere to code style outlined in the [Google C++ Style Guide][] and 15 | [Google Javascript Style Guide][]. 16 | 17 | * Sign the [Contributor License Agreement](https://cla.strongloop.com/strongloop/strong-remoting) 18 | 19 | * Submit a pull request through Github. 20 | 21 | 22 | ### Contributor License Agreement ### 23 | 24 | ``` 25 | Individual Contributor License Agreement 26 | 27 | By signing this Individual Contributor License Agreement 28 | ("Agreement"), and making a Contribution (as defined below) to 29 | StrongLoop, Inc. ("StrongLoop"), You (as defined below) accept and 30 | agree to the following terms and conditions for Your present and 31 | future Contributions submitted to StrongLoop. Except for the license 32 | granted in this Agreement to StrongLoop and recipients of software 33 | distributed by StrongLoop, You reserve all right, title, and interest 34 | in and to Your Contributions. 35 | 36 | 1. Definitions 37 | 38 | "You" or "Your" shall mean the copyright owner or the individual 39 | authorized by the copyright owner that is entering into this 40 | Agreement with StrongLoop. 41 | 42 | "Contribution" shall mean any original work of authorship, 43 | including any modifications or additions to an existing work, that 44 | is intentionally submitted by You to StrongLoop for inclusion in, 45 | or documentation of, any of the products owned or managed by 46 | StrongLoop ("Work"). For purposes of this definition, "submitted" 47 | means any form of electronic, verbal, or written communication 48 | sent to StrongLoop or its representatives, including but not 49 | limited to communication or electronic mailing lists, source code 50 | control systems, and issue tracking systems that are managed by, 51 | or on behalf of, StrongLoop for the purpose of discussing and 52 | improving the Work, but excluding communication that is 53 | conspicuously marked or otherwise designated in writing by You as 54 | "Not a Contribution." 55 | 56 | 2. You Grant a Copyright License to StrongLoop 57 | 58 | Subject to the terms and conditions of this Agreement, You hereby 59 | grant to StrongLoop and recipients of software distributed by 60 | StrongLoop, a perpetual, worldwide, non-exclusive, no-charge, 61 | royalty-free, irrevocable copyright license to reproduce, prepare 62 | derivative works of, publicly display, publicly perform, 63 | sublicense, and distribute Your Contributions and such derivative 64 | works under any license and without any restrictions. 65 | 66 | 3. You Grant a Patent License to StrongLoop 67 | 68 | Subject to the terms and conditions of this Agreement, You hereby 69 | grant to StrongLoop and to recipients of software distributed by 70 | StrongLoop a perpetual, worldwide, non-exclusive, no-charge, 71 | royalty-free, irrevocable (except as stated in this Section) 72 | patent license to make, have made, use, offer to sell, sell, 73 | import, and otherwise transfer the Work under any license and 74 | without any restrictions. The patent license You grant to 75 | StrongLoop under this Section applies only to those patent claims 76 | licensable by You that are necessarily infringed by Your 77 | Contributions(s) alone or by combination of Your Contributions(s) 78 | with the Work to which such Contribution(s) was submitted. If any 79 | entity institutes a patent litigation against You or any other 80 | entity (including a cross-claim or counterclaim in a lawsuit) 81 | alleging that Your Contribution, or the Work to which You have 82 | contributed, constitutes direct or contributory patent 83 | infringement, any patent licenses granted to that entity under 84 | this Agreement for that Contribution or Work shall terminate as 85 | of the date such litigation is filed. 86 | 87 | 4. You Have the Right to Grant Licenses to StrongLoop 88 | 89 | You represent that You are legally entitled to grant the licenses 90 | in this Agreement. 91 | 92 | If Your employer(s) has rights to intellectual property that You 93 | create, You represent that You have received permission to make 94 | the Contributions on behalf of that employer, that Your employer 95 | has waived such rights for Your Contributions, or that Your 96 | employer has executed a separate Corporate Contributor License 97 | Agreement with StrongLoop. 98 | 99 | 5. The Contributions Are Your Original Work 100 | 101 | You represent that each of Your Contributions are Your original 102 | works of authorship (see Section 8 (Submissions on Behalf of 103 | Others) for submission on behalf of others). You represent that to 104 | Your knowledge, no other person claims, or has the right to claim, 105 | any right in any intellectual property right related to Your 106 | Contributions. 107 | 108 | You also represent that You are not legally obligated, whether by 109 | entering into an agreement or otherwise, in any way that conflicts 110 | with the terms of this Agreement. 111 | 112 | You represent that Your Contribution submissions include complete 113 | details of any third-party license or other restriction (including, 114 | but not limited to, related patents and trademarks) of which You 115 | are personally aware and which are associated with any part of 116 | Your Contributions. 117 | 118 | 6. You Don't Have an Obligation to Provide Support for Your Contributions 119 | 120 | You are not expected to provide support for Your Contributions, 121 | except to the extent You desire to provide support. You may provide 122 | support for free, for a fee, or not at all. 123 | 124 | 6. No Warranties or Conditions 125 | 126 | StrongLoop acknowledges that unless required by applicable law or 127 | agreed to in writing, You provide Your Contributions on an "AS IS" 128 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 129 | EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES 130 | OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR 131 | FITNESS FOR A PARTICULAR PURPOSE. 132 | 133 | 7. Submission on Behalf of Others 134 | 135 | If You wish to submit work that is not Your original creation, You 136 | may submit it to StrongLoop separately from any Contribution, 137 | identifying the complete details of its source and of any license 138 | or other restriction (including, but not limited to, related 139 | patents, trademarks, and license agreements) of which You are 140 | personally aware, and conspicuously marking the work as 141 | "Submitted on Behalf of a Third-Party: [named here]". 142 | 143 | 8. Agree to Notify of Change of Circumstances 144 | 145 | You agree to notify StrongLoop of any facts or circumstances of 146 | which You become aware that would make these representations 147 | inaccurate in any respect. Email us at callback@strongloop.com. 148 | ``` 149 | 150 | [Google C++ Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml 151 | [Google Javascript Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml 152 | -------------------------------------------------------------------------------- /lib/jsonrpc-adapter.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Expose `JsonRpcAdapter`. 3 | */ 4 | 5 | module.exports = JsonRpcAdapter; 6 | 7 | /*! 8 | * Module dependencies. 9 | */ 10 | 11 | var EventEmitter = require('events').EventEmitter 12 | , debug = require('debug')('strong-remoting:jsonrpc-adapter') 13 | , util = require('util') 14 | , inherits = util.inherits 15 | , jayson = require('jayson') 16 | , express = require('express') 17 | , bodyParser = require('body-parser') 18 | , cors = require('cors') 19 | , HttpContext = require('./http-context'); 20 | 21 | var json = bodyParser.json; 22 | var urlencoded = bodyParser.urlencoded; 23 | 24 | /** 25 | * Create a new `JsonRpcAdapter` with the given `options`. 26 | * 27 | * @param {Object} options 28 | * @return {JsonRpcAdapter} 29 | */ 30 | 31 | function JsonRpcAdapter(remotes) { 32 | EventEmitter.call(this); 33 | 34 | this.remotes = remotes; 35 | this.Context = HttpContext; 36 | } 37 | 38 | /** 39 | * Inherit from `EventEmitter`. 40 | */ 41 | 42 | inherits(JsonRpcAdapter, EventEmitter); 43 | 44 | /*! 45 | * Simplified APIs 46 | */ 47 | 48 | JsonRpcAdapter.create = 49 | JsonRpcAdapter.createJsonRpcAdapter = function (remotes) { 50 | // add simplified construction / sugar here 51 | return new JsonRpcAdapter(remotes); 52 | }; 53 | 54 | /** 55 | * Get the path for the given method. 56 | */ 57 | 58 | JsonRpcAdapter.prototype.getRoutes = function (obj) { 59 | // build default route 60 | var routes = [ 61 | { 62 | verb: 'POST', 63 | path: obj.name ? ('/' + obj.name) : '' 64 | } 65 | ]; 66 | return routes; 67 | }; 68 | 69 | JsonRpcAdapter.errorHandler = function() { 70 | return function restErrorHandler(err, req, res, next) { 71 | if(typeof err === 'string') { 72 | err = new Error(err); 73 | err.status = err.statusCode = 500; 74 | } 75 | 76 | res.statusCode = err.statusCode || err.status || 500; 77 | 78 | debug('Error in %s %s: %s', req.method, req.url, err.stack); 79 | var data = { 80 | name: err.name, 81 | status: res.statusCode, 82 | message: err.message || 'An unknown error occurred' 83 | }; 84 | 85 | for (var prop in err) 86 | data[prop] = err[prop]; 87 | 88 | // TODO(bajtos) Remove stack info when running in production 89 | data.stack = err.stack; 90 | 91 | res.send({jsonrpc: "2.0", error: {code: -32000, message: "Server error", data: data}, 92 | "id": null}); 93 | }; 94 | }; 95 | 96 | JsonRpcAdapter.prototype.createHandler = function () { 97 | 98 | var root = express.Router(); 99 | var classes = this.remotes.classes(); 100 | 101 | // Add a handler to tolerate empty json as connect's json middleware throws an error 102 | root.use(function (req, res, next) { 103 | if (req.is('application/json')) { 104 | if (req.get('Content-Length') === '0') { // This doesn't cover the transfer-encoding: chunked 105 | req._body = true; // Mark it as parsed 106 | req.body = {}; 107 | } 108 | } 109 | next(); 110 | }); 111 | 112 | // Set strict to be `false` so that anything `JSON.parse()` accepts will be parsed 113 | debug("remoting options: %j", this.remotes.options); 114 | var jsonOptions = this.remotes.options.json || {strict: false}; 115 | var corsOptions = this.remotes.options.cors || {origin: true, credentials: true}; 116 | 117 | // Optimize the cors handler 118 | var corsHandler = function(req, res, next) { 119 | var reqUrl = req.protocol + '://' + req.get('host'); 120 | if (req.method === 'OPTIONS' || reqUrl !== req.get('origin')) { 121 | cors(corsOptions)(req, res, next); 122 | } else { 123 | next(); 124 | } 125 | }; 126 | 127 | // Set up CORS first so that it's always enabled even when parsing errors 128 | // happen in urlencoded/json 129 | root.use(corsHandler); 130 | root.use(json(jsonOptions)); 131 | 132 | root.use(JsonRpcAdapter.errorHandler()); 133 | 134 | classes.forEach(function (sc) { 135 | var server = new jayson.server(); 136 | root.post('/' + sc.name + '/jsonrpc', new jayson.server.interfaces.middleware(server, {})); 137 | 138 | var methods = sc.methods(); 139 | 140 | methods.forEach(function (method) { 141 | // Wrap the method so that it will keep its own receiver - the shared class 142 | var fn = function () { 143 | var args = arguments; 144 | if (method.isStatic) { 145 | method.getFunction().apply(method.ctor, args); 146 | } else { 147 | method.sharedCtor.invoke(method, function (err, instance) { 148 | method.getFunction().apply(instance, args); 149 | }); 150 | } 151 | }; 152 | 153 | /* 154 | I had to this because jayson uses fn.toString to map the parameters, 155 | and since you wrap the method, the jsonrpc server cannot read them. 156 | I solved this overwriting the toString method on the wrapped function, 157 | creating this fake string that has the paramenters names in it. 158 | */ 159 | if(method.accepts) 160 | { 161 | var argsNames = method.accepts.map(function(item) 162 | { 163 | return item.arg; 164 | }); 165 | 166 | fn.toString = function() 167 | { 168 | return 'function (' + argsNames.concat('callback').join(',') + '){}'; 169 | }; 170 | } 171 | 172 | server.method(method.name, fn); 173 | }); 174 | 175 | }); 176 | 177 | return root; 178 | }; 179 | 180 | 181 | JsonRpcAdapter.prototype.allRoutes = function () { 182 | var routes = []; 183 | var adapter = this; 184 | var classes = this.remotes.classes(); 185 | var currentRoot = ''; 186 | 187 | classes.forEach(function (sc) { 188 | 189 | 190 | adapter 191 | .getRoutes(sc) 192 | .forEach(function (classRoute) { 193 | currentRoot = classRoute.path; 194 | var methods = sc.methods(); 195 | 196 | var functions = []; 197 | methods.forEach(function (method) { 198 | // Use functions to keep track of JS functions to dedupe 199 | if (functions.indexOf(method.fn) === -1) { 200 | functions.push(method.fn); 201 | } else { 202 | return; // Skip duplicate methods such as X.m1 = X.m2 = function() {...} 203 | } 204 | adapter.getRoutes(method).forEach(function (route) { 205 | if (method.isStatic) { 206 | addRoute(route.verb, route.path, method); 207 | } else { 208 | adapter 209 | .getRoutes(method.sharedCtor) 210 | .forEach(function (sharedCtorRoute) { 211 | addRoute(route.verb, sharedCtorRoute.path + route.path, method); 212 | }); 213 | } 214 | }); 215 | }); 216 | }); 217 | }); 218 | 219 | return routes; 220 | 221 | 222 | function addRoute(verb, path, method) { 223 | if (path === '/' || path === '//') { 224 | path = currentRoot; 225 | } else { 226 | path = currentRoot + path; 227 | } 228 | 229 | if (path[path.length - 1] === '/') { 230 | path = path.substr(0, path.length - 1); 231 | } 232 | 233 | // TODO this could be cleaner 234 | path = path.replace(/\/\//g, '/'); 235 | 236 | routes.push({ 237 | verb: verb, 238 | path: path, 239 | description: method.description, 240 | notes: method.notes, 241 | method: method.stringName, 242 | accepts: (method.accepts && method.accepts.length) ? method.accepts : undefined, 243 | returns: (method.returns && method.returns.length) ? method.returns : undefined, 244 | errors: (method.errors && method.errors.length) ? method.errors : undefined 245 | }); 246 | } 247 | }; 248 | 249 | -------------------------------------------------------------------------------- /lib/http-invocation.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Expose `HttpInvocation`. 3 | */ 4 | 5 | module.exports = HttpInvocation; 6 | 7 | /*! 8 | * Module dependencies. 9 | */ 10 | 11 | var EventEmitter = require('events').EventEmitter 12 | , debug = require('debug')('strong-remoting:http-invocation') 13 | , util = require('util') 14 | , inherits = util.inherits 15 | , path = require('path') 16 | , assert = require('assert') 17 | , request = require('request') 18 | , Dynamic = require('./dynamic') 19 | , SUPPORTED_TYPES = ['json', 'application/javascript', 'text/javascript'] 20 | , qs = require('qs'); 21 | 22 | 23 | /*! 24 | * JSON Types 25 | */ 26 | 27 | var JSON_TYPES = ['boolean', 'string', 'object', 'number']; 28 | 29 | /** 30 | * Create a new `HttpInvocation`. 31 | * @class 32 | * @param {SharedMethod} method 33 | * @param {Array} [args] 34 | * @param {String} base The base URL 35 | * @property {String} base The base URL 36 | * @property {SharedMethod} method The `SharedMethod` which will be invoked 37 | * @property {Array} args The arguments to be used when invoking the `SharedMethod` 38 | */ 39 | 40 | function HttpInvocation(method, ctorArgs, args, base) { 41 | this.base = base; 42 | this.method = method; 43 | this.args = args || []; 44 | this.ctorArgs = ctorArgs || []; 45 | this.isStatic = 46 | (method.hasOwnProperty('isStatic') && method.isStatic) || 47 | (method.hasOwnProperty('sharedMethod') && method.sharedMethod.isStatic); 48 | var namedArgs = this.namedArgs = {}; 49 | var val; 50 | var type; 51 | 52 | if (!this.isStatic) { 53 | method.restClass.ctor.accepts.forEach(function(accept) { 54 | val = ctorArgs.shift(); 55 | if(HttpInvocation.isAcceptable(val, accept)) { 56 | namedArgs[accept.arg || accept.name] = val; 57 | } 58 | }); 59 | } 60 | 61 | method.accepts.forEach(function(accept) { 62 | val = args.shift(); 63 | if(HttpInvocation.isAcceptable(val, accept)) { 64 | namedArgs[accept.arg || accept.name] = val; 65 | } 66 | }); 67 | } 68 | 69 | /** 70 | * Inherit from `EventEmitter`. 71 | */ 72 | 73 | inherits(HttpInvocation, EventEmitter); 74 | 75 | /** 76 | * Determine if the value matches the given accept definition. 77 | */ 78 | 79 | HttpInvocation.isAcceptable = function(val, accept) { 80 | var acceptArray = Array.isArray(accept.type) || accept.type.toLowerCase() === 'array'; 81 | var type = acceptArray ? 'array' : accept.type && accept.type.toLowerCase(); 82 | var strict = type && type !== 'any'; 83 | 84 | if(acceptArray) { 85 | return Array.isArray(val); 86 | } 87 | 88 | if(strict) { 89 | if(JSON_TYPES.indexOf(type) === -1) { 90 | return typeof val === 'object'; 91 | } 92 | return (typeof val).toLowerCase() === type; 93 | } else { 94 | return true; 95 | } 96 | } 97 | 98 | HttpInvocation.prototype._processArg = function (req, verb, query, accept) { 99 | var httpFormat = accept.http; 100 | var name = accept.name || accept.arg; 101 | var val = this.getArgByName(name); 102 | 103 | if (httpFormat) { 104 | switch (typeof httpFormat) { 105 | case 'function': 106 | // ignore defined formatter 107 | break; 108 | case 'object': 109 | switch (httpFormat.source) { 110 | case 'body': 111 | req.body = val; 112 | break; 113 | case 'form': 114 | // From the form (body) 115 | req.body = req.body || {}; 116 | req.body[name] = val; 117 | break; 118 | case 'query': 119 | // From the query string 120 | if (val !== undefined) { 121 | query = query || {}; 122 | query[name] = val; 123 | } 124 | break; 125 | case 'header': 126 | if (val !== undefined) { 127 | req.headers = req.headers || {}; 128 | req.headers[name] = val; 129 | } 130 | break; 131 | case 'path': 132 | // From the url path 133 | req.url = req.url.replace(':' + name, val); 134 | break; 135 | } 136 | break; 137 | } 138 | } else if (verb.toLowerCase() === 'get') { 139 | // default to query string for GET 140 | if (val !== undefined) { 141 | query = query || {}; 142 | query[name] = val; 143 | } 144 | } else { 145 | // default to storing args on the body for !GET 146 | req.body = req.body || {}; 147 | req.body[name] = val; 148 | } 149 | 150 | return query; 151 | }; 152 | 153 | /** 154 | * Build args object from the http context's `req` and `res`. 155 | */ 156 | 157 | HttpInvocation.prototype.createRequest = function () { 158 | var args = {}; 159 | var method = this.method; 160 | var verb = method.getHttpMethod(); 161 | var req = {json: true, method: verb || 'GET'}; 162 | var accepts = method.accepts; 163 | var ctorAccepts = null; 164 | var returns = method.returns; 165 | var errors = method.errors; 166 | var query; 167 | var i; 168 | 169 | // initial url is the format 170 | req.url = this.base + method.getFullPath(); 171 | 172 | // build request args and method options 173 | if (!this.isStatic) { 174 | ctorAccepts = method.restClass.ctor.accepts; 175 | for (i in ctorAccepts) { 176 | query = this._processArg(req, verb, query, ctorAccepts[i]); 177 | } 178 | } 179 | 180 | for (i in accepts) { 181 | query = this._processArg(req, verb, query, accepts[i]); 182 | } 183 | 184 | if(query) { 185 | req.url += '?' + qs.stringify(query); 186 | } 187 | 188 | return req; 189 | }; 190 | 191 | /** 192 | * Get an arg value by name using the given options. 193 | * 194 | * @param {String} name 195 | */ 196 | 197 | HttpInvocation.prototype.getArgByName = function (name) { 198 | return this.namedArgs[name]; 199 | } 200 | 201 | /** 202 | * Start the invocation. 203 | */ 204 | 205 | HttpInvocation.prototype.invoke = function (callback) { 206 | var req = this.createRequest(); 207 | request(req, function(err, res, body) { 208 | if(err instanceof SyntaxError) { 209 | if(res.status === 204) err = null; 210 | } 211 | if(err) return callback(err); 212 | this.transformResponse(res, body, callback); 213 | }.bind(this)); 214 | } 215 | 216 | /** 217 | * Transform the response into callback arguments 218 | * @param {HttpResponse} res 219 | * @param {Function} callback 220 | */ 221 | 222 | HttpInvocation.prototype.transformResponse = function(res, body, callback) { 223 | var callbackArgs = [null]; // null => placeholder for err 224 | var method = this.method; 225 | var returns = method.returns; 226 | var errors = method.errors; 227 | var isObject = typeof body === 'object'; 228 | var err; 229 | var hasError = res.statusCode >= 400; 230 | var errMsg; 231 | var ctx = { 232 | method: method, 233 | req: res.req, 234 | res: res 235 | }; 236 | 237 | if(hasError) { 238 | if(isObject && body.error) { 239 | err = new Error(body.error.message); 240 | err.name = body.error.name; 241 | err.stack = body.error.stack; 242 | err.details = body.error.details; 243 | } else { 244 | err = new Error('Error: ' + res.statusCode); 245 | } 246 | 247 | return callback(err); 248 | } 249 | 250 | // build request args and method options 251 | returns.forEach(function (ret) { 252 | var httpFormat = ret.http; 253 | var name = ret.name || ret.arg; 254 | var val; 255 | var dynamic; 256 | var type = ret.type; 257 | 258 | if(ret.root) { 259 | val = res.body; 260 | } else { 261 | val = res.body[name]; 262 | } 263 | 264 | if(Dynamic.canConvert(type)) { 265 | dynamic = new Dynamic(val, ctx); 266 | val = dynamic.to(type); 267 | } 268 | 269 | callbackArgs.push(val); 270 | }.bind(this)); 271 | 272 | callback.apply(this, callbackArgs); 273 | } 274 | -------------------------------------------------------------------------------- /lib/shared-class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Expose `SharedClass`. 3 | */ 4 | 5 | module.exports = SharedClass; 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var debug = require('debug')('strong-remoting:shared-class') 12 | , util = require('util') 13 | , inherits = util.inherits 14 | , inflection = require('inflection') 15 | , SharedMethod = require('./shared-method') 16 | , assert = require('assert'); 17 | 18 | /** 19 | * Create a new `SharedClass` with the given `options`. 20 | * 21 | * @class SharedClass 22 | * @param {String} name The `SharedClass` name 23 | * @param {Function} constructor The `constructor` the `SharedClass` represents 24 | * @param {Object} options Additional options. 25 | * @property {Function} ctor The `constructor` 26 | * @property {Object} http The HTTP settings 27 | * @return {SharedClass} 28 | */ 29 | 30 | function SharedClass(name, ctor, options) { 31 | options = options || {}; 32 | this.name = name || ctor.remoteNamespace; 33 | this.ctor = ctor; 34 | this._methods = []; 35 | this._resolvers = []; 36 | this._disabledMethods = {}; 37 | var http = ctor && ctor.http; 38 | var normalize = options.normalizeHttpPath; 39 | 40 | var defaultHttp = {}; 41 | defaultHttp.path = '/' + this.name; 42 | 43 | if(Array.isArray(http)) { 44 | // use array as is 45 | this.http = http; 46 | if(http.length === 0) { 47 | http.push(defaultHttp); 48 | } 49 | if (normalize) { 50 | this.http.forEach(function(h) { 51 | h.path = SharedClass.normalizeHttpPath(h.path); 52 | }); 53 | } 54 | } else { 55 | // set http.path using the name unless it is defined 56 | // TODO(ritch) move http normalization from adapter.getRoutes() to a 57 | // better place... eg SharedMethod#getRoutes() or RestClass 58 | this.http = util._extend(defaultHttp, http); 59 | if (normalize) this.http.path = SharedClass.normalizeHttpPath(this.http.path); 60 | } 61 | 62 | if (typeof ctor === 'function' && ctor.sharedCtor) { 63 | // TODO(schoon) - Can we fall back to using the ctor as a method directly? 64 | // Without that, all remote methods have to be two levels deep, e.g. 65 | // `/meta/routes`. 66 | 67 | this.sharedCtor = new SharedMethod(ctor.sharedCtor, 'sharedCtor', this); 68 | } 69 | assert(this.name, 'must include a remoteNamespace when creating a SharedClass'); 70 | } 71 | 72 | /** 73 | * Normalize http path. 74 | */ 75 | 76 | SharedClass.normalizeHttpPath = function(path) { 77 | if (typeof path !== 'string') return; 78 | return path.replace(/[^\/]+/g, function(match) { 79 | if (match.indexOf(':') > -1) return match; // skip placeholders 80 | return inflection.transform(match, ['underscore', 'dasherize']); 81 | }); 82 | } 83 | 84 | /** 85 | * Get all shared methods belonging to this shared class. 86 | * 87 | * @returns {SharedMethod[]} An array of shared methods 88 | */ 89 | 90 | SharedClass.prototype.methods = function () { 91 | var ctor = this.ctor; 92 | var methods = []; 93 | var sc = this; 94 | var functionIndex = []; 95 | 96 | // static methods 97 | eachRemoteFunctionInObject(ctor, function (fn, name) { 98 | if(functionIndex.indexOf(fn) === -1) { 99 | functionIndex.push(fn); 100 | } else { 101 | sharedMethod = find(methods, fn); 102 | sharedMethod.addAlias(name); 103 | return; 104 | } 105 | methods.push(SharedMethod.fromFunction(fn, name, sc, true)); 106 | }); 107 | 108 | // instance methods 109 | eachRemoteFunctionInObject(ctor.prototype, function (fn, name) { 110 | if(functionIndex.indexOf(fn) === -1) { 111 | functionIndex.push(fn); 112 | } else { 113 | sharedMethod = find(methods, fn); 114 | sharedMethod.addAlias(name); 115 | return; 116 | } 117 | methods.push(SharedMethod.fromFunction(fn, name, sc)); 118 | }); 119 | 120 | // resolvers 121 | this._resolvers.forEach(function(resolver) { 122 | resolver.call(this, _define.bind(sc, methods)); 123 | }); 124 | 125 | methods = methods.concat(this._methods); 126 | 127 | return methods.filter(sc.isMethodEnabled.bind(sc)); 128 | } 129 | 130 | SharedClass.prototype.isMethodEnabled = function(sharedMethod) { 131 | if(!sharedMethod.shared) return false; 132 | 133 | var key = this.getKeyFromMethodNameAndTarget(sharedMethod.name, sharedMethod.isStatic); 134 | 135 | if(this._disabledMethods.hasOwnProperty(key)) { 136 | return false; 137 | } 138 | 139 | return true; 140 | } 141 | 142 | /** 143 | * Define a shared method with the given name. 144 | * 145 | * @param {String} name The method name 146 | * @param {Object} [options] Set of options used to create a `SharedMethod`. 147 | * [See the full set of options](#sharedmethod-new-sharedmethodfn-name-sharedclass-options) 148 | */ 149 | 150 | SharedClass.prototype.defineMethod = function(name, options, fn) { 151 | return _define.call(this, this._methods, name, options, fn); 152 | } 153 | 154 | function _define(methods, name, options, fn) { 155 | options = options || {}; 156 | var sharedMethod = new SharedMethod(fn, name, this, options) 157 | methods.push(sharedMethod); 158 | return sharedMethod; 159 | } 160 | 161 | /** 162 | * Define a shared method resolver for dynamically defining methods. 163 | * 164 | * ```js 165 | * // below is a simple example 166 | * sharedClass.resolve(function(define) { 167 | * define('myMethod', { 168 | * accepts: {arg: 'str', type: 'string'}, 169 | * returns: {arg: 'str', type: 'string'} 170 | * errors: [ { code: 404, message: 'Not Found', responseModel: 'Error' } ] 171 | * }, myMethod); 172 | * }); 173 | * function myMethod(str, cb) { 174 | * cb(null, str); 175 | * } 176 | * ``` 177 | * 178 | * @param {Function} resolver 179 | */ 180 | 181 | SharedClass.prototype.resolve = function(resolver) { 182 | this._resolvers.push(resolver); 183 | } 184 | 185 | /** 186 | * Find a sharedMethod with the given name or function object. 187 | * 188 | * @param {String|Function} fn The function or method name 189 | * @param {Boolean} [isStatic] Required if `fn` is a `String`. 190 | * Only find a static method with the given name. 191 | * @returns {SharedMethod} 192 | */ 193 | 194 | SharedClass.prototype.find = function(fn, isStatic) { 195 | var methods = this.methods(); 196 | return find(methods, fn, isStatic); 197 | } 198 | 199 | /** 200 | * Disable a sharedMethod with the given name or function object. 201 | * 202 | * @param {String} fn The function or method name 203 | * @param {Boolean} isStatic Disable a static or prototype method 204 | */ 205 | 206 | SharedClass.prototype.disableMethod = function(fn, isStatic) { 207 | var disableMethods = this._disabledMethods; 208 | var key = this.getKeyFromMethodNameAndTarget(fn, isStatic); 209 | disableMethods[key] = true; 210 | } 211 | 212 | /** 213 | * Get a key for the given method. 214 | * 215 | * @param {String} fn The function or method name 216 | * @param {Boolean} isStatic 217 | */ 218 | 219 | SharedClass.prototype.getKeyFromMethodNameAndTarget = function(name, isStatic) { 220 | return (isStatic ? '' : 'prototype.') + name; 221 | } 222 | 223 | function find(methods, fn, isStatic) { 224 | for(var i = 0; i < methods.length; i++) { 225 | var method = methods[i]; 226 | if(method.isDelegateFor(fn, isStatic)) return method; 227 | } 228 | return null; 229 | } 230 | 231 | function eachRemoteFunctionInObject(obj, f) { 232 | if(!obj) return; 233 | 234 | for(var key in obj) { 235 | if(key === 'super_') { 236 | // Skip super class 237 | continue; 238 | } 239 | var fn; 240 | 241 | try { 242 | fn = obj[key]; 243 | } catch(e) { 244 | } 245 | 246 | // HACK: [rfeng] Do not expose model constructors 247 | // We have the following usage to set other model classes as properties 248 | // User.email = Email; 249 | // User.accessToken = AccessToken; 250 | // Both Email and AccessToken can have shared flag set to true 251 | if(typeof fn === 'function' && fn.shared && !fn.modelName) { 252 | f(fn, key); 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /test/shared-class.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var extend = require('util')._extend; 3 | var expect = require('chai').expect; 4 | var SharedClass = require('../lib/shared-class'); 5 | var factory = require('./helpers/shared-objects-factory.js'); 6 | var RemoteObjects = require('../'); 7 | 8 | describe('SharedClass', function() { 9 | var SomeClass; 10 | beforeEach(function() { SomeClass = factory.createSharedClass(); }); 11 | 12 | describe('constructor', function() { 13 | it('fills http.path from ctor.http', function() { 14 | SomeClass.http = { path: '/foo' }; 15 | var sc = new SharedClass('some', SomeClass); 16 | expect(sc.http.path).to.equal('/foo'); 17 | }); 18 | 19 | it('fills http.path using the name', function() { 20 | var sc = new SharedClass('some', SomeClass); 21 | expect(sc.http.path).to.equal('/some'); 22 | }); 23 | 24 | it('fills http.path using a normalized path', function() { 25 | var sc = new SharedClass('SomeClass', SomeClass, { normalizeHttpPath: true }); 26 | expect(sc.http.path).to.equal('/some-class'); 27 | }); 28 | 29 | it('does not require a sharedConstructor', function() { 30 | var myClass = {}; 31 | myClass.remoteNamespace = 'bar'; 32 | myClass.foo = function() {}; 33 | myClass.foo.shared = true; 34 | 35 | var sc = new SharedClass(undefined, myClass); 36 | var fns = sc.methods().map(function(m) {return m.name}); 37 | expect(fns).to.contain('foo'); 38 | expect(sc.http).to.eql({ path: '/bar' }); 39 | }); 40 | }); 41 | 42 | describe('sharedClass.methods()', function() { 43 | it('discovers remote methods', function() { 44 | var sc = new SharedClass('some', SomeClass); 45 | SomeClass.staticMethod = function() {}; 46 | SomeClass.staticMethod.shared = true; 47 | SomeClass.prototype.instanceMethod = function() {}; 48 | SomeClass.prototype.instanceMethod.shared = true; 49 | var fns = sc.methods().map(function(m) {return m.fn}); 50 | expect(fns).to.contain(SomeClass.staticMethod); 51 | expect(fns).to.contain(SomeClass.prototype.instanceMethod); 52 | }); 53 | it('only discovers a function once with aliases', function() { 54 | function MyClass() {}; 55 | var sc = new SharedClass('some', MyClass); 56 | var fn = function() {}; 57 | fn.shared = true; 58 | MyClass.a = fn; 59 | MyClass.b = fn; 60 | MyClass.prototype.a = fn; 61 | MyClass.prototype.b = fn; 62 | var methods = sc.methods(); 63 | var fns = methods.map(function(m) {return m.fn}); 64 | expect(fns.length).to.equal(1); 65 | expect(methods[0].aliases.sort()).to.eql(['a', 'b']); 66 | }); 67 | it('discovers multiple functions correctly', function() { 68 | function MyClass() {}; 69 | var sc = new SharedClass('some', MyClass); 70 | MyClass.a = createSharedFn(); 71 | MyClass.b = createSharedFn(); 72 | MyClass.prototype.a = createSharedFn(); 73 | MyClass.prototype.b = createSharedFn(); 74 | var fns = sc.methods().map(function(m) {return m.fn}); 75 | expect(fns.length).to.equal(4); 76 | expect(fns).to.contain(MyClass.a); 77 | expect(fns).to.contain(MyClass.b); 78 | expect(fns).to.contain(MyClass.prototype.a); 79 | expect(fns).to.contain(MyClass.prototype.b); 80 | function createSharedFn() { 81 | var fn = function() {}; 82 | fn.shared = true; 83 | return fn; 84 | } 85 | }); 86 | it('should skip properties that are model classes', function() { 87 | var sc = new SharedClass('some', SomeClass); 88 | function MockModel1() {}; 89 | MockModel1.modelName = 'M1'; 90 | MockModel1.shared = true; 91 | SomeClass.staticMethod = MockModel1; 92 | 93 | function MockModel2() {}; 94 | MockModel2.modelName = 'M2'; 95 | MockModel2.shared = true; 96 | SomeClass.prototype.instanceMethod = MockModel2; 97 | var fns = sc.methods().map(function(m) {return m.fn}); 98 | expect(fns).to.not.contain(SomeClass.staticMethod); 99 | expect(fns).to.not.contain(SomeClass.prototype.instanceMethod); 100 | }); 101 | }); 102 | 103 | describe('sharedClass.defineMethod(name, options)', function() { 104 | it('defines a remote method', function () { 105 | var sc = new SharedClass('SomeClass', SomeClass); 106 | SomeClass.prototype.myMethod = function() {}; 107 | var METHOD_NAME = 'myMethod'; 108 | sc.defineMethod(METHOD_NAME, { 109 | prototype: true 110 | }); 111 | var methods = sc.methods().map(function(m) {return m.name}); 112 | expect(methods).to.contain(METHOD_NAME); 113 | }); 114 | it('should allow a shared class to resolve dynamically defined functions', 115 | function (done) { 116 | var MyClass = function() {}; 117 | var METHOD_NAME = 'dynFn'; 118 | process.nextTick(function() { 119 | MyClass[METHOD_NAME] = function(str, cb) { 120 | cb(null, str); 121 | } 122 | done(); 123 | }); 124 | 125 | var sharedClass = new SharedClass('MyClass', MyClass); 126 | 127 | sharedClass.defineMethod(METHOD_NAME, {}); 128 | var methods = sharedClass.methods().map(function(m) {return m.name}); 129 | expect(methods).to.contain(METHOD_NAME); 130 | } 131 | ); 132 | }); 133 | 134 | describe('sharedClass.resolve(resolver)', function () { 135 | it('should allow sharedMethods to be resolved dynamically', function () { 136 | function MyClass() {}; 137 | MyClass.obj = { 138 | dyn: function(cb) { 139 | cb(); 140 | } 141 | }; 142 | var sharedClass = new SharedClass('MyClass', MyClass); 143 | sharedClass.resolve(function(define) { 144 | define('dyn', {}, MyClass.obj.dyn); 145 | }); 146 | var methods = sharedClass.methods().map(function(m) {return m.name}); 147 | expect(methods).to.contain('dyn'); 148 | }); 149 | }); 150 | 151 | describe('sharedClass.find()', function () { 152 | var sc; 153 | var sm; 154 | beforeEach(function() { 155 | sc = new SharedClass('SomeClass', SomeClass); 156 | SomeClass.prototype.myMethod = function() {}; 157 | var METHOD_NAME = 'myMethod'; 158 | sm = sc.defineMethod(METHOD_NAME, { 159 | prototype: true 160 | }); 161 | }); 162 | it('finds sharedMethod for the given function', function () { 163 | assert(sc.find(SomeClass.prototype.myMethod) === sm); 164 | }); 165 | it('find sharedMethod by name', function () { 166 | assert(sc.find('myMethod') === sm); 167 | }); 168 | }); 169 | 170 | 171 | describe('remotes.addClass(sharedClass)', function() { 172 | it('should make the class available', function () { 173 | var CLASS_NAME = 'SomeClass'; 174 | var remotes = RemoteObjects.create(); 175 | var sharedClass = new SharedClass(CLASS_NAME, SomeClass); 176 | remotes.addClass(sharedClass); 177 | var classes = remotes.classes().map(function(c) {return c.name}); 178 | expect(classes).to.contain(CLASS_NAME); 179 | }); 180 | }); 181 | 182 | describe('sharedClass.disableMethod(methodName, isStatic)', function () { 183 | var sc; 184 | var sm; 185 | var METHOD_NAME = 'testMethod'; 186 | var INST_METHOD_NAME = 'instTestMethod'; 187 | var DYN_METHOD_NAME = 'dynMethod'; 188 | 189 | beforeEach(function() { 190 | sc = new SharedClass('SomeClass', SomeClass); 191 | sm = sc.defineMethod(METHOD_NAME, {isStatic: true}); 192 | sm = sc.defineMethod(INST_METHOD_NAME, {isStatic: false}); 193 | sc.resolve(function(define) { 194 | define(DYN_METHOD_NAME, {isStatic: true}); 195 | }); 196 | }); 197 | 198 | it('excludes disabled static methods from the method list', function () { 199 | sc.disableMethod(METHOD_NAME, true); 200 | var methods = sc.methods().map(function(m) {return m.name}); 201 | expect(methods).to.not.contain(METHOD_NAME); 202 | }); 203 | 204 | it('excludes disabled prototype methods from the method list', function () { 205 | sc.disableMethod(INST_METHOD_NAME, false); 206 | var methods = sc.methods().map(function(m) {return m.name}); 207 | expect(methods).to.not.contain(INST_METHOD_NAME); 208 | }); 209 | 210 | it('excludes disabled dynamic (resolved) methods from the method list', function () { 211 | sc.disableMethod(DYN_METHOD_NAME, true); 212 | var methods = sc.methods().map(function(m) {return m.name}); 213 | expect(methods).to.not.contain(DYN_METHOD_NAME); 214 | }) 215 | ; }); 216 | }); 217 | -------------------------------------------------------------------------------- /lib/http-context.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Expose `HttpContext`. 3 | */ 4 | 5 | module.exports = HttpContext; 6 | 7 | /*! 8 | * Module dependencies. 9 | */ 10 | 11 | var EventEmitter = require('events').EventEmitter 12 | , debug = require('debug')('strong-remoting:http-context') 13 | , util = require('util') 14 | , inherits = util.inherits 15 | , assert = require('assert') 16 | , Dynamic = require('./dynamic') 17 | , js2xmlparser = require('js2xmlparser') 18 | , DEFAULT_SUPPORTED_TYPES = [ 19 | 'application/json', 'application/javascript', 'application/xml', 20 | 'text/javascript', 'text/xml', 21 | 'json', 'xml', 22 | '*/*' 23 | ]; 24 | 25 | /** 26 | * Create a new `HttpContext` with the given `options`. 27 | * 28 | * @param {Object} options 29 | * @return {HttpContext} 30 | * @class 31 | */ 32 | 33 | function HttpContext(req, res, method, options) { 34 | this.req = req; 35 | this.res = res; 36 | this.method = method; 37 | this.args = this.buildArgs(method); 38 | this.methodString = method.stringName; 39 | this.options = options || {}; 40 | this.supportedTypes = this.options.supportedTypes || DEFAULT_SUPPORTED_TYPES; 41 | 42 | if (this.supportedTypes === DEFAULT_SUPPORTED_TYPES && !this.options.xml) { 43 | // Disable all XML-based types by default 44 | this.supportedTypes = this.supportedTypes.filter(function(type) { 45 | return !/\bxml\b/i.test(type); 46 | }); 47 | } 48 | } 49 | 50 | /** 51 | * Inherit from `EventEmitter`. 52 | */ 53 | 54 | inherits(HttpContext, EventEmitter); 55 | 56 | /** 57 | * Build args object from the http context's `req` and `res`. 58 | */ 59 | 60 | HttpContext.prototype.buildArgs = function (method) { 61 | var args = {}; 62 | var ctx = this; 63 | var accepts = method.accepts; 64 | var returns = method.returns; 65 | var errors = method.errors; 66 | 67 | // build arguments from req and method options 68 | accepts.forEach(function (o) { 69 | var httpFormat = o.http; 70 | var name = o.name || o.arg; 71 | var val; 72 | 73 | if(httpFormat) { 74 | switch(typeof httpFormat) { 75 | case 'function': 76 | // the options have defined a formatter 77 | val = httpFormat(this); 78 | break; 79 | case 'object': 80 | switch(httpFormat.source) { 81 | case 'body': 82 | val = this.req.body; 83 | break; 84 | case 'form': 85 | // From the form (body) 86 | val = this.req.body && this.req.body[name]; 87 | break; 88 | case 'query': 89 | // From the query string 90 | val = this.req.query[name]; 91 | break; 92 | case 'path': 93 | // From the url path 94 | val = this.req.params[name]; 95 | break; 96 | case 'header': 97 | val = this.req.get(name); 98 | break; 99 | case 'req': 100 | // Direct access to http req 101 | val = this.req; 102 | break; 103 | case 'res': 104 | // Direct access to http res 105 | val = this.res; 106 | break; 107 | case 'context': 108 | // Direct access to http context 109 | val = this; 110 | break; 111 | } 112 | break; 113 | } 114 | } else { 115 | val = this.getArgByName(name, o); 116 | } 117 | 118 | // cast booleans and numbers 119 | var dynamic; 120 | var otype = (typeof o.type === 'string') && o.type.toLowerCase(); 121 | 122 | if(Dynamic.canConvert(otype)) { 123 | dynamic = new Dynamic(val, ctx); 124 | val = dynamic.to(otype); 125 | } 126 | 127 | // set the argument value 128 | args[o.arg] = val; 129 | }.bind(this)); 130 | 131 | return args; 132 | } 133 | 134 | /** 135 | * Get an arg by name using the given options. 136 | * 137 | * @param {String} name 138 | * @param {Object} options **optional** 139 | */ 140 | 141 | HttpContext.prototype.getArgByName = function (name, options) { 142 | var req = this.req; 143 | var args = req.param('args'); 144 | 145 | if(args) { 146 | args = JSON.parse(args); 147 | } 148 | 149 | if(typeof args !== 'object' || !args) { 150 | args = {}; 151 | } 152 | 153 | var arg = (args && args[name] !== undefined) ? args[name] : 154 | this.req.param(name) !== undefined ? this.req.param(name) : 155 | this.req.get(name); 156 | // search these in order by name 157 | // req.params 158 | // req.body 159 | // req.query 160 | // req.header 161 | 162 | 163 | // coerce simple types in objects 164 | if(typeof arg === 'object') { 165 | arg = coerceAll(arg); 166 | } 167 | 168 | return arg; 169 | } 170 | 171 | /*! 172 | * Integer test regexp. 173 | */ 174 | 175 | var isint = /^[0-9]+$/; 176 | 177 | /*! 178 | * Float test regexp. 179 | */ 180 | 181 | var isfloat = /^([0-9]+)?\.[0-9]+$/; 182 | 183 | function coerce(str) { 184 | if(typeof str != 'string') return str; 185 | if ('null' == str) return null; 186 | if ('true' == str) return true; 187 | if ('false' == str) return false; 188 | if (isfloat.test(str)) return parseFloat(str, 10); 189 | if (isint.test(str)) return parseInt(str, 10); 190 | return str; 191 | } 192 | 193 | // coerce every string in the given object / array 194 | function coerceAll(obj) { 195 | var type = Array.isArray(obj) ? 'array' : typeof obj; 196 | 197 | switch(type) { 198 | case 'string': 199 | return coerce(obj); 200 | break; 201 | case 'object': 202 | if(obj) { 203 | Object.keys(obj).forEach(function (key) { 204 | obj[key] = coerceAll(obj[key]); 205 | }); 206 | } 207 | break; 208 | case 'array': 209 | obj.map(function (o) { 210 | return coerceAll(o); 211 | }); 212 | break; 213 | } 214 | 215 | return obj; 216 | } 217 | 218 | /** 219 | * Invoke the given shared method using the provided scope against the current context. 220 | */ 221 | 222 | HttpContext.prototype.invoke = function (scope, method, fn, isCtor) { 223 | var args = this.args; 224 | if(isCtor) { 225 | try { 226 | args = this.buildArgs(method); 227 | } catch(err) { 228 | // JSON.parse() might throw 229 | return fn(err); 230 | } 231 | } 232 | var http = method.http; 233 | var pipe = http && http.pipe; 234 | var pipeDest = pipe && pipe.dest; 235 | var pipeSrc = pipe && pipe.source; 236 | 237 | if(pipeDest) { 238 | // only support response for now 239 | switch(pipeDest) { 240 | case 'res': 241 | // Probably not correct...but passes my test. 242 | this.res.header('Content-Type', 'application/json'); 243 | this.res.header('Transfer-Encoding', 'chunked'); 244 | 245 | var stream = method.invoke(scope, args, fn); 246 | stream.pipe(this.res); 247 | break; 248 | default: 249 | fn(new Error('unsupported pipe destination')); 250 | break; 251 | } 252 | } else if(pipeSrc) { 253 | // only support request for now 254 | switch(pipeDest) { 255 | case 'req': 256 | this.req.pipe(method.invoke(scope, args, fn)); 257 | break; 258 | default: 259 | fn(new Error('unsupported pipe source')); 260 | break; 261 | } 262 | } else { 263 | // simple invoke 264 | method.invoke(scope, args, fn); 265 | } 266 | } 267 | 268 | function toJSON(input) { 269 | if (!input) { 270 | return input; 271 | } 272 | if (typeof input.toJSON === 'function') { 273 | return input.toJSON(); 274 | } else if (Array.isArray(input)) { 275 | return input.map(toJSON); 276 | } else { 277 | return input; 278 | } 279 | } 280 | 281 | function toXML(input) { 282 | var xml; 283 | if (input && typeof input.toXML === 'function') { 284 | xml = input.toXML(); 285 | } else { 286 | if (input) { 287 | // Trigger toJSON() conversions 288 | input = toJSON(input); 289 | } 290 | if (Array.isArray(input)) { 291 | input = { result: input }; 292 | } 293 | xml = js2xmlparser('response', input, { 294 | prettyPrinting: { 295 | indentString: ' ' 296 | }, 297 | convertMap: { 298 | '[object Date]': function(date) { 299 | return date.toISOString(); 300 | } 301 | } 302 | }); 303 | } 304 | return xml; 305 | } 306 | /** 307 | * Finish the request and send the correct response. 308 | */ 309 | 310 | HttpContext.prototype.done = function () { 311 | // send the result back as 312 | // the requested content type 313 | var data = this.result; 314 | var res = this.res; 315 | var accepts = this.req.accepts(this.supportedTypes); 316 | 317 | if (this.req.query._format) { 318 | accepts = this.req.query._format.toLowerCase(); 319 | } 320 | var dataExists = typeof data !== 'undefined'; 321 | 322 | if(dataExists) { 323 | switch(accepts) { 324 | case '*/*': 325 | case 'application/json': 326 | case 'json': 327 | res.json(data); 328 | break; 329 | case 'application/javascript': 330 | case 'text/javascript': 331 | res.jsonp(data); 332 | break; 333 | case 'application/xml': 334 | case 'text/xml': 335 | case 'xml': 336 | if (accepts === 'application/xml') { 337 | res.header('Content-Type', 'application/xml'); 338 | } else { 339 | res.header('Content-Type', 'text/xml'); 340 | } 341 | if (data === null) { 342 | res.header('Content-Length', '7'); 343 | res.end(''); 344 | } else { 345 | try { 346 | var xml = toXML(data); 347 | res.send(xml); 348 | } catch(e) { 349 | res.send(500, e + '\n' + data); 350 | } 351 | } 352 | break; 353 | default: 354 | // not acceptable 355 | res.send(406); 356 | break; 357 | } 358 | } else { 359 | res.get('Content-Type') || res.header('Content-Type', 'application/json'); 360 | res.statusCode = 204; 361 | res.end(); 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /example/rest-models/public/todos.js: -------------------------------------------------------------------------------- 1 | // An example Backbone application contributed by 2 | // [Jérôme Gravel-Niquet](http://jgn.me/). This demo uses a simple 3 | // [LocalStorage adapter](backbone-localstorage.html) 4 | // to persist Backbone models within your browser. 5 | 6 | // Load the application once the DOM is ready, using `jQuery.ready`: 7 | $(function(){ 8 | 9 | // Remote REST Contract 10 | var contract = { 11 | routes: { 12 | 'Todo.all': {verb: 'get', path: '/t'}, 13 | 'Todo.prototype.save': {verb: 'post', path: '/t/:id'}, 14 | 'Todo.prototype.fetch': {verb: 'get', path: '/t/:id'} 15 | } 16 | }; 17 | 18 | // Remote Objects 19 | var remoteObjects = RemoteObjects.connect('http://localhost:3000', contract); 20 | 21 | // Todo Model 22 | // ---------- 23 | 24 | // Our basic **Todo** model has `title`, `order`, and `done` attributes. 25 | var Todo = Backbone.Model.extend({ 26 | 27 | // Default attributes for the todo item. 28 | defaults: function() { 29 | return { 30 | title: "empty todo...", 31 | order: Todos.nextOrder(), 32 | done: false 33 | }; 34 | }, 35 | 36 | // Toggle the `done` state of this todo item. 37 | toggle: function() { 38 | this.save({done: !this.get("done")}); 39 | }, 40 | 41 | save: function (data) { 42 | this.set(data); 43 | var remoteObj = remoteObjects.construct('Todo', {data: this.toJSON()}); 44 | remoteObj.invoke('save', function () { 45 | // saved 46 | }); 47 | } 48 | }); 49 | 50 | // Todo Collection 51 | // --------------- 52 | 53 | // The collection of todos is backed a remote server. 54 | var TodoList = Backbone.Collection.extend({ 55 | 56 | // Reference to this collection's model. 57 | model: Todo, 58 | 59 | // Filter down the list of all todo items that are finished. 60 | done: function() { 61 | return this.where({done: true}); 62 | }, 63 | 64 | // Filter down the list to only todo items that are still not finished. 65 | remaining: function() { 66 | return this.without.apply(this, this.done()); 67 | }, 68 | 69 | // We keep the Todos in sequential order, despite being saved by unordered 70 | // GUID in the database. This generates the next order number for new items. 71 | nextOrder: function() { 72 | if (!this.length) return 1; 73 | return this.last().get('order') + 1; 74 | }, 75 | 76 | // Todos are sorted by their original insertion order. 77 | comparator: 'order', 78 | 79 | fetch: function () { 80 | var self = this; 81 | 82 | remoteObjects.invoke('Todo.all', null, null, function (err, data) { 83 | self.reset(data); 84 | }); 85 | } 86 | }); 87 | 88 | // Create our global collection of **Todos**. 89 | var Todos = new TodoList; 90 | 91 | // Todo Item View 92 | // -------------- 93 | 94 | // The DOM element for a todo item... 95 | var TodoView = Backbone.View.extend({ 96 | 97 | //... is a list tag. 98 | tagName: "li", 99 | 100 | // Cache the template function for a single item. 101 | template: _.template($('#item-template').html()), 102 | 103 | // The DOM events specific to an item. 104 | events: { 105 | "click .toggle" : "toggleDone", 106 | "dblclick .view" : "edit", 107 | "click a.destroy" : "clear", 108 | "keypress .edit" : "updateOnEnter", 109 | "blur .edit" : "close" 110 | }, 111 | 112 | // The TodoView listens for changes to its model, re-rendering. Since there's 113 | // a one-to-one correspondence between a **Todo** and a **TodoView** in this 114 | // app, we set a direct reference on the model for convenience. 115 | initialize: function() { 116 | this.listenTo(this.model, 'change', this.render); 117 | this.listenTo(this.model, 'destroy', this.remove); 118 | }, 119 | 120 | // Re-render the titles of the todo item. 121 | render: function() { 122 | this.$el.html(this.template(this.model.toJSON())); 123 | this.$el.toggleClass('done', this.model.get('done')); 124 | this.input = this.$('.edit'); 125 | return this; 126 | }, 127 | 128 | // Toggle the `"done"` state of the model. 129 | toggleDone: function() { 130 | this.model.toggle(); 131 | }, 132 | 133 | // Switch this view into `"editing"` mode, displaying the input field. 134 | edit: function() { 135 | this.$el.addClass("editing"); 136 | this.input.focus(); 137 | }, 138 | 139 | // Close the `"editing"` mode, saving changes to the todo. 140 | close: function() { 141 | var value = this.input.val(); 142 | if (!value) { 143 | this.clear(); 144 | } else { 145 | this.model.save({title: value}); 146 | this.$el.removeClass("editing"); 147 | } 148 | }, 149 | 150 | // If you hit `enter`, we're through editing the item. 151 | updateOnEnter: function(e) { 152 | if (e.keyCode == 13) this.close(); 153 | }, 154 | 155 | // Remove the item, destroy the model. 156 | clear: function() { 157 | this.model.destroy(); 158 | } 159 | 160 | }); 161 | 162 | // The Application 163 | // --------------- 164 | 165 | // Our overall **AppView** is the top-level piece of UI. 166 | var AppView = Backbone.View.extend({ 167 | 168 | // Instead of generating a new element, bind to the existing skeleton of 169 | // the App already present in the HTML. 170 | el: $("#todoapp"), 171 | 172 | // Our template for the line of statistics at the bottom of the app. 173 | statsTemplate: _.template($('#stats-template').html()), 174 | 175 | // Delegated events for creating new items, and clearing completed ones. 176 | events: { 177 | "keypress #new-todo": "createOnEnter", 178 | "click #clear-completed": "clearCompleted", 179 | "click #toggle-all": "toggleAllComplete" 180 | }, 181 | 182 | // At initialization we bind to the relevant events on the `Todos` 183 | // collection, when items are added or changed. Kick things off by 184 | // loading any preexisting todos that might be saved in *localStorage*. 185 | initialize: function() { 186 | 187 | this.input = this.$("#new-todo"); 188 | this.allCheckbox = this.$("#toggle-all")[0]; 189 | 190 | this.listenTo(Todos, 'add', this.addOne); 191 | this.listenTo(Todos, 'reset', this.addAll); 192 | this.listenTo(Todos, 'all', this.render); 193 | 194 | this.footer = this.$('footer'); 195 | this.main = $('#main'); 196 | 197 | Todos.fetch(); 198 | }, 199 | 200 | // Re-rendering the App just means refreshing the statistics -- the rest 201 | // of the app doesn't change. 202 | render: function() { 203 | var done = Todos.done().length; 204 | var remaining = Todos.remaining().length; 205 | 206 | if (Todos.length) { 207 | this.main.show(); 208 | this.footer.show(); 209 | this.footer.html(this.statsTemplate({done: done, remaining: remaining})); 210 | } else { 211 | this.main.hide(); 212 | this.footer.hide(); 213 | } 214 | 215 | this.allCheckbox.checked = !remaining; 216 | }, 217 | 218 | // Add a single todo item to the list by creating a view for it, and 219 | // appending its element to the `