diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 0000000..843b4f7 --- /dev/null +++ b/.jshintignore @@ -0,0 +1,2 @@ +node_modules +client/node_modules diff --git a/LICENSE.md b/LICENSE.md index 29d7815..12c1973 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,9 +1,21 @@ -Copyright (c) 2013-2015 StrongLoop, Inc and other contributors. +Copyright (c) 2013-2015 StrongLoop, Inc. -loopback uses a dual license model. +MIT license -You may use this library under the terms of the [MIT License][], -or under the terms of the [StrongLoop Subscription Agreement][]. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -[MIT License]: http://opensource.org/licenses/MIT -[StrongLoop Subscription Agreement]: http://strongloop.com/license +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 2eede18..8449186 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,87 @@ -# WORK IN PROGRESS +# loopback-example-connector (remote) -This repo is still in the works. The contents may or not be up to date at this -point in time. +This is a very simple example of using the LoopBack [remote connector](http://loopback.io/doc/en/lb3/Remote-connector.html), [loopback-connector-remote](https://github.com/strongloop/loopback-connector-remote). ---- +## Overview -# loopback-example-connector +The example has the following structure: -LoopBack connector examples. +* `server`: A LoopBack application that connects to a backend data source (just the in-memory data source here) and provides a CRUD API (both Node and REST) to interact with the data source. +* `client`: A Node application that connects to the LoopBack server application using the [remote connector](https://github.com/strongloop/loopback-connector-remote). This acts as a very simple Node client SDK for LoopBack. +* `common/models`: Model definitions shared between client and server applications. Using a shared model definition ensures that client and server expect the same model structures. This simple example defines only +one model: `Person`, with a single property, `name`. +* `examples`: Contains examples of using the Node SDK in `client` to connect to the server API. + * `create.js`: A simple example script that creates a new Person record (instance). + +## How to run the examples + +**Clone the repo** + +``` +$ git clone https://github.com/strongloop/loopback-example-connector.git +$ cd loopback-example-connector +$ git checkout remote +``` + +**Starting the Server** + +Initially, you need to run `npm install` to install all the dependencies for both client and server. +Then, start the server application. + +``` +$ cd client +$ npm install +$ cd ../server +$ npm install +$ node . +``` + +**Basic CRUD Example** + +Now in another shell, run an example that uses the client "SDK." + +``` +$ node examples/create.js +Created Person... +{ name: 'Fred', id: 1 } +``` + +Now open LoopBack Explorer at http://0.0.0.0:3001/explorer/. This provides a view into the server application REST API. + +Go to http://0.0.0.0:3001/explorer/#!/People/find to expand the `GET /People` operation. +Then click **Try it!**. + +In **Response Body**, you will see the record that `create.js` created via the Node client SDK: + +``` +[ + { + "name": "Fred", + "id": 1 + } +] +``` + +**Auth Example** + +This example demonstrates the following basic tasks (using the remote connector): + + - Registering a user + - Logging in as a user + - Defining a custom remote method + - Securing access to custom methods + +After running the server, you can run the `examples/auth.js` example in a +separate shell. + +``` +$ node examples/auth.js +Got error (Authorization Required) when trying to call method without auth +Registered a user +Logged in as foo@bar.com +Set access token for all future requests. (MGd...JMA==) +Called a custom method (myMethod) as a logged in user +Logged out and unset the acces token for future invocations +Got error (Authorization Required) when trying to call method without auth +``` -Connector|Branch -:--|:-- -Remote|https://github.com/strongloop/loopback-example-connector/tree/remote -REST|https://github.com/strongloop/loopback-example-connector/tree/rest -SOAP|https://github.com/strongloop/loopback-example-connector/tree/soap diff --git a/client/.editorconfig b/client/.editorconfig new file mode 100644 index 0000000..3ee22e5 --- /dev/null +++ b/client/.editorconfig @@ -0,0 +1,13 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/client/.jshintignore b/client/.jshintignore new file mode 100644 index 0000000..ee8c771 --- /dev/null +++ b/client/.jshintignore @@ -0,0 +1,2 @@ +/client/ +/node_modules/ diff --git a/client/.jshintrc b/client/.jshintrc new file mode 100644 index 0000000..feb0928 --- /dev/null +++ b/client/.jshintrc @@ -0,0 +1,21 @@ +{ + "node": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "eqeqeq": true, + "eqnull": true, + "immed": true, + "indent": 2, + "latedef": "nofunc", + "newcap": true, + "nonew": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": false, + "trailing": true, + "sub": true, + "maxlen": 80 +} diff --git a/client/.npmignore b/client/.npmignore new file mode 100644 index 0000000..7ec7473 --- /dev/null +++ b/client/.npmignore @@ -0,0 +1,16 @@ +.idea +.project +*.sublime-* +.DS_Store +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.swp +*.swo +node_modules +coverage +*.tgz +*.xml diff --git a/client/client.js b/client/client.js new file mode 100644 index 0000000..7531930 --- /dev/null +++ b/client/client.js @@ -0,0 +1,9 @@ +var loopback = require('loopback'); +var boot = require('loopback-boot'); + +var app = module.exports = loopback(); + +// Bootstrap the application, configure models, datasources and middleware. +// Sub-apps like REST API are mounted via boot scripts. +boot(app, __dirname); + diff --git a/client/config.json b/client/config.json new file mode 100644 index 0000000..8404e74 --- /dev/null +++ b/client/config.json @@ -0,0 +1,29 @@ +{ + "restApiRoot": "/api", + "host": "0.0.0.0", + "port": 3000, + "remoting": { + "context": { + "enableHttpContext": false + }, + "rest": { + "normalizeHttpPath": false, + "xml": false + }, + "json": { + "strict": false, + "limit": "100kb" + }, + "urlencoded": { + "extended": true, + "limit": "100kb" + }, + "cors": { + "origin": true, + "credentials": true + }, + "errorHandler": { + "disableStackTrace": false + } + } +} diff --git a/client/datasources.json b/client/datasources.json new file mode 100644 index 0000000..9f6512a --- /dev/null +++ b/client/datasources.json @@ -0,0 +1,11 @@ +{ + "db": { + "name": "db", + "connector": "memory" + }, + "remoteDS": { + "url": "http://0.0.0.0:3001/api", + "name": "remoteDS", + "connector": "remote" + } +} diff --git a/client/middleware.json b/client/middleware.json new file mode 100644 index 0000000..9b009cc --- /dev/null +++ b/client/middleware.json @@ -0,0 +1,27 @@ +{ + "initial:before": { + "loopback#favicon": {} + }, + "initial": { + "compression": {} + }, + "session": { + }, + "auth": { + }, + "parse": { + }, + "routes": { + "loopback#status": { + "paths": "/" + } + }, + "files": { + }, + "final": { + "loopback#urlNotFound": {} + }, + "final:after": { + "errorhandler": {} + } +} diff --git a/client/model-config.json b/client/model-config.json new file mode 100644 index 0000000..a3a6dc5 --- /dev/null +++ b/client/model-config.json @@ -0,0 +1,16 @@ +{ + "_meta": { + "sources": [ + "loopback/common/models", + "loopback/server/models", + "../common/models", + "./models" + ] + }, + "Person": { + "dataSource": "remoteDS" + }, + "Customer": { + "dataSource": "remoteDS" + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..61c276f --- /dev/null +++ b/client/package.json @@ -0,0 +1,28 @@ +{ + "name": "remote-connector-client", + "version": "1.0.0", + "main": "server/server.js", + "scripts": { + "pretest": "jshint ." + }, + "dependencies": { + "async": "^0.9.0", + "compression": "^1.0.3", + "errorhandler": "^1.1.1", + "loopback": "^2.8.0", + "loopback-boot": "^2.4.0", + "loopback-datasource-juggler": "^2.7.0", + "serve-favicon": "^2.0.1" + }, + "optionalDependencies": { + "loopback-explorer": "^1.1.0" + }, + "devDependencies": { + "jshint": "^2.5.6" + }, + "repository": { + "type": "", + "url": "" + }, + "description": "remote-connector-client" +} diff --git a/common/models/customer.js b/common/models/customer.js new file mode 100644 index 0000000..83db41b --- /dev/null +++ b/common/models/customer.js @@ -0,0 +1,13 @@ +module.exports = function(Customer) { + Customer.myMethod = function(cb) { + cb(); + }; + + Customer.remoteMethod('myMethod', { + isStatic: true, + returns: { + arg: 'user', + type: 'Customer' + } + }); +}; diff --git a/common/models/customer.json b/common/models/customer.json new file mode 100644 index 0000000..f04b462 --- /dev/null +++ b/common/models/customer.json @@ -0,0 +1,18 @@ +{ + "name": "Customer", + "base": "User", + "idInjection": true, + "properties": {}, + "validations": [], + "relations": {}, + "acls": [ + { + "accessType": "EXECUTE", + "principalType": "ROLE", + "principalId": "$authenticated", + "permission": "ALLOW", + "property": "myMethod" + } + ], + "methods": [] +} diff --git a/common/models/person.js b/common/models/person.js new file mode 100644 index 0000000..57960df --- /dev/null +++ b/common/models/person.js @@ -0,0 +1,3 @@ +module.exports = function(Person) { + +}; diff --git a/common/models/person.json b/common/models/person.json new file mode 100644 index 0000000..c8a52f1 --- /dev/null +++ b/common/models/person.json @@ -0,0 +1,15 @@ +{ + "name": "Person", + "plural": "People", + "base": "PersistedModel", + "idInjection": true, + "properties": { + "name": { + "type": "string" + } + }, + "validations": [], + "relations": {}, + "acls": [], + "methods": [] +} diff --git a/examples/auth.js b/examples/auth.js new file mode 100644 index 0000000..665aeb9 --- /dev/null +++ b/examples/auth.js @@ -0,0 +1,88 @@ +var async = require('async'); + +// the client loopback application +var app = require('../client/client'); + +// the Customer model +var Customer = app.models.Customer; + +// the remote datasource +var remoteDs = app.dataSources.remoteDS; + +// the strong-remoting RemoteObjects instance +var remotes = remoteDs.connector.remotes; + +// the example user credentials +var credentials = { + email: 'foo@bar.com', + password: '1234' +}; + +async.series([ + function(next) { + Customer.myMethod(function(err) { + if(err) { + console.log('Got error (%s) when trying to call method without auth', err.message); + } + next(); + }); + }, + function(next) { + // register a user + Customer.create(credentials, handleError(function(err) { + console.log('Registered a user'); + next(); + })); + }, + function(next) { + // login as the newly created user + Customer.login(credentials, handleError(function(err, token) { + console.log('Logged in as', credentials.email); + + // store the token to allow logout + credentials.token = token; + + // set the access token to be used for all future invocations + remotes.auth = { + bearer: (new Buffer(token.id)).toString('base64'), + sendImmediately: true + }; + console.log('Set access token for all future requests. (%s)', remotes.auth.bearer); + next(); + })); + }, + function(next) { + // this method can only be called by logged in users + Customer.myMethod(handleError(function(err) { + console.log('Called a custom method (myMethod) as a logged in user'); + next(); + })); + }, + function(next) { + Customer.logout(credentials.token.id, handleError(function(err) { + console.log('Logged out and unset the acces token for future invocations'); + // unset the access token for future invocations + remotes.auth = null; + next(); + })); + }, + function(next) { + Customer.myMethod(function(err) { + if(err) { + console.log('Got error (%s) when trying to call method without auth', err.message); + } + }); + } +]); + +// utility for handling errors +function handleError(fn) { + return function(err) { + if(err) { + console.error(err); + process.exit(); + } else { + fn.apply(this, arguments); + } + }; +} diff --git a/examples/create.js b/examples/create.js new file mode 100644 index 0000000..477eb12 --- /dev/null +++ b/examples/create.js @@ -0,0 +1,10 @@ +app = require('../client/client'); + +// call a method on the server +app.models.Person.create({ + name: 'Fred' +}, function(err, newperson) { + console.log("Created Person..."); + console.log(err || newperson); +}); + diff --git a/package.json b/package.json new file mode 100644 index 0000000..734f927 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "loopback-example-remote", + "version": "1.0.0", + "description": "This is a very simple example of using the LoopBack [remote connector](http://docs.strongloop.com/display/LB/Remote+connector), [loopback-connector-remote](https://github.com/strongloop/loopback-connector-remote).", + "main": "server/server.js", + "directories": { + "example": "examples" + }, + "scripts": { + "postinstall": "npm i client/", + "test": "mocha -R spec test", + "pretest": "jshint ." + }, + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-example-remote.git" + }, + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/strongloop/loopback-example-remote/issues" + }, + "homepage": "https://github.com/strongloop/loopback-example-remote", + "dependencies": { + "async": "^0.9.0", + "compression": "^1.0.3", + "errorhandler": "^1.1.1", + "loopback": "^2.8.0", + "loopback-boot": "^2.4.0", + "loopback-component-explorer": "^2.1.0", + "loopback-datasource-juggler": "^2.7.0", + "serve-favicon": "^2.0.1", + "supertest": "^1.1.0" + }, + "optionalDependencies": { + }, + "devDependencies": { + "mocha": "^1.20.1", + "jshint": "^2.5.6" + } +} diff --git a/server/.editorconfig b/server/.editorconfig new file mode 100644 index 0000000..3ee22e5 --- /dev/null +++ b/server/.editorconfig @@ -0,0 +1,13 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/server/.jshintignore b/server/.jshintignore new file mode 100644 index 0000000..ee8c771 --- /dev/null +++ b/server/.jshintignore @@ -0,0 +1,2 @@ +/client/ +/node_modules/ diff --git a/server/.jshintrc b/server/.jshintrc new file mode 100644 index 0000000..25956ef --- /dev/null +++ b/server/.jshintrc @@ -0,0 +1,28 @@ +{ + "node": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "eqeqeq": true, + "eqnull": true, + "immed": true, + "indent": 2, + "latedef": "nofunc", + "newcap": true, + "nonew": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": false, + "trailing": true, + "sub": true, + "maxlen": 80, + "globals" : { + /* MOCHA */ + "describe" : false, + "it" : false, + "before" : false, + "after" : false + } +} diff --git a/server/.npmignore b/server/.npmignore new file mode 100644 index 0000000..7ec7473 --- /dev/null +++ b/server/.npmignore @@ -0,0 +1,16 @@ +.idea +.project +*.sublime-* +.DS_Store +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.swp +*.swo +node_modules +coverage +*.tgz +*.xml diff --git a/server/boot/authentication.js b/server/boot/authentication.js new file mode 100644 index 0000000..a87cd08 --- /dev/null +++ b/server/boot/authentication.js @@ -0,0 +1,4 @@ +module.exports = function enableAuthentication(server) { + // enable authentication + server.enableAuth(); +}; diff --git a/server/component-config.json b/server/component-config.json new file mode 100644 index 0000000..2b75490 --- /dev/null +++ b/server/component-config.json @@ -0,0 +1,5 @@ +{ + "loopback-component-explorer": { + "mountPath": "explorer" + } +} \ No newline at end of file diff --git a/server/config.json b/server/config.json new file mode 100644 index 0000000..05bc9ca --- /dev/null +++ b/server/config.json @@ -0,0 +1,29 @@ +{ + "restApiRoot": "/api", + "host": "0.0.0.0", + "port": 3001, + "remoting": { + "context": { + "enableHttpContext": false + }, + "rest": { + "normalizeHttpPath": false, + "xml": false + }, + "json": { + "strict": false, + "limit": "100kb" + }, + "urlencoded": { + "extended": true, + "limit": "100kb" + }, + "cors": { + "origin": true, + "credentials": true + }, + "errorHandler": { + "disableStackTrace": false + } + } +} diff --git a/server/datasources.json b/server/datasources.json new file mode 100644 index 0000000..d6caf56 --- /dev/null +++ b/server/datasources.json @@ -0,0 +1,6 @@ +{ + "db": { + "name": "db", + "connector": "memory" + } +} diff --git a/server/middleware.json b/server/middleware.json new file mode 100644 index 0000000..bd33ee4 --- /dev/null +++ b/server/middleware.json @@ -0,0 +1,27 @@ +{ + "initial:before": { + "loopback#favicon": {} + }, + "initial": { + "compression": {} + }, + "session": { + }, + "auth": { + }, + "parse": { + }, + "routes": { + "loopback#rest": { + "paths": ["${restApiRoot}"] + } + }, + "files": { + }, + "final": { + "loopback#urlNotFound": {} + }, + "final:after": { + "errorhandler": {} + } +} diff --git a/server/model-config.json b/server/model-config.json new file mode 100644 index 0000000..a95b52a --- /dev/null +++ b/server/model-config.json @@ -0,0 +1,37 @@ +{ + "_meta": { + "sources": [ + "loopback/common/models", + "loopback/server/models", + "../common/models", + "./models" + ] + }, + "User": { + "dataSource": "db" + }, + "AccessToken": { + "dataSource": "db", + "public": false + }, + "ACL": { + "dataSource": "db", + "public": false + }, + "RoleMapping": { + "dataSource": "db", + "public": false + }, + "Role": { + "dataSource": "db", + "public": false + }, + "Person": { + "dataSource": "db", + "public": true + }, + "Customer": { + "dataSource": "db", + "public": true + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..e5c12a0 --- /dev/null +++ b/server/package.json @@ -0,0 +1,27 @@ +{ + "name": "remote-connector-server", + "version": "1.0.0", + "main": "server/server.js", + "scripts": { + "pretest": "jshint ." + }, + "dependencies": { + "compression": "^1.0.3", + "errorhandler": "^1.1.1", + "loopback": "^2.8.0", + "loopback-boot": "^2.4.0", + "loopback-datasource-juggler": "^2.7.0", + "serve-favicon": "^2.0.1" + }, + "optionalDependencies": { + "loopback-explorer": "^1.1.0" + }, + "devDependencies": { + "jshint": "^2.5.6" + }, + "repository": { + "type": "", + "url": "" + }, + "description": "remote-connector-server" +} diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..e49265f --- /dev/null +++ b/server/server.js @@ -0,0 +1,25 @@ +var loopback = require('loopback'); +var boot = require('loopback-boot'); +var app = module.exports = loopback(); + + +// Bootstrap the application, configure models, datasources and middleware. +// Sub-apps like REST API are mounted via boot scripts. +boot(app, __dirname); + +app.start = function() { + // start the web server + return app.listen(function() { + app.emit('started'); + console.log('Web server listening at: %s', app.get('url')); + if (app.get('loopback-component-explorer')) { + var explorerPath = app.get('loopback-component-explorer').mountPath; + console.log('Browse your REST API at %s%s', app.get('url'), explorerPath); + } + }); +}; + +// start the server if `$ node server.js` +if (require.main === module) { + app.start(); +} diff --git a/test/rest_api_test.js b/test/rest_api_test.js new file mode 100644 index 0000000..a68cb27 --- /dev/null +++ b/test/rest_api_test.js @@ -0,0 +1,60 @@ +/* jshint camelcase: false */ +var app = require('../server/server'); +var client_app = require('../client/client'); +var request = require('supertest'); +var assert = require('assert'); + +function json(verb, url) { + return request(app)[verb](url) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect('Content-Type', /json/); + } + +describe('REST API request', function() { + before(function(done) { + require('./start-server'); + done(); + }); + + after(function(done) { + app.removeAllListeners('started'); + app.removeAllListeners('loaded'); + done(); + }); + + it('should create a person', function(done) { + app.models.Person.create({name: 'Fred'}, function(err, newperson) { + console.log("Created Person..."); + console.log(err || newperson); + assert(newperson); + done(); + }); + }); + + var credentials = { + email: 'foo@bar.com', + password: '1234' + }; + it('should create a customer', function(done) { + app.models.Customer.create(credentials, function(err, customer) { + console.log("Created Customer..."); + console.log(err || customer); + assert(customer); + app.models.Customer.login(credentials, function(err, token){ + console.log("Logged in Customer..."); + console.log(err || token); + assert(token); + done(); + }); + }); + }); +}); + +describe('Unexpected Usage', function(){ + it('should not crash the server when posting a bad id', function(done){ + json('post', '/api/users/foobar') + .send({}) + .expect(404, done); + }); +}); diff --git a/test/start-server.js b/test/start-server.js new file mode 100644 index 0000000..51ffc0c --- /dev/null +++ b/test/start-server.js @@ -0,0 +1,13 @@ +var app = require('../server/server'); + +module.exports = function(done) { + if (app.loaded) { + app.once('started', done); + app.start(); + } else { + app.once('loaded', function() { + app.once('started', done); + app.start(); + }); + } +};