diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 00000000..ed1f2e3e --- /dev/null +++ b/.jscsrc @@ -0,0 +1,22 @@ +{ + "preset": "google", + "requireCurlyBraces": [ + "else", + "for", + "while", + "do", + "try", + "catch" + ], + "disallowSpacesInsideObjectBrackets": null, + "maximumLineLength": { + "value": 150, + "allowComments": true, + "allowRegex": true + }, + "validateJSDoc": { + "checkParamNames": false, + "checkRedundantParams": false, + "requireParamTypes": true + } +} diff --git a/.jshintrc b/.jshintrc index 91fb46ef..5665b3fe 100644 --- a/.jshintrc +++ b/.jshintrc @@ -5,10 +5,9 @@ "indent": 2, "undef": true, "quotmark": "single", -"maxlen": 150, -"trailing": true, "newcap": true, "nonew": true, +"sub": true, "laxcomma": true, "laxbreak": true } diff --git a/CHANGES.md b/CHANGES.md index d8551ddb..1a66f035 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,35 +1,2286 @@ -# Breaking Changes +2014-11-19, Version 2.8.0 +========================= -# 1.9 + * Expose more loopback middleware for require (Raymond Feng) -## Remote Method API + * Scope app middleware to a list of paths (Miroslav Bajtoš) -`loopback.remoteMethod()` is now deprecated. + * Update CONTRIBUTING.md (Alex Voitau) -Defining remote methods now should be done like this: + * Fix the model name for hasMany/through relation (Raymond Feng) -```js -// static -MyModel.greet = function(msg, cb) { - cb(null, 'greetings... ' + msg); -} + * Fixing the model attach (wfgomes) -MyModel.remoteMethod( - 'greet', - { - accepts: [{arg: 'msg', type: 'string'}], - returns: {arg: 'greeting', type: 'string'} - } -); -``` + * Minor: update jsdoc for PersistedModel.updateAll (Alex Voitau) -**NOTE: remote instance method support is also now deprecated... -Use static methods instead. If you absolutely need it you can still set -`options.isStatic = false`** We plan to drop support for instance methods in -`2.0`. + * AccessToken: optional `options` in findForRequest (Miroslav Bajtoš) -## Remote Instance Methods + * server-app: improve jsdoc comments (Miroslav Bajtoš) -All remote instance methods have been replaced with static replacements. + * server-app: middleware API improvements (Miroslav Bajtoš) -The REST API is backwards compatible. + * typo of port server (wfgomes) + + * Move middleware sources to `server/middleware` (Miroslav Bajtoš) + + * app.middleware: verify serial exec of handlers (Miroslav Bajtoš) + + * Simplify `app.defineMiddlewarePhases` (Miroslav Bajtoš) + + * Make sure loopback has all properties from express (Raymond Feng) + + * Implement `app.defineMiddlewarePhases` (Miroslav Bajtoš) + + * Implement app.middlewareFromConfig (Miroslav Bajtoš) + + * middleware/token: store the token in current ctx (Miroslav Bajtoš) + + * Fix `loopback.getCurrentContext` (Miroslav Bajtoš) + + * Update chai to ^1.10.0 (Miroslav Bajtoš) + + * package: fix deps (Miroslav Bajtoš) + + * Middleware phases - initial implementation (Miroslav Bajtoš) + + * Allows ACLs/settings in model config (Raymond Feng) + + * Remove context middleware per Ritchie (Rand McKinney) + + * Add API doc for context middleware - see #337 (crandmck) + + * Update persisted-model.js (Rand McKinney) + + * rest middleware: clean up context config (Miroslav Bajtoš) + + * Move `context` example to a standalone app (Miroslav Bajtoš) + + * Enable the context middleware from loopback.rest (Raymond Feng) + + * Add context propagation middleware (Raymond Feng) + + * Changes to JSdoc comments (Rand McKinney) + + * Reorder classes alphabetically in each section (Rand McKinney) + + * common: coding style cleanup (Miroslav Bajtoš) + + * Coding style cleanup (Gruntfile, lib) (Miroslav Bajtoš) + + * Enable jscs for `lib`, fix style violations (Rob Halff) + + * Add access-context.js to API doc (Rand McKinney) + + * Remove doc for debug function (Rand McKinney) + + * Update registry.js (Rand McKinney) + + * Fix the jsdoc for User.login (Raymond Feng) + + * Deleted instantiation of new Change model. This PR removes the instantiation of a new change model as models return from Change.find are already instances of Change. This solves the duplicate Id issue #649 (Berkeley Martinez) + + * Expose path to the built-in favicon file (Miroslav Bajtoš) + + * Add API docs for `loopback.static`. (Miroslav Bajtoš) + + * Add test for `remoting.rest.supportedTypes` (Miroslav Bajtoš) + + * Revert "rest handler options" (Miroslav Bajtoš) + + * REST handler options. (Guilherme Cirne) + + * The elapsed time in milliseconds can be 0 (less than 1 ms) (Raymond Feng) + + +2014-10-27, Version 2.7.0 +========================= + + * Bump version (Raymond Feng) + + * User: custom email headers in verify (Juan Pizarro) + + * Add realm support (Raymond Feng) + + * Make sure GET /:id/exists returns 200 {exists: true|false} https://github.com/strongloop/loopback/issues/679 (Raymond Feng) + + * Adjust id handling to deal with 0 and null (Chris S) + + * Force principalId to be a string. (Chris S) + + +2014-10-23, Version 2.6.0 +========================= + + * User: fix `confirm` permissions (Miroslav Bajtoš) + + * Use === to compare with 0 (Rob Halff) + + * add laxbreak option (Rob Halff) + + * use singlequotes (Rob Halff) + + * split jshint task for test & lib (Rob Halff) + + * allow comma first style and increase line length (Rob Halff) + + * add missing semicolons (Rob Halff) + + * Support per-model and per-handler remoting options (Fabien Franzen) + + * Fix JSdoc for registerResolver (Rand McKinney) + + * lib/application: improve URL building algo (Miroslav Bajtoš) + + * Fix findById callback signature (Rand McKinney) + + * JSdoc fixes (Rand McKinney) + + * Fix places using undefined variables (Miroslav Bajtoš) + + * Clean up jsdoc comments (crandmck) + + * models: move Change LDL def into a json file (Miroslav Bajtoš) + + * models: move Checkpoint LDL def into a json file (Miroslav Bajtoš) + + * models: move Role LDL def into a json file (Miroslav Bajtoš) + + * models: move RoleMapping def into its own files (Miroslav Bajtoš) + + * models: move ACL LDL def into a json file (Miroslav Bajtoš) + + * models: move Scope def into its own files (Miroslav Bajtoš) + + * models: move AccessToken LDL def into a json file (Miroslav Bajtoš) + + * models: move Application LDL def into a json file (Miroslav Bajtoš) + + * models: move Email LDL def into `email.json` (Miroslav Bajtoš) + + * models: move User LDL def into `user.json` (Miroslav Bajtoš) + + * test: run more tests in the browser (Miroslav Bajtoš) + + * test: verify exported models (Miroslav Bajtoš) + + * test: remove infinite timeout (Miroslav Bajtoš) + + * Auto-load and register built-in `Checkpoint` model (Miroslav Bajtoš) + + * Skip static ACL entries that don't match the property (Raymond Feng) + + * Dismantle `lib/models`. (Miroslav Bajtoš) + + * Register built-in models in a standalone file (Miroslav Bajtoš) + + +2014-10-10, Version 2.4.1 +========================= + + * models/change: fix `id` property definition (Miroslav Bajtoš) + + * Added class properties jsdoc. (Rand McKinney) + + * Fixed up JS Doc (Rand McKinney) + + * Update contribution guidelines (Ryan Graham) + + * Document ACL class properties (Rand McKinney) + + * Add properties JSdoc. (Rand McKinney) + + * Move looback remote connector to npm module (Krishna Raman) + + * Update strong-remoting version (Ritchie Martori) + + * Document user class properties (Ritchie Martori) + + * Add Model.disableRemoteMethod() (Ritchie Martori) + + +2014-09-12, Version 2.2.0 +========================= + + * Bump versions (Raymond Feng) + + * PersistedModel: add remote method aliases (Miroslav Bajtoš) + + * Fix last commit, which misplaced an ACL. Move the ACL inside "acls". Signed-off-by: Carey Richard Murphey (zxvv) + + * Add an ACL to User, to allow everyone to execute User.passwordReset(). (zxvv) + + * package: add "web" keyword (Miroslav Bajtoš) + + * Fix require (Fabien Franzen) + + * Fix coercion for remoting on vanilla models (Ritchie Martori) + + * user#login include server crash fix (Alexander Ryzhikov) + + * Update model.js (Rand McKinney) + + * Restrict: only hasManyThrough relation can have additional properties (Clark Wang) + + * Restrict that only hasManyThrough can have additional properties (Clark Wang) + + * Add tests for hasManyThrough link with data (Clark Wang) + + * Support data field as body for link operation (Clark Wang) + + * Tiny fix: correct url format (Fabien Franzen) + + * Fix embedsMany/findById to return proper 404 response (Fabien Franzen) + + * registry: warn when dataSource is not specified (Miroslav Bajtoš) + + * Only validate dataSource when defined (Fixes #482) (Ritchie Martori) + + * Fix tests (Fabien Franzen) + + * Enable remoting for embedsOne relation (Jaka Hudoklin) + + * Allow 'where' argument for scoped count API (Fabien Franzen) + + * Account for undefined before/afterListeners (Fabien Franzen) + + * added test and fixed changing passed in object within ctor (britztopher) + + * adding the ability to use single or multiple email transports in datasources.json file (britztopher) + + * added the ability to use an array of transports or just a single trnasport (britztopher) + + +2014-08-18, Version 2.1.3 +========================= + + * Bump version (Raymond Feng) + + * Make sure AccessToken extends from PersistedModel (Raymond Feng) + + * add count to relations and scopes (Jaka Hudoklin) + + * Remove `req.resume` from `app.enableAuth` (Miroslav Bajtoš) + + * Fix accessToken property docs (Ritchie Martori) + + * Make sure scoped methods are remoted (Raymond Feng) + + * Pass in remotingContext for ACL (Raymond Feng) + + * Fix reference to app (Raymond Feng) + + * Fix doc for the EXECUTE (Raymond Feng) + + * Don't assume relation.modelTo in case of polymorphic belongsTo (Fabien Franzen) + + * Fix "callbacl" by "callback" in doc (Steve Grosbois) + + * Inherit hooks when nesting (Fabien Franzen) + + * Changed options.path to options.http.path (Fabien Franzen) + + * filterMethod can also be a direct callback (Fabien Franzen) + + * filterMethod option (fn) to filter nested remote methods (Fabien Franzen) + + * Fix test to be more specific (Fabien Franzen) + + * Implement Model.nestRemoting (Fabien Franzen) + + * Allow custom relation path (http) - enable hasOne remoting access (Fabien Franzen) + + * Expose Model.exists over HTTP HEAD (Raymond Feng) + + * Return data source for app.dataSource() (Raymond Feng) + + * Fix typo in README (Ritchie Martori) + + * Integration test: referencesMany (Fabien Franzen) + + * Integration test: embedsMany (Fabien Franzen) + + * Fix jsdoc for remoteMethod() (Rand McKinney) + + * Map exists to HEAD for REST (Raymond Feng) + + * Fix https://github.com/strongloop/loopback/issues/413 (Raymond Feng) + + * Build the email verification url from app context (Raymond Feng) + + * Update test case to remove usage of deprecated express apis (Raymond Feng) + + * updated LB module diagram (altsang) + + * Update package.json (Al Tsang) + + * Updates for 2.0 (crandmck) + + * Update module diagram again (crandmck) + + * Update module diagram (crandmck) + + * Emit a 'modelRemoted' event by app.model() (Raymond Feng) + + * Fix remoting types for related models (Raymond Feng) + + * Fix for email transports (Raymond Feng) + + * Remove the link to obsolete wiki page to favor loopback.io (Raymond Feng) + + * Enhance the base model assertions (Raymond Feng) + + * Set up the base model based on the connector types (Raymond Feng) + + * express-middleware: improve error message (Miroslav Bajtoš) + + * Remove `app.docs()` (Miroslav Bajtoš) + + * Remove `loopback.compat.usePluralNamesForRemoting` (Miroslav Bajtoš) + + * Validate username uniqueness (Jaka Hudoklin) + + * Add descriptions for custom methods on user model (Raymond Feng) + + * Move remoting metadata from juggler to loopback (Raymond Feng) + + * Upgrade to nodemailer 1.0.1 (Raymond Feng) + + * 2.0.0-beta6 (Miroslav Bajtoš) + + * lib/application: publish Change models to REST API (Miroslav Bajtoš) + + * models/change: fix typo (Miroslav Bajtoš) + + * checkpoint: fix `current()` (Miroslav Bajtoš) + + * 2.0.0-beta5 (Miroslav Bajtoš) + + * 2.0.0-beta4 (Miroslav Bajtoš) + + * package: upgrade juggler to 2.0.0-beta2 (Miroslav Bajtoš) + + * Fix loopback in PhantomJS, fix karma tests (Miroslav Bajtoš) + + * Allow peer to use beta2 of datasource-juggler (and future) (Laurent) + + * Remove `app.boot` (Miroslav Bajtoš) + + * Fix remote method definition in client-server example (Ritchie Martori) + + * lib/registry: `getModel` throws, add `findModel` (Miroslav Bajtoš) + + * Remove loopback-explorer from dev deps (Miroslav Bajtoš) + + * Add loopback.version back (Miroslav Bajtoš) + + * registry: export DataSource class (Miroslav Bajtoš) + + * registry: fix non-unique default dataSources (Miroslav Bajtoš) + + * lib/registry fix jsdoc comments (Miroslav Bajtoš) + + * test: add debug logs (Miroslav Bajtoš) + + * refactor: extract runtime and registry (Miroslav Bajtoš) + + * Remove assertIsModel and isDataSource (Miroslav Bajtoš) + + * lib/loopback: fix jsdoc comments (Miroslav Bajtoš) + + * Add createModelFromConfig and configureModel() (Miroslav Bajtoš) + + * Rename DataModel to PersistedModel (Miroslav Bajtoš) + + * Make app.get/app.set available in browser (Miroslav Bajtoš) + + * Exclude express-middleware from browser bundle (Miroslav Bajtoš) + + * test: Remove forgotten call of `console.log()` (Miroslav Bajtoš) + + * Clean up express middleware dependencies (Raymond Feng) + + * Update strong-remoting dep (Raymond Feng) + + * Rename express-wrapper to express-middleware (Raymond Feng) + + * Clean up the tests (Raymond Feng) + + * Upgrade to Express 4.x (Raymond Feng) + + * Deprecate app.boot, remove app.installMiddleware (Miroslav Bajtoš) + + * 2.0.0-beta3 (Miroslav Bajtoš) + + * package.json: fix malformed json (Miroslav Bajtoš) + + * 2.0.0-beta2 (Ritchie Martori) + + * 2.0.0-beta1 (Ritchie Martori) + + * Add RC version (Ritchie Martori) + + * Depend on juggler@1.6.0 (Ritchie Martori) + + * !fixup Mark DAO methods as delegate (Ritchie Martori) + + * Ensure changes are created in sync (Ritchie Martori) + + * Remove un-rectify-able changes (Ritchie Martori) + + * Rework change conflict detection (Ritchie Martori) + + * - Use the RemoteObjects class to find remote objects instead of creating a cache - Use the SharedClass class to build the remote connector - Change default base model from Model to DataModel - Fix DataModel errors not logging correct method names - Use the strong-remoting 1.4 resolver API to resolve dynamic remote methods (relation api) - Remove use of fn object for storing remoting meta data (Ritchie Martori) + + * In progress: rework remoting meta-data (Ritchie Martori) + + * Add test for conflicts where both deleted (Ritchie Martori) + + * Rework replication test (Ritchie Martori) + + * bump juggler version (Ritchie Martori) + + * Change#getModel(), Doc cleanup, Conflict event (Ritchie Martori) + + * Add error logging for missing data (Ritchie Martori) + + * Fix issues when using MongoDB for replication (Ritchie Martori) + + * !fixup Test cleanup (Ritchie Martori) + + * Move replication implementation to DataModel (Ritchie Martori) + + * All tests passing (Ritchie Martori) + + * !fixup use DataModel instead of Model for all data based models (Ritchie Martori) + + * fixup! unskip failing tests (Ritchie Martori) + + * !fixup RemoteConnector tests (Ritchie Martori) + + * Add missing test/model file (Ritchie Martori) + + * Refactor DataModel remoting (Ritchie Martori) + + * !fixup .replicate() argument handling (Ritchie Martori) + + * Fixes for e2e replication / remote connector tests (Ritchie Martori) + + * Add replication e2e tests (Ritchie Martori) + + * fixup! Assert model exists (Ritchie Martori) + + * fixup! rename Change.track => rectifyModelChanges (Ritchie Martori) + + * Add model tests (Ritchie Martori) + + * Add replication example (Ritchie Martori) + + * Add Checkpoint model and Model replication methods (Ritchie Martori) + + * Add Change model (Ritchie Martori) + + +2014-08-12, Version 1.10.1 +========================== + + * Remove `req.resume` from `app.enableAuth` (Miroslav Bajtoš) + + +2014-08-08, Version 2.1.1 +========================= + + * Bump version (Raymond Feng) + + * Make sure scoped methods are remoted (Raymond Feng) + + * Pass in remotingContext for ACL (Raymond Feng) + + * Fix reference to app (Raymond Feng) + + * Don't assume relation.modelTo in case of polymorphic belongsTo (Fabien Franzen) + + +2014-08-07, Version 2.1.0 +========================= + + * Bump version (Raymond Feng) + + * Fix doc for the EXECUTE (Raymond Feng) + + * Fix "callbacl" by "callback" in doc (Steve Grosbois) + + * Inherit hooks when nesting (Fabien Franzen) + + * Changed options.path to options.http.path (Fabien Franzen) + + * filterMethod can also be a direct callback (Fabien Franzen) + + * filterMethod option (fn) to filter nested remote methods (Fabien Franzen) + + * Fix test to be more specific (Fabien Franzen) + + * Implement Model.nestRemoting (Fabien Franzen) + + * Allow custom relation path (http) - enable hasOne remoting access (Fabien Franzen) + + * Expose Model.exists over HTTP HEAD (Raymond Feng) + + * Return data source for app.dataSource() (Raymond Feng) + + * Fix typo in README (Ritchie Martori) + + * Integration test: referencesMany (Fabien Franzen) + + * Integration test: embedsMany (Fabien Franzen) + + * Fix jsdoc for remoteMethod() (Rand McKinney) + + * Map exists to HEAD for REST (Raymond Feng) + + * Build the email verification url from app context (Raymond Feng) + + +2014-07-27, Version 2.0.2 +========================= + + * Fix https://github.com/strongloop/loopback/issues/413 (Raymond Feng) + + * Update test case to remove usage of deprecated express apis (Raymond Feng) + + +2014-07-26, Version 2.0.1 +========================= + + * Bump version (Raymond Feng) + + * updated LB module diagram (altsang) + + * Update package.json (Al Tsang) + + * Updates for 2.0 (crandmck) + + * Update module diagram again (crandmck) + + * Update module diagram (crandmck) + + * Emit a 'modelRemoted' event by app.model() (Raymond Feng) + + * Fix remoting types for related models (Raymond Feng) + + * Fix for email transports (Raymond Feng) + + * Remove the link to obsolete wiki page to favor loopback.io (Raymond Feng) + + +2014-07-22, Version 2.0.0 +========================= + + * Enhance the base model assertions (Raymond Feng) + + * Report error for User.confirm() (Raymond Feng) + + * Set up the base model based on the connector types (Raymond Feng) + + * express-middleware: improve error message (Miroslav Bajtoš) + + * Remove `app.docs()` (Miroslav Bajtoš) + + * Remove `loopback.compat.usePluralNamesForRemoting` (Miroslav Bajtoš) + + * Validate username uniqueness (Jaka Hudoklin) + + * Add descriptions for custom methods on user model (Raymond Feng) + + * Move remoting metadata from juggler to loopback (Raymond Feng) + + * Upgrade to nodemailer 1.0.1 (Raymond Feng) + + * Enhance the error message (Raymond Feng) + + +2014-07-16, Version 2.0.0-beta7 +=============================== + + * Bump version (Raymond Feng) + + * 2.0.0-beta6 (Miroslav Bajtoš) + + * lib/application: publish Change models to REST API (Miroslav Bajtoš) + + * models/change: fix typo (Miroslav Bajtoš) + + * checkpoint: fix `current()` (Miroslav Bajtoš) + + * 2.0.0-beta5 (Miroslav Bajtoš) + + * 2.0.0-beta4 (Miroslav Bajtoš) + + * package: upgrade juggler to 2.0.0-beta2 (Miroslav Bajtoš) + + * Fix loopback in PhantomJS, fix karma tests (Miroslav Bajtoš) + + * Allow peer to use beta2 of datasource-juggler (and future) (Laurent) + + * Remove `app.boot` (Miroslav Bajtoš) + + * Fix remote method definition in client-server example (Ritchie Martori) + + * lib/registry: `getModel` throws, add `findModel` (Miroslav Bajtoš) + + * Remove loopback-explorer from dev deps (Miroslav Bajtoš) + + * Add loopback.version back (Miroslav Bajtoš) + + * registry: export DataSource class (Miroslav Bajtoš) + + * registry: fix non-unique default dataSources (Miroslav Bajtoš) + + * lib/registry fix jsdoc comments (Miroslav Bajtoš) + + * test: add debug logs (Miroslav Bajtoš) + + * refactor: extract runtime and registry (Miroslav Bajtoš) + + * Remove assertIsModel and isDataSource (Miroslav Bajtoš) + + * lib/loopback: fix jsdoc comments (Miroslav Bajtoš) + + * Add createModelFromConfig and configureModel() (Miroslav Bajtoš) + + * Rename DataModel to PersistedModel (Miroslav Bajtoš) + + * Make app.get/app.set available in browser (Miroslav Bajtoš) + + * Exclude express-middleware from browser bundle (Miroslav Bajtoš) + + * test: Remove forgotten call of `console.log()` (Miroslav Bajtoš) + + * Clean up express middleware dependencies (Raymond Feng) + + * Update strong-remoting dep (Raymond Feng) + + * Rename express-wrapper to express-middleware (Raymond Feng) + + * Clean up the tests (Raymond Feng) + + * Upgrade to Express 4.x (Raymond Feng) + + * Deprecate app.boot, remove app.installMiddleware (Miroslav Bajtoš) + + * 2.0.0-beta3 (Miroslav Bajtoš) + + * package.json: fix malformed json (Miroslav Bajtoš) + + * 2.0.0-beta2 (Ritchie Martori) + + * 2.0.0-beta1 (Ritchie Martori) + + * Add RC version (Ritchie Martori) + + * Depend on juggler@1.6.0 (Ritchie Martori) + + * !fixup Mark DAO methods as delegate (Ritchie Martori) + + * Ensure changes are created in sync (Ritchie Martori) + + * Remove un-rectify-able changes (Ritchie Martori) + + * Rework change conflict detection (Ritchie Martori) + + * - Use the RemoteObjects class to find remote objects instead of creating a cache - Use the SharedClass class to build the remote connector - Change default base model from Model to DataModel - Fix DataModel errors not logging correct method names - Use the strong-remoting 1.4 resolver API to resolve dynamic remote methods (relation api) - Remove use of fn object for storing remoting meta data (Ritchie Martori) + + * In progress: rework remoting meta-data (Ritchie Martori) + + * Add test for conflicts where both deleted (Ritchie Martori) + + * Rework replication test (Ritchie Martori) + + * bump juggler version (Ritchie Martori) + + * Change#getModel(), Doc cleanup, Conflict event (Ritchie Martori) + + * Add error logging for missing data (Ritchie Martori) + + * Fix issues when using MongoDB for replication (Ritchie Martori) + + * !fixup Test cleanup (Ritchie Martori) + + * Move replication implementation to DataModel (Ritchie Martori) + + * All tests passing (Ritchie Martori) + + * !fixup use DataModel instead of Model for all data based models (Ritchie Martori) + + * fixup! unskip failing tests (Ritchie Martori) + + * !fixup RemoteConnector tests (Ritchie Martori) + + * Add missing test/model file (Ritchie Martori) + + * Refactor DataModel remoting (Ritchie Martori) + + * !fixup .replicate() argument handling (Ritchie Martori) + + * Fixes for e2e replication / remote connector tests (Ritchie Martori) + + * Add replication e2e tests (Ritchie Martori) + + * fixup! Assert model exists (Ritchie Martori) + + * fixup! rename Change.track => rectifyModelChanges (Ritchie Martori) + + * Add model tests (Ritchie Martori) + + * Add replication example (Ritchie Martori) + + * Add Checkpoint model and Model replication methods (Ritchie Martori) + + * Add Change model (Ritchie Martori) + + +2014-07-16, Version 1.10.0 +========================== + + * Remove unused dep (Raymond Feng) + + * Bump version and update deps (Raymond Feng) + + * Upgrade to loopback-datasource-juggler@1.7.0 (Raymond Feng) + + * Refactor modelBuilder to registry and set up default model (Raymond Feng) + + * Add a test case for credentials/challenges (Raymond Feng) + + * Fix credentials/challenges types (Raymond Feng) + + * Update modules for examples (Raymond Feng) + + * Split out aliases for deleteById and destroyAll functions for jsdoc. (crandmck) + + * Remove unused deps (Raymond Feng) + + * Refactor email verification tests into a new group (Raymond Feng) + + * Fix the typo (Raymond Feng) + + * Add an option to honor emailVerified (Raymond Feng) + + * Update module list in README (Raymond Feng) + + * Refine the test cases for relation REST APIs (Raymond Feng) + + * test: add check of Model remote methods (Miroslav Bajtoš) + + * Adjust the REST mapping for add/remove (Raymond Feng) + + * Add a test case for hasMany through add/remove remoting (Raymond Feng) + + * Fix the typo and add Bearer token support (Raymond Feng) + + * Update README (Raymond Feng) + + * Fix misleading token middleware documentation (Aleksandr Tsertkov) + + +2014-07-15, Version 2.0.0-beta6 +=============================== + + * 2.0.0-beta6 (Miroslav Bajtoš) + + * lib/application: publish Change models to REST API (Miroslav Bajtoš) + + * models/change: fix typo (Miroslav Bajtoš) + + * checkpoint: fix `current()` (Miroslav Bajtoš) + + +2014-07-03, Version 2.0.0-beta5 +=============================== + + * 2.0.0-beta5 (Miroslav Bajtoš) + + * app: update `url` on `listening` event (Miroslav Bajtoš) + + * 2.0.0-beta4 (Miroslav Bajtoš) + + * package: upgrade juggler to 2.0.0-beta2 (Miroslav Bajtoš) + + * Fix loopback in PhantomJS, fix karma tests (Miroslav Bajtoš) + + * Allow peer to use beta2 of datasource-juggler (and future) (Laurent) + + * Remove `app.boot` (Miroslav Bajtoš) + + * Fix remote method definition in client-server example (Ritchie Martori) + + * lib/registry: `getModel` throws, add `findModel` (Miroslav Bajtoš) + + * Remove loopback-explorer from dev deps (Miroslav Bajtoš) + + * Add loopback.version back (Miroslav Bajtoš) + + * registry: export DataSource class (Miroslav Bajtoš) + + * registry: fix non-unique default dataSources (Miroslav Bajtoš) + + * lib/registry fix jsdoc comments (Miroslav Bajtoš) + + * test: add debug logs (Miroslav Bajtoš) + + * refactor: extract runtime and registry (Miroslav Bajtoš) + + * Remove assertIsModel and isDataSource (Miroslav Bajtoš) + + * lib/loopback: fix jsdoc comments (Miroslav Bajtoš) + + * Add createModelFromConfig and configureModel() (Miroslav Bajtoš) + + * Rename DataModel to PersistedModel (Miroslav Bajtoš) + + * Make app.get/app.set available in browser (Miroslav Bajtoš) + + * Exclude express-middleware from browser bundle (Miroslav Bajtoš) + + * test: Remove forgotten call of `console.log()` (Miroslav Bajtoš) + + * Clean up express middleware dependencies (Raymond Feng) + + * Update strong-remoting dep (Raymond Feng) + + * Rename express-wrapper to express-middleware (Raymond Feng) + + * Clean up the tests (Raymond Feng) + + * Upgrade to Express 4.x (Raymond Feng) + + * Deprecate app.boot, remove app.installMiddleware (Miroslav Bajtoš) + + * 2.0.0-beta3 (Miroslav Bajtoš) + + * package.json: fix malformed json (Miroslav Bajtoš) + + * 2.0.0-beta2 (Ritchie Martori) + + * 2.0.0-beta1 (Ritchie Martori) + + * Add RC version (Ritchie Martori) + + * Depend on juggler@1.6.0 (Ritchie Martori) + + * !fixup Mark DAO methods as delegate (Ritchie Martori) + + * Ensure changes are created in sync (Ritchie Martori) + + * Remove un-rectify-able changes (Ritchie Martori) + + * Rework change conflict detection (Ritchie Martori) + + * - Use the RemoteObjects class to find remote objects instead of creating a cache - Use the SharedClass class to build the remote connector - Change default base model from Model to DataModel - Fix DataModel errors not logging correct method names - Use the strong-remoting 1.4 resolver API to resolve dynamic remote methods (relation api) - Remove use of fn object for storing remoting meta data (Ritchie Martori) + + * In progress: rework remoting meta-data (Ritchie Martori) + + * Add test for conflicts where both deleted (Ritchie Martori) + + * Rework replication test (Ritchie Martori) + + * bump juggler version (Ritchie Martori) + + * Change#getModel(), Doc cleanup, Conflict event (Ritchie Martori) + + * Add error logging for missing data (Ritchie Martori) + + * Fix issues when using MongoDB for replication (Ritchie Martori) + + * !fixup Test cleanup (Ritchie Martori) + + * Move replication implementation to DataModel (Ritchie Martori) + + * All tests passing (Ritchie Martori) + + * !fixup use DataModel instead of Model for all data based models (Ritchie Martori) + + * fixup! unskip failing tests (Ritchie Martori) + + * !fixup RemoteConnector tests (Ritchie Martori) + + * Add missing test/model file (Ritchie Martori) + + * Refactor DataModel remoting (Ritchie Martori) + + * !fixup .replicate() argument handling (Ritchie Martori) + + * Fixes for e2e replication / remote connector tests (Ritchie Martori) + + * Add replication e2e tests (Ritchie Martori) + + * fixup! Assert model exists (Ritchie Martori) + + * fixup! rename Change.track => rectifyModelChanges (Ritchie Martori) + + * Add model tests (Ritchie Martori) + + * Add replication example (Ritchie Martori) + + * Add Checkpoint model and Model replication methods (Ritchie Martori) + + * Add Change model (Ritchie Martori) + + +2014-06-27, Version 1.9.1 +========================= + + * Fix "ReferenceError: loopback is not defined" in registry.memory(). (Guilherme Cirne) + + * Invalid Access Token return 401 (Karl Mikkelsen) + + * Bump version and update deps (Raymond Feng) + + * Update debug setting (Raymond Feng) + + * Mark `app.boot` as deprecated. (Miroslav Bajtoš) + + * Update link to doc (Rand McKinney) + + +2014-06-26, Version 2.0.0-beta4 +=============================== + + * 2.0.0-beta4 (Miroslav Bajtoš) + + * package: upgrade juggler to 2.0.0-beta2 (Miroslav Bajtoš) + + * Fix loopback in PhantomJS, fix karma tests (Miroslav Bajtoš) + + * Allow peer to use beta2 of datasource-juggler (and future) (Laurent) + + * Remove `app.boot` (Miroslav Bajtoš) + + * Fix remote method definition in client-server example (Ritchie Martori) + + * lib/registry: `getModel` throws, add `findModel` (Miroslav Bajtoš) + + * Remove loopback-explorer from dev deps (Miroslav Bajtoš) + + * Add loopback.version back (Miroslav Bajtoš) + + * registry: export DataSource class (Miroslav Bajtoš) + + * registry: fix non-unique default dataSources (Miroslav Bajtoš) + + * lib/registry fix jsdoc comments (Miroslav Bajtoš) + + * test: add debug logs (Miroslav Bajtoš) + + * refactor: extract runtime and registry (Miroslav Bajtoš) + + * Remove assertIsModel and isDataSource (Miroslav Bajtoš) + + * lib/loopback: fix jsdoc comments (Miroslav Bajtoš) + + * Add createModelFromConfig and configureModel() (Miroslav Bajtoš) + + * Rename DataModel to PersistedModel (Miroslav Bajtoš) + + * Make app.get/app.set available in browser (Miroslav Bajtoš) + + * Exclude express-middleware from browser bundle (Miroslav Bajtoš) + + * test: Remove forgotten call of `console.log()` (Miroslav Bajtoš) + + * Clean up express middleware dependencies (Raymond Feng) + + * Update strong-remoting dep (Raymond Feng) + + * Rename express-wrapper to express-middleware (Raymond Feng) + + * Clean up the tests (Raymond Feng) + + * Upgrade to Express 4.x (Raymond Feng) + + * Deprecate app.boot, remove app.installMiddleware (Miroslav Bajtoš) + + * 2.0.0-beta3 (Miroslav Bajtoš) + + * package.json: fix malformed json (Miroslav Bajtoš) + + * 2.0.0-beta2 (Ritchie Martori) + + * 2.0.0-beta1 (Ritchie Martori) + + * Add RC version (Ritchie Martori) + + * Depend on juggler@1.6.0 (Ritchie Martori) + + * !fixup Mark DAO methods as delegate (Ritchie Martori) + + * Ensure changes are created in sync (Ritchie Martori) + + * Remove un-rectify-able changes (Ritchie Martori) + + * Rework change conflict detection (Ritchie Martori) + + * - Use the RemoteObjects class to find remote objects instead of creating a cache - Use the SharedClass class to build the remote connector - Change default base model from Model to DataModel - Fix DataModel errors not logging correct method names - Use the strong-remoting 1.4 resolver API to resolve dynamic remote methods (relation api) - Remove use of fn object for storing remoting meta data (Ritchie Martori) + + * In progress: rework remoting meta-data (Ritchie Martori) + + * Add test for conflicts where both deleted (Ritchie Martori) + + * Rework replication test (Ritchie Martori) + + * bump juggler version (Ritchie Martori) + + * Change#getModel(), Doc cleanup, Conflict event (Ritchie Martori) + + * Add error logging for missing data (Ritchie Martori) + + * Fix issues when using MongoDB for replication (Ritchie Martori) + + * !fixup Test cleanup (Ritchie Martori) + + * Move replication implementation to DataModel (Ritchie Martori) + + * All tests passing (Ritchie Martori) + + * !fixup use DataModel instead of Model for all data based models (Ritchie Martori) + + * fixup! unskip failing tests (Ritchie Martori) + + * !fixup RemoteConnector tests (Ritchie Martori) + + * Add missing test/model file (Ritchie Martori) + + * Refactor DataModel remoting (Ritchie Martori) + + * !fixup .replicate() argument handling (Ritchie Martori) + + * Fixes for e2e replication / remote connector tests (Ritchie Martori) + + * Add replication e2e tests (Ritchie Martori) + + * fixup! Assert model exists (Ritchie Martori) + + * fixup! rename Change.track => rectifyModelChanges (Ritchie Martori) + + * Add model tests (Ritchie Martori) + + * Add replication example (Ritchie Martori) + + * Add Checkpoint model and Model replication methods (Ritchie Martori) + + * Add Change model (Ritchie Martori) + + +2014-06-25, Version 1.9.0 +========================= + + * Bump version and update deps (Raymond Feng) + + * Update debug setting (Raymond Feng) + + * Mark `app.boot` as deprecated. (Miroslav Bajtoš) + + * Update link to doc (Rand McKinney) + + * Update juggler dep (Raymond Feng) + + * Remove relationNameFor (Raymond Feng) + + * Fix a slowdown caused by mutation of an incoming accessToken option. (Samuel Reed) + + * package: the next version will be a minor version (Miroslav Bajtoš) + + * lib/application: Remove forgotten `loopback` ref (Miroslav Bajtoš) + + * Allow customization of ACL http status (Karl Mikkelsen) + + * Expose loopback as `app.loopback` (Miroslav Bajtoš) + + * registry: export DataSource class (Miroslav Bajtoš) + + * registry: fix non-unique default dataSources (Miroslav Bajtoš) + + * lib/registry fix jsdoc comments (Miroslav Bajtoš) + + * test: add debug logs (Miroslav Bajtoš) + + * refactor: extract runtime and registry (Miroslav Bajtoš) + + * Remove assertIsModel and isDataSource (Miroslav Bajtoš) + + * Add createModelFromConfig and configureModel() (Miroslav Bajtoš) + + * Make app.get/app.set available in browser (Miroslav Bajtoš) + + * package: upgrade Mocha to 1.20 (Miroslav Bajtoš) + + * test: fix ACL integration tests (Miroslav Bajtoš) + + * JSDoc fixes (crandmck) + + * Add a test case (Raymond Feng) + + * Set the role id to be generated (Raymond Feng) + + * Tidy up app.model() to remove duplicate & recusrive call (Raymond Feng) + + * Register existing model to app.models during app.model() (Raymond Feng) + + * JSDoc cleanup (crandmck) + + * Bump version so that we can republish (Raymond Feng) + + +2014-06-09, Version 1.8.6 +========================= + + * Bump version (Raymond Feng) + + * Use constructor to reference the model class (Raymond Feng) + + * Allow the creation of access token to be overriden (Raymond Feng) + + * Fixup JSDocs; note: updateOrCreate function alias pulled out on separate line for docs (crandmck) + + * Added middleware and API doc headings (crandmck) + + * Update JSDoc (crandmck) + + * Update docs.json (Rand McKinney) + + * Removed old .md files from API docs (Rand McKinney) + + * Delete api-model.md (Rand McKinney) + + * Delete api-datasource.md (Rand McKinney) + + * Delete api-geopoint.md (Rand McKinney) + + * Remove duplicate doc content (Rand McKinney) + + * Add note about unavailable args to remote hooks. (Rand McKinney) + + * Undo incorrect changes I made -- per Ritchie (Rand McKinney) + + * Update strong-remoting to 1.5 (Ritchie Martori) + + * Remove "user" as arg to beforeRemote(..) (Rand McKinney) + + * !fixup only set ctx.accessType when sharedMethod is available (Ritchie Martori) + + * Refactor ACL to allow for `methodNames` / aliases (Ritchie Martori) + + * Update README and the module diagram (Raymond Feng) + + +2014-05-28, Version 2.0.0-beta3 +=============================== + + * 2.0.0-beta3 (Miroslav Bajtoš) + + * package.json: fix malformed json (Miroslav Bajtoš) + + * app: implement `connector()` and `connectors` (Miroslav Bajtoš) + + * Fix a typo in `app.boot`. (Samuel Reed) + + * 2.0.0-beta2 (Ritchie Martori) + + * 2.0.0-beta1 (Ritchie Martori) + + * Make app.datasources unique per app instance (Miroslav Bajtoš) + + * Add RC version (Ritchie Martori) + + * Depend on juggler@1.6.0 (Ritchie Martori) + + * !fixup Mark DAO methods as delegate (Ritchie Martori) + + * Ensure changes are created in sync (Ritchie Martori) + + * Remove un-rectify-able changes (Ritchie Martori) + + * Rework change conflict detection (Ritchie Martori) + + * - Use the RemoteObjects class to find remote objects instead of creating a cache - Use the SharedClass class to build the remote connector - Change default base model from Model to DataModel - Fix DataModel errors not logging correct method names - Use the strong-remoting 1.4 resolver API to resolve dynamic remote methods (relation api) - Remove use of fn object for storing remoting meta data (Ritchie Martori) + + * In progress: rework remoting meta-data (Ritchie Martori) + + * Add test for conflicts where both deleted (Ritchie Martori) + + * Rework replication test (Ritchie Martori) + + * bump juggler version (Ritchie Martori) + + * Change#getModel(), Doc cleanup, Conflict event (Ritchie Martori) + + * Add error logging for missing data (Ritchie Martori) + + * Fix issues when using MongoDB for replication (Ritchie Martori) + + * !fixup Test cleanup (Ritchie Martori) + + * Move replication implementation to DataModel (Ritchie Martori) + + * All tests passing (Ritchie Martori) + + * !fixup use DataModel instead of Model for all data based models (Ritchie Martori) + + * fixup! unskip failing tests (Ritchie Martori) + + * !fixup RemoteConnector tests (Ritchie Martori) + + * Add missing test/model file (Ritchie Martori) + + * Refactor DataModel remoting (Ritchie Martori) + + * !fixup .replicate() argument handling (Ritchie Martori) + + * Fixes for e2e replication / remote connector tests (Ritchie Martori) + + * Add replication e2e tests (Ritchie Martori) + + * fixup! Assert model exists (Ritchie Martori) + + * fixup! rename Change.track => rectifyModelChanges (Ritchie Martori) + + * Add model tests (Ritchie Martori) + + * Add replication example (Ritchie Martori) + + * Add Checkpoint model and Model replication methods (Ritchie Martori) + + * Add Change model (Ritchie Martori) + + +2014-05-27, Version 1.8.5 +========================= + + * Bump version (Raymond Feng) + + * Add postgresql to the keywords (Raymond Feng) + + * updated package.json with SOAP and framework keywords (altsang) + + * updated package.json with keywords and updated description (Raymond Feng) + + +2014-05-27, Version 1.8.4 +========================= + + * Add more keywords (Raymond Feng) + + * Bump version (Raymond Feng) + + * app: flatten model config (Miroslav Bajtoš) + + * Fix the test for mocha 1.19.0 (Raymond Feng) + + * Update dependencies (Raymond Feng) + + * Added more keywords (Rand McKinney) + + * Update README and the module diagram (Raymond Feng) + + * added "REST API" keyword (Rand McKinney) + + * added 'web' and 'framework' keywords (Rand McKinney) + + * Make image URL absolute for npmjs.org. (Rand McKinney) + + * Use common syntax for juggler dep (Ritchie Martori) + + * Modify `loopback.rest` to include `loopback.token` (Miroslav Bajtoš) + + * Relax validation object test (Ritchie Martori) + + * Make juggler version a bit more strict to avoid pulling in breaking changes (Ritchie Martori) + + * Change module diagram to local png (Rand McKinney) + + * Add LoopBack modules diagram (crandmck) + + * Update README.md (sumitha) + + * Update README.md (Al Tsang) + + * added github prefix to path (altsang) + + * removed githalytics, added sl-beacon (altsang) + + * Update README and license link (Raymond Feng) + + * Add CLA (Raymond Feng) + + +2014-05-16, Version 1.8.2 +========================= + + * test/geo-point: relax too precise assertions (Miroslav Bajtoš) + + * Fix typo "Unkown" => "Unknown" (Adam Schwartz) + + * Support all 1.x versions of datasource-juggler (Miroslav Bajtoš) + + * Remove validation methods, now covered in JSDoc. (Rand McKinney) + + * Remove docs/api-geopoint.md from docs (Rand McKinney) + + * Removed docs/api-datasource.md (Rand McKinney) + + * Update README.md (Al Tsang) + + * Update README.md (Rand McKinney) + + * Move content from wiki on LB modules. (Rand McKinney) + + * Add homepage to package.json (Ritchie Martori) + + * Fix bug in User#resetPassword (haio) + + * Fix client-server example (Ritchie Martori) + + * Ensure roleId and principalId to be string in Role#isInRole (haio) + + * typo (haio) + + * Add more check on principalId (haio) + + * Convert principalId to String (haio) + + +2014-04-24, Version 1.8.1 +========================= + + * Bump version (Raymond Feng) + + * Fix constructor JSDoc (crandmck) + + * Remove intermediate section headers from nav (crandmck) + + * Rename the method so that it won't conflict with Model.checkAccess (Raymond Feng) + + * Fix/remove ctx.user documentation (Ritchie Martori) + + * Documentation cleanup (Ritchie Martori) + + * Fix save implementation for remoting connector (Ritchie Martori) + + * Add basic Remote connector e2e test (Ritchie Martori) + + * Bump juggler version (Ritchie Martori) + + * Add test for remoting nested hidden properties (Ritchie Martori) + + * Fix #229 (Whitespaces removed (Alex Pica) + + * Add nodemailer to browser ignores (Ritchie Martori) + + * Add an assertion to the returned store object (Raymond Feng) + + * Add an integration test for belongsTo remoting (Raymond Feng) + + * Depend on strong-remoting 1.3 (Ritchie Martori) + + * Support host / port in Remote connector (Ritchie Martori) + + * Throw useful errors in DataModel stub methods (Ritchie Martori) + + * Move proxy creation from remote connector into base model class (Ritchie Martori) + + * Remove reload method body (Ritchie Martori) + + * Add Remote connector (Ritchie Martori) + + * Initial client-server example (Ritchie Martori) + + +2014-04-04, Version 1.7.4 +========================= + + * Clean up JSDoc comments. Remove doc for deprecated installMiddleware function (crandmck) + + * Describe the "id" parameter of model's sharedCtor (Miroslav Bajtoš) + + * Update and cleanup JSDoc (crandmck) + + * Cleanup and update of jsdoc (crandmck) + + * Add link to loopback.io (Rand McKinney) + + * Update user.js (Doug Toppin) + + * Add hidden property documentation (Ritchie Martori) + + * test: add hasAndBelongsToMany integration test (Miroslav Bajtoš) + + * fix to enable ACL for confirm link sent by email (Doug Toppin) + + * Add hidden property support to models (Ritchie Martori) + + * Allow app.model() to accept a DataSource instance (Ritchie Martori) + + * Make verifications url safe (Ritchie Martori) + + * Try to fix org.pegdown.ParsingTimeoutException (Rand McKinney) + + * using base64 caused an occasional token string to contain '+' which resulted in a space being embedded in the token. 'hex' should always produce a url safe string for the token. (Doug Toppin) + + * Sending email was missing the from field (Doug Toppin) + + +2014-03-19, Version 1.7.2 +========================= + + * Bump version (Raymond Feng) + + * Add more comments (Raymond Feng) + + * Improve the ACL matching algorithm (Raymond Feng) + + +2014-03-18, Version 1.7.1 +========================= + + * Add test for request pausing during authentication (Miroslav Bajtoš) + + * Pause the req before checking access (Raymond Feng) + + * Remove the generated flag as the id is set by the before hook (Raymond Feng) + + * Improvements to JSDoc comments (crandmck) + + * Fixes to JSDoc for API docs (crandmck) + + * Remove oauth2 models as they will be packaged in a separate module (Raymond Feng) + + * Update api-model.md (Rand McKinney) + + * Minor doc fix (Ritchie Martori) + + * Set the correct status code for User.login (Raymond Feng) + + +2014-02-21, Version 1.7.0 +========================= + + * Bump version to 1.7.0 (Raymond Feng) + + * Update deps (Raymond Feng) + + * Bump version and update deps (Raymond Feng) + + * Rewrite test for clear handler cache. (Guilherme Cirne) + + * Allows options to be passed to strong-remoting (Raymond Feng) + + * Remove coercion from port check (Ritchie Martori) + + * The simplest possible solution for clearing the handler cache when registering a model. (Guilherme Cirne) + + * Remove outdated test readme (Ritchie Martori) + + * Remove unnecessary lines (Alberto Leal) + + * Update the license text (Raymond Feng) + + * Make sure User/AccessToken relations are set up by default (Raymond Feng) + + * Remove unused karma packages (Ritchie Martori) + + * Add karma for running browser tests (Ritchie Martori) + + * Dual license: MIT + StrongLoop (Raymond Feng) + + +2014-02-12, Version 1.6.2 +========================= + + * Bump version and update deps (Raymond Feng) + + * Documentation (generated) fix (Aurelien Chivot) + + * Use hex encoding for application ids/keys (Raymond Feng) + + * Add app.isAuthEnabled. (Miroslav Bajtoš) + + * Make app.models unique per app instance (Miroslav Bajtoš) + + * Fix incorrect usage of `app` in app.test.js (Miroslav Bajtoš) + + * Make sure the configured ACL submodel is used (Raymond Feng) + + +2014-01-30, Version 1.6.1 +========================= + + * Add `include=user` param to `User.login` (Miroslav Bajtoš) + + * Describe `access_token` param of `User.logout` (Miroslav Bajtoš) + + * Remove the generated flag for access token id (Raymond Feng) + + * Remove message prefix as debug will print it (Raymond Feng) + + * Add debug information for user.login (Raymond Feng) + + +2014-01-27, Version 1.6.0 +========================= + + * Update dependencies (Miroslav Bajtoš) + + * Add loopback.compat to simplify upgrade to 1.6 (Miroslav Bajtoš) + + * Register exported models using singular names (Miroslav Bajtoš) + + * User: use User.http.path (Miroslav Bajtoš) + + +2014-01-23, Version 1.5.3 +========================= + + * Bump version (Raymond Feng) + + * Add a test for autoAttach (Raymond Feng) + + * Fix the Role ref to RoleMapping (Raymond Feng) + + * Fix the Scope reference to models (Raymond Feng) + + * Lookup the email model (Raymond Feng) + + * Add lookback.getModelByType() and use it resolve model deps (Raymond Feng) + + * Fix user test race condition (Ritchie Martori) + + * Fix race condition where MyEmail model was not attached to the correct dataSource in tests (Ritchie Martori) + + * Fix the method args (Raymond Feng) + + * Fix the typo for the method name (Raymond Feng) + + * Small change to text webhook. (Rand McKinney) + + * Minor wording change for testing purposes. (Rand McKinney) + + * Fix capitalization and punctuation. (Rand McKinney) + + * Minor wording cleanup. (Rand McKinney) + + * Prevent autoAttach from overriding existing data source (Raymond Feng) + + +2014-01-17, Version 1.5.2 +========================= + + * Bump version (Raymond Feng) + + * Clean up loopback.js doc and add it to docs.json (Raymond Feng) + + * Fix the jsdoc for loopback.getModel() (Raymond Feng) + + * Make sure defaultPermission is checked (Raymond Feng) + + * Remove the dangling require (Raymond Feng) + + * Make ACL model subclassing friendly (Raymond Feng) + + * Fix heading levels in docs/ markdown files. (Miroslav Bajtoš) + + * Remove docs/rest.md (Miroslav Bajtoš) + + * Improve jsdox documentation of app object (Miroslav Bajtoš) + + * Make sure methods are called in the context of the calling class (Raymond Feng) + + * Start to move md to jsdoc (Ritchie Martori) + + +2014-01-14, Version 1.5.0 +========================= + + + +2014-01-14, Version 1.5.1 +========================= + + * Bump version (Raymond Feng) + + * Make sure methods are called in the context of the calling class (Raymond Feng) + + * Start to move md to jsdoc (Ritchie Martori) + + * Replace `on` with `once` in middleware examples (Miroslav Bajtoš) + + * Fix incorrect transports (Ritchie Martori) + + * Speed up tests accessing User.password (Miroslav Bajtoš) + + * Describe loopback.ValidationError in API docs. (Miroslav Bajtoš) + + * Implement app.installMiddleware (Miroslav Bajtoš) + + * Implement `app.listen` (Miroslav Bajtoš) + + * Provide sane default for email connector transports (Ritchie Martori) + + * Add an empty transportsIndex to the mail connector by default (Ritchie Martori) + + * Add missing assert in user model (Ritchie Martori) + + * docs: document remote method `description` (Miroslav Bajtoš) + + +2014-01-07, Version 1.4.2 +========================= + + * Bump version (Raymond Feng) + + * Add app.restApiRoot setting (Miroslav Bajtoš) + + * Fix links so they work on apidocs site. (Rand McKinney) + + * Add ValidationError to loopback exports. (Miroslav Bajtoš) + + * Add API docs to README (Ritchie Martori) + + * Fixed some broken links and added ACL example in createModel() (Rand McKinney) + + +2013-12-20, Version 1.4.1 +========================= + + * Explicitly depend on juggler@1.2.11 (Ritchie Martori) + + * Add e2e tests for relations (Ritchie Martori) + + * Fix destroyAll reference (Ritchie Martori) + + * Add reference documentation using sdocs (Ritchie) + + * Update README for application model (Raymond Feng) + + +2013-12-18, Version 1.4.0 +========================= + + * app.boot() now loads the "boot" directory (Ritchie Martori) + + * Clean up the test case (Raymond Feng) + + * Remove the default values for gateway/port (Raymond Feng) + + * Reformat the code using 2 space identation (Raymond Feng) + + * Allow cert/key data to be shared by push/feedback (Raymond Feng) + + * fixup - Include accessToken in user logout tests (Ritchie Martori) + + * Logout now automatically pulls the accessToken from the request (Ritchie Martori) + + * Fix tests depending on old behavior of default User ACLs (Ritchie Martori) + + * Add default user ACLs (Ritchie Martori) + + * Define schema for GCM push-notification settings (Miroslav Bajtoš) + + * Improve debug statements for access control (Ritchie Martori) + + +2013-12-13, Version 1.3.4 +========================= + + * Dont attempt access checking on models without a check access method (Ritchie Martori) + + * App config settings are now available from app.get() (Ritchie Martori) + + * Fix user not allowed to delete itself if user (Ritchie Martori) + + * Only look at cookies if they are available (Ritchie Martori) + + * Remove the empty comment and set default token (Raymond Feng) + + * Refactor to the code use wrapper classes (Raymond Feng) + + * Enhance getRoles() to support smart roles (Raymond Feng) + + * Fix the algorithm for Role.isInRole and ACL.checkAccess (Raymond Feng) + + * Various ACL fixes (Ritchie Martori) + + * Add user default ACLs (Ritchie Martori) + + * Allow requests without auth tokens (Ritchie Martori) + + * Fix base class not being actual base class (Ritchie Martori) + + * Fix the ACL resolution against rules by matching score (Raymond Feng) + + * Add access type checking (Ritchie Martori) + + * Add Model.requireToken for disabling token requirement (Ritchie Martori) + + * Add Model.requireToken, default swagger to false (Ritchie Martori) + + * Add password reset (Ritchie Martori) + + +2013-12-06, Version 1.3.3 +========================= + + * Bump version (Raymond Feng) + + +2013-12-06, Version show +======================== + + * Bump version (Raymond Feng) + + * Make loopback-datasource-juggler a peer dep (Raymond Feng) + + * Add blank line before list so it lays out properly. (Rand McKinney) + + * Fix list format and minor wording fix. (Rand McKinney) + + * Small fix to link text (Rand McKinney) + + * Minor formatting and wording fixes. (Rand McKinney) + + * docs: describe http mapping of arguments (Miroslav Bajtos) + + +2013-12-05, Version 1.3.2 +========================= + + * Bump version (Ritchie) + + * SLA-725 support PORT and HOST environment for PaaS support (Ritchie) + + +2013-12-04, Version 1.3.1 +========================= + + * Fix the test assertion as the error message is changed. (Raymond Feng) + + * Bump version (Raymond Feng) + + * Remove superfluous head1 (Rand McKinney) + + * Fixed some list formatting issues. (Rand McKinney) + + * Fix list formats to play well with wiki markdown macro. (Rand McKinney) + + * Minor reformatting. (Rand McKinney) + + * Sadly, HTML table format is unusable in documentation wiki. Revert to lame md format. (Rand McKinney) + + * Reformat table for /find operation arguments (Rand McKinney) + + * Deleted extra space that foiled bold formatting (Rand McKinney) + + * Changed h3's to bold text to avoid generating items in TOC (Rand McKinney) + + * Fixed erroneous heading level (Rand McKinney) + + * Use loopback.AccessToken as default (Ritchie Martori) + + * Fix missing assert (Ritchie Martori) + + * Minor formatting fixes to make it play well in wiki (Rand McKinney) + + * Initial auth implementation (Ritchie Martori) + + * added Google Analytics to README.md to test tracking (altsang) + + * Delete types.md (Rand McKinney) + + * Delete resources.md (Rand McKinney) + + * Delete quickstart.md (Rand McKinney) + + * Delete js.md (Rand McKinney) + + * Delete java.md (Rand McKinney) + + * Delete ios.md (Rand McKinney) + + * Delete gettingstarted.md (Rand McKinney) + + * Delete concepts.md (Rand McKinney) + + * Delete cli.md (Rand McKinney) + + * Delete bundled-models.md (Rand McKinney) + + * Delete apiexplorer.md (Rand McKinney) + + * Delete intro.md (Rand McKinney) + + * Add .jshintignore (Miroslav Bajtos) + + * Add test for findById returning 404 (Miroslav Bajtos) + + * Fix minor autoWiring bugs (Ritchie Martori) + + * Add unauthenticated role (Raymond Feng) + + * Add checkAccess for subject and token (Raymond Feng) + + * Start to support smart roles such as owner (Raymond Feng) + + * Add jshint configuration. (Miroslav Bajtos) + + * Update rest.md (Rand McKinney) + + * Update api.md (Rand McKinney) + + * Add status middleware (Ritchie Martori) + + * Auto attach all models created (Ritchie Martori) + + * Update docs.json (Rand McKinney) + + * Add loopback.urlNotFound() middleware. (Miroslav Bajtos) + + * Remove .attachTo() from tests (Ritchie Martori) + + * Create api-model-remote.md (Rand McKinney) + + * Create api-model.md (Rand McKinney) + + * Create api-geopoint.md (Rand McKinney) + + * Create api-datasource.md (Rand McKinney) + + * Create api-app.md (Rand McKinney) + + * Debugging odd defineFK behavior (Ritchie Martori) + + * Update the doc link (Raymond Feng) + + * Initial auto wiring for model dataSources (Ritchie Martori) + + * Add public flag checking (Ritchie Martori) + + +2013-11-18, Version 1.3.0 +========================= + + * Upgrade nodemailer (Ritchie Martori) + + * Bump minor version (Ritchie Martori) + + * Add LoopBack forum link (Raymond Feng) + + * Remove blanket (Raymond Feng) + + * Switch to modelBuilder (Raymond Feng) + + * Allow ACLs for methods/relations (Raymond Feng) + + * Allows LDL level ACLs (Raymond Feng) + + * Update dependencies (Raymond Feng) + + * Fix the permission resolution (Raymond Feng) + + * Simplify check permission (Raymond Feng) + + * Fix the permission check (Raymond Feng) + + * Add oauth2 related models (Raymond Feng) + + * Add a stub to register role resolvers (Raymond Feng) + + * Add tests for isInRole and getRoles (Raymond Feng) + + * Add constants and more tests (Raymond Feng) + + * Define the models/relations for ACL (Raymond Feng) + + * Start to build the ACL models (Raymond Feng) + + * Update acl/role models (Raymond Feng) + + * Update ACL model (Raymond Feng) + + * Update AccessToken and User relationship (Ritchie Martori) + + * Added AccessToken created property (Ritchie Martori) + + * Update session / token documentation (Ritchie Martori) + + * Add loopback.token() middleware (Ritchie Martori) + + * Rename Session => AccessToken (Ritchie) + + * Bump verison (Ritchie Martori) + + * Fix bundle model name casing (Ritchie Martori) + + * Remove old node versions from travis (Ritchie Martori) + + * Add explicit Strong-remoting dep version (Ritchie Martori) + + * Add travis (Ritchie Martori) + + * Bump version (Ritchie Martori) + + * Update "hasMany" example (Ritchie Martori) + + * Code review fixes based on feedback from https://github.com/strongloop/loopback/pull/57 (Ritchie Martori) + + * Automatically convert strings to connectors if they are LoopBack connectors (Ritchie Martori) + + * Update api.md (Rand McKinney) + + * Update docs.json (Rand McKinney) + + * Create types.md (Rand McKinney) + + * Create bundled-models.md (Rand McKinney) + + * Update java.md (Rand McKinney) + + * Add app.dataSource() method (Ritchie) + + * Add app.boot() (Ritchie Martori) + + * README updates (Ritchie Martori) + + * Remove the proxy as it is now handled by the juggler (Raymond Feng) + + * Add MySQL connector (Raymond Feng) + + * Add belongsTo and hasAndBelongsToMany (Raymond Feng) + + * Clean up the model (Raymond Feng) + + * Added link to Doxygen API docs. (Rand McKinney) + + * Refactor email model into mail connector (Ritchie Martori) + + * Update Application model for the push notification (Raymond Feng) + + * Fix missing assert module (Ritchie Martori) + + * Fix the test as DAO now ignores undefined value for query (Raymond Feng) + + * Simplified LB architecture diagram (crandmck) + + * reorg and rewriting of first part of LoopBack guide, with new diagram (crandmck) + + * Fix the id and property access (Raymond Feng) + + * Update remote method example (Ritchie) + + * Update intro.md (Rand McKinney) + + * Update rest.md (Rand McKinney) + + * more cleanup (altsang) + + * remove >>>>>> from bad merge (altsang) + + * merged in Schoons' changes to mobile clients section (altsang) + + * revised per Ritchie's comments (altsang) + + * Revise Mobile Clients copy. (Michael Schoonmaker) + + * added Matt S' section on Mobile Clients (altsang) + + * filled out Big Picture (altsang) + + * revised shell -> node.js api (altsang) + + * Fix the preposition (Raymond Feng) + + * One more fix based on the comment (Raymond Feng) + + * Drop sure (Raymond Feng) + + * Add the missing article (Raymond Feng) + + * Add section for api explorer to docs (Raymond Feng) + + * added apiexplorer placeholder and big picture (altsang) + + * removed 'the' before StrongLoop Suite (altsang) + + * Remove redundant version in docs and testing docs webhook (Ritchie Martori) + + * removed version text (altsang) + + * Add keywords to package.json (Raymond Feng) + + * Add repo (Raymond Feng) + + * Finalize package.json for sls-1.0.0 (Raymond Feng) + + * Update docs for api->project rename. (Michael Schoonmaker) + + +2013-09-12, Version strongloopsuite-1.0.0-5 +=========================================== + + + +2013-09-12, Version strongloopsuite-1.0.0-4 +=========================================== + + * Update docs for api->project rename. (Michael Schoonmaker) + + * Use a pure JS bcrypt (Ritchie) + + * Added little boxes to Getting Started. (Michael Schoonmaker) + + +2013-09-11, Version strongloopsuite-1.0.0-3 +=========================================== + + * Add keywords to package.json (Raymond Feng) + + +2013-09-10, Version strongloopsuite-1.0.0-2 +=========================================== + + * Add repo (Raymond Feng) + + * Finalize package.json for sls-1.0.0 (Raymond Feng) + + * Changed tag to strongloopsuite-1.0.0-2 (cgole) + + +2013-09-09, Version strongloopsuite-1.0.0-1 +=========================================== + + * Update assets mapping (Raymond Feng) + + * Update concepts doc with a new diagram (Raymond Feng) + + * Simplify readme (Ritchie Martori) + + * Add placeholders for client apis (Ritchie Martori) + + * Add command line docs (Ritchie Martori) + + +2013-09-05, Version strongloopsuite-1.0.0-0 +=========================================== + + * Updated to use tagged version strongloopsuite-1.0.0-0 of dependencies (cgole) + + * Add getting started link (Ritchie Martori) + + * Update the Quick Start (Ritchie Martori) + + * Update model docs further. (Michael Schoonmaker) + + * Updated model docs (Ritchie Martori) + + * Concepts overhaul in progress (Ritchie Martori) + + +2013-09-04, Version 1.2.0 +========================= + + * Updated to use tagged version of loopback-datasource-juggler and strong-remoting (cgole) + + * Fix package.json to remove duplicate mocha deps (Raymond Feng) + + * Tidy up package.json for LoopBack 1.0.0 (Raymond Feng) + + * Update license (Raymond Feng) + + * Update the rest doc with more samples, fix the curl encoding (Raymond Feng) + + * Remove the todos example and fix doc example (Ritchie Martori) + + * doc/concepts: fixed link to strong-remoting docs (Miroslav Bajtos) + + * Update the internal prefix (Raymond Feng) + + * Update findOne (Raymond Feng) + + * Update REST doc based on the PR feedback (Raymond Feng) + + * Update REST doc (Raymond Feng) + + * Update the docs to fix into width of 80 (Raymond Feng) + + * intro edits and TOC adjustments (Al Tsang) + + * Fix the test case (Raymond Feng) + + +2013-08-27, Version 0.2.1 +========================= + + * Doc edits (Ritchie Martori) + + * Update concepts (Raymond Feng) + + * Add more description about the filter arg for find() (Raymond Feng) + + * Update the concepts.md and link to related guides (Raymond Feng) + + * Update rest.md (Raymond Feng) + + * Add quickstart (Ritchie Martori) + + * Start to add rest.md (Raymond Feng) + + * adjusting concept headers, cleaning up intro, more instructions on getting started (Al Tsang) + + * Use findById to look up the instance by id (Raymond Feng) + + * Update the list of shared methods (Raymond Feng) + + * Make sure User.setup calls Model.setup to support shared ctor (Raymond Feng) + + * Add LICENSE (Raymond Feng) + + * Added code coverage blanket.js (cgole) + + * took google docs TOC and put into sdocs (Al Tsang) + + * Added placeholder docs (Ritchie Martori) + + * Use strong-task-emitter (Raymond Feng) + + * Rename 'loopback-data' to 'loopback-datasource-juggler' (Raymond Feng) + + * Fix login query (Ritchie Martori) + + * Implement required and update invlaid id schemas (Ritchie Martori) + + * Remove auth middleware and passport until adding in acl and strategies (Ritchie Martori) + + * Clean up log out methods (Ritchie Martori) + + * Swagger integration (Ritchie) + + * Fix hasMany / relational methods. Update docs. (Ritchie) + + * Add root true to remote methods (Ritchie) + + * Fix bad connector path (Ritchie) + + * Fix the test case (Raymond Feng) + + * Rename adapter to connector (Raymond Feng) + + * Add more docs and apis to application model (Raymond Feng) + + * Add a deleteById test (Raymond Feng) + + * Rename sl-remoting to strong-remoting (Ritchie Martori) + + * Add more functions and tests for Application model (Raymond Feng) + + * More readme cleanup (Ritchie) + + * README cleanup (Ritchie) + + * Fix renaming manually (Ritchie) + + * Manually merge application (Ritchie) + + * Manually merge rest adapter (Ritchie) + + * Add fields documentation (Ritchie) + + * More cleanup for test/README.md (Ritchie Martori) + + * Cleanup test markdown (Ritchie Martori) + + * Add memory docs and test (Ritchie Martori) + + * Remove remote option object (Ritchie Martori) + + * Rename jugglingdb to loopback-data (Raymond Feng) + + * Add renamed files (Raymond Feng) + + * rename asteroid to loopback (Raymond Feng) + + * Fix model remoting issue. (Ritchie Martori) + + * Fix inheritance bug (Ritchie Martori) + + * Remove updateAttribute as remote method (Ritchie Martori) + + * Fix login bug. (Ritchie Martori) + + * Added bcrypt for password hashing (Ritchie Martori) + + * Refactor Model into class. Make createModel() just sugar. (Ritchie Martori) + + * Remove data argument name from user tests (Ritchie Martori) + + * Validate uniqueness and format of User email. (Ritchie Martori) + + * Add user.logout() sugar method and update logout docs (Ritchie Martori) + + * Create 64 byte session ids (Ritchie Martori) + + * Tests README (Ritchie Martori) + + * Experiment application model (Raymond Feng) + + * Updated generated test docs (Ritchie Martori) + + * Update docs and add asteroid.memory() sugar api (Ritchie Martori) + + * Add exports to models (Raymond Feng) + + * Updating models (Raymond Feng) + + * Add basic email verification (Ritchie Martori) + + * Initial users (Ritchie Martori) + + * Add default user properties (Ritchie Martori) + + * Add initial User model (Ritchie Martori) + + * Remove app.modelBuilder() (Ritchie Martori) + + * Add more user model docs (Ritchie Martori) + + * Update README.md (cgole) + + * Fix type in docs (Ritchie Martori) + + * Add normalized properties to Models (Ritchie Martori) + + * Add schema skeletons for built-in models (Raymond Feng) + + * Fix service() & services() (Raymond Feng) + + * Add service method (Ritchie Martori) + + * Add more info to the models (Raymond Feng) + + * Add more information to the logical models (Raymond Feng) + + * Only build a sl remoting handler when a model is added to the app. (Ritchie Martori) + + * Add user model docs. (Ritchie Martori) + + * Bump version (Ritchie Martori) + + * Add geo point tests (Ritchie Martori) + + * Rename long to lng (Ritchie Martori) + + * Add geo point (Ritchie Martori) + + * model.find => model.findById, model.all => model.find (Ritchie Martori) + + +2013-06-24, Version 0.8.0 +========================= + + * First release! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b19f22ba..f9782513 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ Contributing to `loopback` is easy. In a few simple steps: * Adhere to code style outlined in the [Google C++ Style Guide][] and [Google Javascript Style Guide][]. - * Sign the [Contributor License Agreement](https://cla.strongloop.com/strongloop/loopback) + * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/loopback) * Submit a pull request through Github. diff --git a/Gruntfile.js b/Gruntfile.js index 26e4feb7..a9804e59 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -33,9 +33,23 @@ module.exports = function(grunt) { lib: { src: ['lib/**/*.js'] }, - test: { - src: ['test/**/*.js'] + common: { + src: ['common/**/*.js'] + }, + server: { + src: ['server/**/*.js'] } + // TODO tests don't pass yet + // test: { + // src: ['test/**/*.js'] + // } + }, + jscs: { + gruntfile: 'Gruntfile.js', + lib: ['lib/**/*.js'], + common: ['common/**/*.js'], + server: ['server/**/*.js'] + // TODO(bajtos) - test/**/*.js }, watch: { gruntfile: { @@ -80,7 +94,7 @@ module.exports = function(grunt) { karma: { 'unit-once': { configFile: 'test/karma.conf.js', - browsers: [ 'PhantomJS' ], + browsers: ['PhantomJS'], singleRun: true, reporters: ['dots', 'junit'], @@ -182,6 +196,7 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-jscs'); grunt.loadNpmTasks('grunt-karma'); grunt.registerTask('e2e-server', function() { @@ -196,6 +211,8 @@ module.exports = function(grunt) { grunt.registerTask('default', ['browserify']); grunt.registerTask('test', [ + 'jscs', + 'jshint', process.env.JENKINS_HOME ? 'mochaTest:unit-xml' : 'mochaTest:unit', 'karma:unit-once']); diff --git a/common/models/access-token.js b/common/models/access-token.js index 03185009..4e01a33d 100644 --- a/common/models/access-token.js +++ b/common/models/access-token.js @@ -2,10 +2,10 @@ * Module Dependencies. */ -var loopback = require('../../lib/loopback') - , assert = require('assert') - , uid = require('uid2') - , DEFAULT_TOKEN_LEN = 64; +var loopback = require('../../lib/loopback'); +var assert = require('assert'); +var uid = require('uid2'); +var DEFAULT_TOKEN_LEN = 64; /** * Token based authentication and access control. @@ -57,7 +57,7 @@ module.exports = function(AccessToken) { fn(null, guid); } }); - } + }; /*! * Hook to create accessToken id. @@ -75,7 +75,7 @@ module.exports = function(AccessToken) { next(); } }); - } + }; /** * Find a token for the given `ServerRequest`. @@ -88,6 +88,11 @@ module.exports = function(AccessToken) { */ AccessToken.findForRequest = function(req, options, cb) { + if (cb === undefined && typeof options === 'function') { + cb = options; + options = {}; + } + var id = tokenIdForRequest(req, options); if (id) { @@ -115,7 +120,7 @@ module.exports = function(AccessToken) { cb(); }); } - } + }; /** * Validate the token. @@ -151,7 +156,7 @@ module.exports = function(AccessToken) { } catch (e) { cb(e); } - } + }; function tokenIdForRequest(req, options) { var params = options.params || []; diff --git a/common/models/acl.js b/common/models/acl.js index 1bb81407..b10fa1c1 100644 --- a/common/models/acl.js +++ b/common/models/acl.js @@ -179,7 +179,7 @@ module.exports = function(ACL) { ACL.prototype.score = function(req) { return this.constructor.getMatchingScore(this, req); - } + }; /*! * Resolve permission from the ACLs @@ -199,24 +199,26 @@ module.exports = function(ACL) { var score = 0; for (var i = 0; i < acls.length; i++) { - score = ACL.getMatchingScore(acls[i], req); + var candidate = acls[i]; + score = ACL.getMatchingScore(candidate, req); if (score < 0) { // the highest scored ACL did not match break; } if (!req.isWildcard()) { // We should stop from the first match for non-wildcard - permission = acls[i].permission; + permission = candidate.permission; break; } else { - if (req.exactlyMatches(acls[i])) { - permission = acls[i].permission; + if (req.exactlyMatches(candidate)) { + permission = candidate.permission; break; } // For wildcard match, find the strongest permission - if (AccessContext.permissionOrder[acls[i].permission] - > AccessContext.permissionOrder[permission]) { - permission = acls[i].permission; + var candidateOrder = AccessContext.permissionOrder[candidate.permission]; + var permissionOrder = AccessContext.permissionOrder[permission]; + if (candidateOrder > permissionOrder) { + permission = candidate.permission; } } } @@ -246,8 +248,7 @@ module.exports = function(ACL) { var staticACLs = []; if (modelClass && modelClass.settings.acls) { modelClass.settings.acls.forEach(function(acl) { - if (!acl.property || acl.property === ACL.ALL - || property === acl.property) { + if (!acl.property || acl.property === ACL.ALL || property === acl.property) { staticACLs.push(new ACL({ model: model, property: acl.property || ACL.ALL, @@ -259,11 +260,15 @@ module.exports = function(ACL) { } }); } - var prop = modelClass && - (modelClass.definition.properties[property] // regular property - || (modelClass._scopeMeta && modelClass._scopeMeta[property]) // relation/scope - || modelClass[property] // static method - || modelClass.prototype[property]); // prototype method + var prop = modelClass && ( + // regular property + modelClass.definition.properties[property] || + // relation/scope + (modelClass._scopeMeta && modelClass._scopeMeta[property]) || + // static method + modelClass[property] || + // prototype method + modelClass.prototype[property]); if (prop && prop.acls) { prop.acls.forEach(function(acl) { staticACLs.push(new ACL({ @@ -311,7 +316,7 @@ module.exports = function(ACL) { debug('Permission denied by statically resolved permission'); debug(' Resolved Permission: %j', resolved); process.nextTick(function() { - callback && callback(null, resolved); + if (callback) callback(null, resolved); }); return; } @@ -321,7 +326,7 @@ module.exports = function(ACL) { model: model, property: propertyQuery, accessType: accessTypeQuery}}, function(err, dynACLs) { if (err) { - callback && callback(err); + if (callback) callback(err); return; } acls = acls.concat(dynACLs); @@ -330,7 +335,7 @@ module.exports = function(ACL) { var modelClass = loopback.findModel(model); resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW; } - callback && callback(null, resolved); + if (callback) callback(null, resolved); }); }; @@ -344,7 +349,7 @@ module.exports = function(ACL) { debug('accessType %s', this.accessType); debug('permission %s', this.permission); } - } + }; /** * Check if the request has the permission to access. @@ -381,7 +386,7 @@ module.exports = function(ACL) { this.find({where: {model: model.modelName, property: propertyQuery, accessType: accessTypeQuery}}, function(err, acls) { if (err) { - callback && callback(err); + if (callback) callback(err); return; } var inRoleTasks = []; @@ -392,8 +397,9 @@ module.exports = function(ACL) { // Check exact matches for (var i = 0; i < context.principals.length; i++) { var p = context.principals[i]; - if (p.type === acl.principalType - && String(p.id) === String(acl.principalId)) { + var typeMatch = p.type === acl.principalType; + var idMatch = String(p.id) === String(acl.principalId); + if (typeMatch && idMatch) { effectiveACLs.push(acl); return; } @@ -415,7 +421,7 @@ module.exports = function(ACL) { async.parallel(inRoleTasks, function(err, results) { if (err) { - callback && callback(err, null); + if (callback) callback(err, null); return; } var resolved = self.resolvePermission(effectiveACLs, req); @@ -424,7 +430,7 @@ module.exports = function(ACL) { } debug('---Resolved---'); resolved.debug(); - callback && callback(null, resolved); + if (callback) callback(null, resolved); }); }); }; @@ -452,11 +458,10 @@ module.exports = function(ACL) { this.checkAccessForContext(context, function(err, access) { if (err) { - callback && callback(err); + if (callback) callback(err); return; } - callback && callback(null, access.permission !== ACL.DENY); + if (callback) callback(null, access.permission !== ACL.DENY); }); }; - -} +}; diff --git a/common/models/application.js b/common/models/application.js index b466de51..1f6148ff 100644 --- a/common/models/application.js +++ b/common/models/application.js @@ -141,7 +141,7 @@ module.exports = function(Application) { Application.resetKeys = function(appId, cb) { this.findById(appId, function(err, app) { if (err) { - cb && cb(err, app); + if (cb) cb(err, app); return; } app.resetKeys(cb); @@ -166,7 +166,7 @@ module.exports = function(Application) { Application.authenticate = function(appId, key, cb) { this.findById(appId, function(err, app) { if (err || !app) { - cb && cb(err, null); + if (cb) cb(err, null); return; } var result = null; @@ -180,7 +180,7 @@ module.exports = function(Application) { break; } } - cb && cb(null, result); + if (cb) cb(null, result); }); }; }; diff --git a/common/models/change.js b/common/models/change.js index b83eade7..d4aa33b4 100644 --- a/common/models/change.js +++ b/common/models/change.js @@ -2,14 +2,13 @@ * Module Dependencies. */ -var PersistedModel = require('../../lib/loopback').PersistedModel - , loopback = require('../../lib/loopback') - , crypto = require('crypto') - , CJSON = {stringify: require('canonical-json')} - , async = require('async') - , assert = require('assert') - , debug = require('debug')('loopback:change'); - +var PersistedModel = require('../../lib/loopback').PersistedModel; +var loopback = require('../../lib/loopback'); +var crypto = require('crypto'); +var CJSON = {stringify: require('canonical-json')}; +var async = require('async'); +var assert = require('assert'); +var debug = require('debug')('loopback:change'); /** * Change list entry. @@ -55,8 +54,8 @@ module.exports = function(Change) { if (!hasModel) return null; return Change.idForModel(this.modelName, this.modelId); - } - } + }; + }; Change.setup(); /** @@ -82,7 +81,7 @@ module.exports = function(Change) { }); }); async.parallel(tasks, callback); - } + }; /** * Get an identifier for a given model. @@ -94,7 +93,7 @@ module.exports = function(Change) { Change.idForModel = function(modelName, modelId) { return this.hash([modelName, modelId].join('-')); - } + }; /** * Find or create a change for the given model. @@ -126,7 +125,7 @@ module.exports = function(Change) { ch.save(callback); } }); - } + }; /** * Update (or create) the change with the current revision. @@ -148,7 +147,7 @@ module.exports = function(Change) { cb = cb || function(err) { if (err) throw new Error(err); - } + }; async.parallel(tasks, function(err) { if (err) return cb(err); @@ -194,7 +193,7 @@ module.exports = function(Change) { cb(); }); } - } + }; /** * Get a change's current revision based on current data. @@ -214,7 +213,7 @@ module.exports = function(Change) { cb(null, null); } }); - } + }; /** * Create a hash of the given `string` with the `options.hashAlgorithm`. @@ -229,7 +228,7 @@ module.exports = function(Change) { .createHash(Change.settings.hashAlgorithm || 'sha1') .update(str) .digest('hex'); - } + }; /** * Get the revision string for the given object @@ -239,7 +238,7 @@ module.exports = function(Change) { Change.revisionForInst = function(inst) { return this.hash(CJSON.stringify(inst)); - } + }; /** * Get a change's type. Returns one of: @@ -263,7 +262,7 @@ module.exports = function(Change) { return Change.DELETE; } return Change.UNKNOWN; - } + }; /** * Compare two changes. @@ -276,7 +275,7 @@ module.exports = function(Change) { var thisRev = this.rev || null; var thatRev = change.rev || null; return thisRev === thatRev; - } + }; /** * Does this change conflict with the given change. @@ -290,7 +289,7 @@ module.exports = function(Change) { if (Change.bothDeleted(this, change)) return false; if (this.isBasedOn(change)) return false; return true; - } + }; /** * Are both changes deletes? @@ -300,9 +299,9 @@ module.exports = function(Change) { */ Change.bothDeleted = function(a, b) { - return a.type() === Change.DELETE - && b.type() === Change.DELETE; - } + return a.type() === Change.DELETE && + b.type() === Change.DELETE; + }; /** * Determine if the change is based on the given change. @@ -312,7 +311,7 @@ module.exports = function(Change) { Change.prototype.isBasedOn = function(change) { return this.prev === change.rev; - } + }; /** * Determine the differences for a given model since a given checkpoint. @@ -393,11 +392,11 @@ module.exports = function(Change) { conflicts: conflicts }); }); - } + }; /** * Correct all change list entries. - * @param {Function} callback + * @param {Function} cb */ Change.rectifyAll = function(cb) { @@ -407,11 +406,10 @@ module.exports = function(Change) { this.find(function(err, changes) { if (err) return cb(err); changes.forEach(function(change) { - change = new Change(change); change.rectify(); }); }); - } + }; /** * Get the checkpoint model. @@ -426,13 +424,13 @@ module.exports = function(Change) { + ' is not attached to a dataSource'); checkpointModel.attachTo(this.dataSource); return checkpointModel; - } + }; Change.handleError = function(err) { if (!this.settings.ignoreErrors) { throw err; } - } + }; Change.prototype.debug = function() { if (debug.enabled) { @@ -445,7 +443,7 @@ module.exports = function(Change) { debug('\tmodelId', this.modelId); debug('\ttype', this.type()); } - } + }; /** * Get the `Model` class for `change.modelName`. @@ -454,7 +452,7 @@ module.exports = function(Change) { Change.prototype.getModelCtor = function() { return this.constructor.settings.trackModel; - } + }; Change.prototype.getModelId = function() { // TODO(ritch) get rid of the need to create an instance @@ -463,13 +461,13 @@ module.exports = function(Change) { var m = new Model(); m.setId(id); return m.getId(); - } + }; Change.prototype.getModel = function(callback) { var Model = this.constructor.settings.trackModel; var id = this.getModelId(); Model.findById(id, callback); - } + }; /** * When two changes conflict a conflict is created. @@ -533,7 +531,7 @@ module.exports = function(Change) { if (err) return cb(err); cb(null, source, target); } - } + }; /** * Get the conflicting changes. @@ -578,7 +576,7 @@ module.exports = function(Change) { if (err) return cb(err); cb(null, sourceChange, targetChange); } - } + }; /** * Resolve the conflict. @@ -594,7 +592,7 @@ module.exports = function(Change) { sourceChange.prev = targetChange.rev; sourceChange.save(cb); }); - } + }; /** * Determine the conflict type. @@ -624,5 +622,5 @@ module.exports = function(Change) { } return cb(null, Change.UNKNOWN); }); - } + }; }; diff --git a/common/models/checkpoint.js b/common/models/checkpoint.js index ed57de53..2bba736a 100644 --- a/common/models/checkpoint.js +++ b/common/models/checkpoint.js @@ -47,7 +47,7 @@ module.exports = function(Checkpoint) { }); } }); - } + }; Checkpoint.beforeSave = function(next, model) { if (!model.getId() && model.seq === undefined) { @@ -59,5 +59,5 @@ module.exports = function(Checkpoint) { } else { next(); } - } + }; }; diff --git a/common/models/role-mapping.js b/common/models/role-mapping.js index a6bfc4e7..3e87e563 100644 --- a/common/models/role-mapping.js +++ b/common/models/role-mapping.js @@ -23,14 +23,14 @@ module.exports = function(RoleMapping) { * @param {Error} err * @param {Application} application */ - RoleMapping.prototype.application = function (callback) { + RoleMapping.prototype.application = function(callback) { if (this.principalType === RoleMapping.APPLICATION) { - var applicationModel = this.constructor.Application - || loopback.getModelByType(loopback.Application); + var applicationModel = this.constructor.Application || + loopback.getModelByType(loopback.Application); applicationModel.findById(this.principalId, callback); } else { - process.nextTick(function () { - callback && callback(null, null); + process.nextTick(function() { + if (callback) callback(null, null); }); } }; @@ -41,14 +41,14 @@ module.exports = function(RoleMapping) { * @param {Error} err * @param {User} user */ - RoleMapping.prototype.user = function (callback) { + RoleMapping.prototype.user = function(callback) { if (this.principalType === RoleMapping.USER) { - var userModel = this.constructor.User - || loopback.getModelByType(loopback.User); + var userModel = this.constructor.User || + loopback.getModelByType(loopback.User); userModel.findById(this.principalId, callback); } else { - process.nextTick(function () { - callback && callback(null, null); + process.nextTick(function() { + if (callback) callback(null, null); }); } }; @@ -59,14 +59,14 @@ module.exports = function(RoleMapping) { * @param {Error} err * @param {User} childUser */ - RoleMapping.prototype.childRole = function (callback) { + RoleMapping.prototype.childRole = function(callback) { if (this.principalType === RoleMapping.ROLE) { var roleModel = this.constructor.Role || loopback.getModelByType(loopback.Role); roleModel.findById(this.principalId, callback); } else { - process.nextTick(function () { - callback && callback(null, null); + process.nextTick(function() { + if (callback) callback(null, null); }); } }; diff --git a/common/models/role.js b/common/models/role.js index 406389d9..595b4eac 100644 --- a/common/models/role.js +++ b/common/models/role.js @@ -33,7 +33,7 @@ module.exports = function(Role) { roleMappingModel.find({where: {roleId: this.id, principalType: RoleMapping.USER}}, function(err, mappings) { if (err) { - callback && callback(err); + if (callback) callback(err); return; } return mappings.map(function(m) { @@ -46,7 +46,7 @@ module.exports = function(Role) { roleMappingModel.find({where: {roleId: this.id, principalType: RoleMapping.APPLICATION}}, function(err, mappings) { if (err) { - callback && callback(err); + if (callback) callback(err); return; } return mappings.map(function(m) { @@ -59,7 +59,7 @@ module.exports = function(Role) { roleMappingModel.find({where: {roleId: this.id, principalType: RoleMapping.ROLE}}, function(err, mappings) { if (err) { - callback && callback(err); + if (callback) callback(err); return; } return mappings.map(function(m) { @@ -72,10 +72,10 @@ module.exports = function(Role) { // Special roles Role.OWNER = '$owner'; // owner of the object - Role.RELATED = "$related"; // any User with a relationship to the object - Role.AUTHENTICATED = "$authenticated"; // authenticated user - Role.UNAUTHENTICATED = "$unauthenticated"; // authenticated user - Role.EVERYONE = "$everyone"; // everyone + Role.RELATED = '$related'; // any User with a relationship to the object + Role.AUTHENTICATED = '$authenticated'; // authenticated user + Role.UNAUTHENTICATED = '$unauthenticated'; // authenticated user + Role.EVERYONE = '$everyone'; // everyone /** * Add custom handler for roles. @@ -93,7 +93,7 @@ module.exports = function(Role) { Role.registerResolver(Role.OWNER, function(role, context, callback) { if (!context || !context.model || !context.modelId) { process.nextTick(function() { - callback && callback(null, false); + if (callback) callback(null, false); }); return; } @@ -152,13 +152,13 @@ module.exports = function(Role) { modelClass.findById(modelId, function(err, inst) { if (err || !inst) { debug('Model not found for id %j', modelId); - callback && callback(err, false); + if (callback) callback(err, false); return; } debug('Model found: %j', inst); var ownerId = inst.userId || inst.owner; if (ownerId) { - callback && callback(null, matches(ownerId, userId)); + if (callback) callback(null, matches(ownerId, userId)); return; } else { // Try to follow belongsTo @@ -166,19 +166,21 @@ module.exports = function(Role) { var rel = modelClass.relations[r]; if (rel.type === 'belongsTo' && isUserClass(rel.modelTo)) { debug('Checking relation %s to %s: %j', r, rel.modelTo.modelName, rel); - inst[r](function(err, user) { - if (!err && user) { - debug('User found: %j', user.id); - callback && callback(null, matches(user.id, userId)); - } else { - callback && callback(err, false); - } - }); + inst[r](processRelatedUser); return; } } debug('No matching belongsTo relation found for model %j and user: %j', modelId, userId); - callback && callback(null, false); + if (callback) callback(null, false); + } + + function processRelatedUser(err, user) { + if (!err && user) { + debug('User found: %j', user.id); + if (callback) callback(null, matches(user.id, userId)); + } else { + if (callback) callback(err, false); + } } }); }; @@ -186,7 +188,7 @@ module.exports = function(Role) { Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) { if (!context) { process.nextTick(function() { - callback && callback(null, false); + if (callback) callback(null, false); }); return; } @@ -202,19 +204,19 @@ module.exports = function(Role) { */ Role.isAuthenticated = function isAuthenticated(context, callback) { process.nextTick(function() { - callback && callback(null, context.isAuthenticated()); + if (callback) callback(null, context.isAuthenticated()); }); }; Role.registerResolver(Role.UNAUTHENTICATED, function(role, context, callback) { process.nextTick(function() { - callback && callback(null, !context || !context.isAuthenticated()); + if (callback) callback(null, !context || !context.isAuthenticated()); }); }); Role.registerResolver(Role.EVERYONE, function(role, context, callback) { process.nextTick(function() { - callback && callback(null, true); // Always true + if (callback) callback(null, true); // Always true }); }); @@ -245,7 +247,7 @@ module.exports = function(Role) { if (context.principals.length === 0) { debug('isInRole() returns: false'); process.nextTick(function() { - callback && callback(null, false); + if (callback) callback(null, false); }); return; } @@ -262,7 +264,7 @@ module.exports = function(Role) { if (inRole) { debug('isInRole() returns: %j', inRole); process.nextTick(function() { - callback && callback(null, true); + if (callback) callback(null, true); }); return; } @@ -270,11 +272,11 @@ module.exports = function(Role) { var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping); this.findOne({where: {name: role}}, function(err, result) { if (err) { - callback && callback(err); + if (callback) callback(err); return; } if (!result) { - callback && callback(null, false); + if (callback) callback(null, false); return; } debug('Role found: %j', result); @@ -303,7 +305,7 @@ module.exports = function(Role) { } }, function(inRole) { debug('isInRole() returns: %j', inRole); - callback && callback(null, inRole); + if (callback) callback(null, inRole); }); }); @@ -315,8 +317,8 @@ module.exports = function(Role) { * @param {Function} callback * * @callback {Function} callback - * @param err - * @param {String[]} An array of role ids + * @param {Error=} err + * @param {String[]} roles An array of role ids */ Role.getRoles = function(context, callback) { if (!(context instanceof AccessContext)) { @@ -354,8 +356,8 @@ module.exports = function(Role) { // Check against the role mappings var principalType = p.type || undefined; var principalId = p.id == null ? undefined : p.id; - - if(typeof principalId !== 'string' && principalId != null) { + + if (typeof principalId !== 'string' && principalId != null) { principalId = principalId.toString(); } @@ -371,13 +373,13 @@ module.exports = function(Role) { principalId: principalId}}, function(err, mappings) { debug('Role mappings found: %s %j', err, mappings); if (err) { - done && done(err); + if (done) done(err); return; } mappings.forEach(function(m) { addRole(m.roleId); }); - done && done(); + if (done) done(); }); }); } @@ -385,7 +387,7 @@ module.exports = function(Role) { async.parallel(inRoleTasks, function(err, results) { debug('getRoles() returns: %j %j', err, roles); - callback && callback(err, roles); + if (callback) callback(err, roles); }); }; }; diff --git a/common/models/scope.js b/common/models/scope.js index 7ffe1d1d..39bd9bde 100644 --- a/common/models/scope.js +++ b/common/models/scope.js @@ -23,14 +23,14 @@ module.exports = function(Scope) { * @param {String|Error} err The error object * @param {AccessRequest} result The access permission */ - Scope.checkPermission = function (scope, model, property, accessType, callback) { + Scope.checkPermission = function(scope, model, property, accessType, callback) { var ACL = loopback.ACL; assert(ACL, 'ACL model must be defined before Scope.checkPermission is called'); - this.findOne({where: {name: scope}}, function (err, scope) { + this.findOne({where: {name: scope}}, function(err, scope) { if (err) { - callback && callback(err); + if (callback) callback(err); } else { var aclModel = loopback.getModelByType(ACL); aclModel.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback); diff --git a/common/models/user.js b/common/models/user.js index 5bd3869c..019894c5 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -2,15 +2,15 @@ * Module Dependencies. */ -var loopback = require('../../lib/loopback') - , path = require('path') - , SALT_WORK_FACTOR = 10 - , crypto = require('crypto') - , bcrypt = require('bcryptjs') - , DEFAULT_TTL = 1209600 // 2 weeks in seconds - , DEFAULT_RESET_PW_TTL = 15 * 60 // 15 mins in seconds - , DEFAULT_MAX_TTL = 31556926 // 1 year in seconds - , assert = require('assert'); +var loopback = require('../../lib/loopback'); +var path = require('path'); +var SALT_WORK_FACTOR = 10; +var crypto = require('crypto'); +var bcrypt = require('bcryptjs'); +var DEFAULT_TTL = 1209600; // 2 weeks in seconds +var DEFAULT_RESET_PW_TTL = 15 * 60; // 15 mins in seconds +var DEFAULT_MAX_TTL = 31556926; // 1 year in seconds +var assert = require('assert'); var debug = require('debug')('loopback:user'); @@ -40,531 +40,533 @@ var debug = require('debug')('loopback:user'); module.exports = function(User) { -/** - * Create access token for the logged in user. This method can be overridden to - * customize how access tokens are generated - * - * @param [Number} ttl The requested ttl - * @callack {Function} cb The callback function - * @param {String|Error} err The error string or object - * @param {AccessToken} token The generated access token object - */ -User.prototype.createAccessToken = function(ttl, cb) { - var userModel = this.constructor; - ttl = Math.min(ttl || userModel.settings.ttl, userModel.settings.maxTTL); - this.accessTokens.create({ - ttl: ttl - }, cb); -}; + /** + * Create access token for the logged in user. This method can be overridden to + * customize how access tokens are generated + * + * @param {Number} ttl The requested ttl + * @callack {Function} cb The callback function + * @param {String|Error} err The error string or object + * @param {AccessToken} token The generated access token object + */ + User.prototype.createAccessToken = function(ttl, cb) { + var userModel = this.constructor; + ttl = Math.min(ttl || userModel.settings.ttl, userModel.settings.maxTTL); + this.accessTokens.create({ + ttl: ttl + }, cb); + }; -function splitPrincipal(name, realmDelimiter) { - var parts = [null, name]; - if(!realmDelimiter) { + function splitPrincipal(name, realmDelimiter) { + var parts = [null, name]; + if (!realmDelimiter) { + return parts; + } + var index = name.indexOf(realmDelimiter); + if (index !== -1) { + parts[0] = name.substring(0, index); + parts[1] = name.substring(index + realmDelimiter.length); + } return parts; } - var index = name.indexOf(realmDelimiter); - if (index !== -1) { - parts[0] = name.substring(0, index); - parts[1] = name.substring(index + realmDelimiter.length); - } - return parts; -} -/** - * Normalize the credentials - * @param {Object} credentials The credential object - * @param {Boolean} realmRequired - * @param {String} realmDelimiter The realm delimiter, if not set, no realm is needed - * @returns {Object} The normalized credential object - */ -User.normalizeCredentials = function(credentials, realmRequired, realmDelimiter) { - var query = {}; - credentials = credentials || {}; - if(!realmRequired) { - if (credentials.email) { - query.email = credentials.email; - } else if (credentials.username) { - query.username = credentials.username; - } - } else { - if (credentials.realm) { - query.realm = credentials.realm; - } - var parts; - if (credentials.email) { - parts = splitPrincipal(credentials.email, realmDelimiter); - query.email = parts[1]; - if (parts[0]) { - query.realm = parts[0]; + /** + * Normalize the credentials + * @param {Object} credentials The credential object + * @param {Boolean} realmRequired + * @param {String} realmDelimiter The realm delimiter, if not set, no realm is needed + * @returns {Object} The normalized credential object + */ + User.normalizeCredentials = function(credentials, realmRequired, realmDelimiter) { + var query = {}; + credentials = credentials || {}; + if (!realmRequired) { + if (credentials.email) { + query.email = credentials.email; + } else if (credentials.username) { + query.username = credentials.username; } - } else if (credentials.username) { - parts = splitPrincipal(credentials.username, realmDelimiter); - query.username = parts[1]; - if (parts[0]) { - query.realm = parts[0]; + } else { + if (credentials.realm) { + query.realm = credentials.realm; } - } - } - return query; -} - -/** - * Login a user by with the given `credentials`. - * - * ```js - * User.login({username: 'foo', password: 'bar'}, function (err, token) { -* console.log(token.id); -* }); - * ``` - * - * @param {Object} credentials - * @callback {Function} callback - * @param {Error} err - * @param {AccessToken} token - */ - -User.login = function(credentials, include, fn) { - var self = this; - if (typeof include === 'function') { - fn = include; - include = undefined; - } - - include = (include || ''); - if (Array.isArray(include)) { - include = include.map(function(val) { - return val.toLowerCase(); - }); - } else { - include = include.toLowerCase(); - } - - var realmDelimiter; - // Check if realm is required - var realmRequired = !!(self.settings.realmRequired || - self.settings.realmDelimiter); - if (realmRequired) { - realmDelimiter = self.settings.realmDelimiter; - } - var query = self.normalizeCredentials(credentials, realmRequired, - realmDelimiter); - - if(realmRequired && !query.realm) { - var err1 = new Error('realm is required'); - err1.statusCode = 400; - return fn(err1); - } - if (!query.email && !query.username) { - var err2 = new Error('username or email is required'); - err2.statusCode = 400; - return fn(err2); - } - - self.findOne({where: query}, function(err, user) { - var defaultError = new Error('login failed'); - defaultError.statusCode = 401; - - if (err) { - debug('An error is reported from User.findOne: %j', err); - fn(defaultError); - } else if (user) { - if (self.settings.emailVerificationRequired) { - if (!user.emailVerified) { - // Fail to log in if email verification is not done yet - debug('User email has not been verified'); - err = new Error('login failed as the email has not been verified'); - err.statusCode = 401; - return fn(err); + var parts; + if (credentials.email) { + parts = splitPrincipal(credentials.email, realmDelimiter); + query.email = parts[1]; + if (parts[0]) { + query.realm = parts[0]; + } + } else if (credentials.username) { + parts = splitPrincipal(credentials.username, realmDelimiter); + query.username = parts[1]; + if (parts[0]) { + query.realm = parts[0]; } } - user.hasPassword(credentials.password, function(err, isMatch) { - if (err) { - debug('An error is reported from User.hasPassword: %j', err); - fn(defaultError); - } else if (isMatch) { - user.createAccessToken(credentials.ttl, function(err, token) { - if (err) return fn(err); - if (Array.isArray(include) ? include.indexOf('user') !== -1 : include === 'user') { - // NOTE(bajtos) We can't set token.user here: - // 1. token.user already exists, it's a function injected by - // "AccessToken belongsTo User" relation - // 2. ModelBaseClass.toJSON() ignores own properties, thus - // the value won't be included in the HTTP response - // See also loopback#161 and loopback#162 - token.__data.user = user; - } - fn(err, token); - }); - } else { - debug('The password is invalid for user %s', query.email || query.username); - fn(defaultError); - } + } + return query; + }; + + /** + * Login a user by with the given `credentials`. + * + * ```js + * User.login({username: 'foo', password: 'bar'}, function (err, token) { + * console.log(token.id); + * }); + * ``` + * + * @param {Object} credentials username/password or email/password + * @param {String[]|String} [include] Optionally set it to "user" to include + * the user info + * @callback {Function} callback Callback function + * @param {Error} err Error object + * @param {AccessToken} token Access token if login is successful + */ + + User.login = function(credentials, include, fn) { + var self = this; + if (typeof include === 'function') { + fn = include; + include = undefined; + } + + include = (include || ''); + if (Array.isArray(include)) { + include = include.map(function(val) { + return val.toLowerCase(); }); } else { - debug('No matching record is found for user %s', query.email || query.username); - fn(defaultError); + include = include.toLowerCase(); } - }); -}; -/** - * Logout a user with the given accessToken id. - * - * ```js - * User.logout('asd0a9f8dsj9s0s3223mk', function (err) { -* console.log(err || 'Logged out'); -* }); - * ``` - * - * @param {String} accessTokenID - * @callback {Function} callback - * @param {Error} err - */ - -User.logout = function(tokenId, fn) { - this.relations.accessTokens.modelTo.findById(tokenId, function(err, accessToken) { - if (err) { - fn(err); - } else if (accessToken) { - accessToken.destroy(fn); - } else { - fn(new Error('could not find accessToken')); + var realmDelimiter; + // Check if realm is required + var realmRequired = !!(self.settings.realmRequired || + self.settings.realmDelimiter); + if (realmRequired) { + realmDelimiter = self.settings.realmDelimiter; } - }); -} + var query = self.normalizeCredentials(credentials, realmRequired, + realmDelimiter); -/** - * Compare the given `password` with the users hashed password. - * - * @param {String} password The plain text password - * @returns {Boolean} - */ + if (realmRequired && !query.realm) { + var err1 = new Error('realm is required'); + err1.statusCode = 400; + return fn(err1); + } + if (!query.email && !query.username) { + var err2 = new Error('username or email is required'); + err2.statusCode = 400; + return fn(err2); + } -User.prototype.hasPassword = function(plain, fn) { - if (this.password && plain) { - bcrypt.compare(plain, this.password, function(err, isMatch) { - if (err) return fn(err); - fn(null, isMatch); - }); - } else { - fn(null, false); - } -} + self.findOne({where: query}, function(err, user) { + var defaultError = new Error('login failed'); + defaultError.statusCode = 401; -/** - * Verify a user's identity by sending them a confirmation email. - * - * ```js - * var options = { -* type: 'email', -* to: user.email, -* template: 'verify.ejs', -* redirect: '/' -* }; - * - * user.verify(options, next); - * ``` - * - * @options {Object} options - * @property {String} type Must be 'email'. - * @property {String} to Email address to which verification email is sent. - * @property {String} from Sender email addresss, for example - * `'noreply@myapp.com'`. - * @property {String} subject Subject line text. - * @property {String} text Text of email. - * @property {String} template Name of template that displays verification - * page, for example, `'verify.ejs'. - * @property {String} redirect Page to which user will be redirected after - * they verify their email, for example `'/'` for root URI. - */ - -User.prototype.verify = function(options, fn) { - var user = this; - var userModel = this.constructor; - assert(typeof options === 'object', 'options required when calling user.verify()'); - assert(options.type, 'You must supply a verification type (options.type)'); - assert(options.type === 'email', 'Unsupported verification type'); - assert(options.to || this.email, 'Must include options.to when calling user.verify() or the user must have an email property'); - assert(options.from, 'Must include options.from when calling user.verify() or the user must have an email property'); - - options.redirect = options.redirect || '/'; - options.template = path.resolve(options.template || path.join(__dirname, '..', '..', 'templates', 'verify.ejs')); - options.user = this; - options.protocol = options.protocol || 'http'; - - var app = userModel.app; - options.host = options.host || (app && app.get('host')) || 'localhost'; - options.port = options.port || (app && app.get('port')) || 3000; - options.restApiRoot = options.restApiRoot || (app && app.get('restApiRoot')) || '/api'; - options.verifyHref = options.verifyHref || - options.protocol - + '://' - + options.host - + ':' - + options.port - + options.restApiRoot - + userModel.http.path - + userModel.confirm.http.path - + '?uid=' - + options.user.id - + '&redirect=' - + options.redirect; - - - // Email model - var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email); - - crypto.randomBytes(64, function(err, buf) { - if (err) { - fn(err); - } else { - user.verificationToken = buf.toString('hex'); - user.save(function(err) { - if (err) { - fn(err); - } else { - sendEmail(user); + if (err) { + debug('An error is reported from User.findOne: %j', err); + fn(defaultError); + } else if (user) { + if (self.settings.emailVerificationRequired) { + if (!user.emailVerified) { + // Fail to log in if email verification is not done yet + debug('User email has not been verified'); + err = new Error('login failed as the email has not been verified'); + err.statusCode = 401; + return fn(err); + } } + user.hasPassword(credentials.password, function(err, isMatch) { + if (err) { + debug('An error is reported from User.hasPassword: %j', err); + fn(defaultError); + } else if (isMatch) { + user.createAccessToken(credentials.ttl, function(err, token) { + if (err) return fn(err); + if (Array.isArray(include) ? include.indexOf('user') !== -1 : include === 'user') { + // NOTE(bajtos) We can't set token.user here: + // 1. token.user already exists, it's a function injected by + // "AccessToken belongsTo User" relation + // 2. ModelBaseClass.toJSON() ignores own properties, thus + // the value won't be included in the HTTP response + // See also loopback#161 and loopback#162 + token.__data.user = user; + } + fn(err, token); + }); + } else { + debug('The password is invalid for user %s', query.email || query.username); + fn(defaultError); + } + }); + } else { + debug('No matching record is found for user %s', query.email || query.username); + fn(defaultError); + } + }); + }; + + /** + * Logout a user with the given accessToken id. + * + * ```js + * User.logout('asd0a9f8dsj9s0s3223mk', function (err) { + * console.log(err || 'Logged out'); + * }); + * ``` + * + * @param {String} accessTokenID + * @callback {Function} callback + * @param {Error} err + */ + + User.logout = function(tokenId, fn) { + this.relations.accessTokens.modelTo.findById(tokenId, function(err, accessToken) { + if (err) { + fn(err); + } else if (accessToken) { + accessToken.destroy(fn); + } else { + fn(new Error('could not find accessToken')); + } + }); + }; + + /** + * Compare the given `password` with the users hashed password. + * + * @param {String} password The plain text password + * @returns {Boolean} + */ + + User.prototype.hasPassword = function(plain, fn) { + if (this.password && plain) { + bcrypt.compare(plain, this.password, function(err, isMatch) { + if (err) return fn(err); + fn(null, isMatch); }); + } else { + fn(null, false); } - }); + }; - // TODO - support more verification types - function sendEmail(user) { - options.verifyHref += '&token=' + user.verificationToken; + /** + * Verify a user's identity by sending them a confirmation email. + * + * ```js + * var options = { + * type: 'email', + * to: user.email, + * template: 'verify.ejs', + * redirect: '/' + * }; + * + * user.verify(options, next); + * ``` + * + * @options {Object} options + * @property {String} type Must be 'email'. + * @property {String} to Email address to which verification email is sent. + * @property {String} from Sender email addresss, for example + * `'noreply@myapp.com'`. + * @property {String} subject Subject line text. + * @property {String} text Text of email. + * @property {String} template Name of template that displays verification + * page, for example, `'verify.ejs'. + * @property {String} redirect Page to which user will be redirected after + * they verify their email, for example `'/'` for root URI. + */ - options.text = options.text || 'Please verify your email by opening this link in a web browser:\n\t{href}'; + User.prototype.verify = function(options, fn) { + var user = this; + var userModel = this.constructor; + assert(typeof options === 'object', 'options required when calling user.verify()'); + assert(options.type, 'You must supply a verification type (options.type)'); + assert(options.type === 'email', 'Unsupported verification type'); + assert(options.to || this.email, 'Must include options.to when calling user.verify() or the user must have an email property'); + assert(options.from, 'Must include options.from when calling user.verify() or the user must have an email property'); - options.text = options.text.replace('{href}', options.verifyHref); + options.redirect = options.redirect || '/'; + options.template = path.resolve(options.template || path.join(__dirname, '..', '..', 'templates', 'verify.ejs')); + options.user = this; + options.protocol = options.protocol || 'http'; - var template = loopback.template(options.template); - Email.send({ - to: options.to || user.email, - from: options.from, - subject: options.subject || 'Thanks for Registering', - text: options.text, - html: template(options), - headers: options.headers || {} - }, function (err, email) { - if(err) { + var app = userModel.app; + options.host = options.host || (app && app.get('host')) || 'localhost'; + options.port = options.port || (app && app.get('port')) || 3000; + options.restApiRoot = options.restApiRoot || (app && app.get('restApiRoot')) || '/api'; + options.verifyHref = options.verifyHref || + options.protocol + + '://' + + options.host + + ':' + + options.port + + options.restApiRoot + + userModel.http.path + + userModel.confirm.http.path + + '?uid=' + + options.user.id + + '&redirect=' + + options.redirect; + + // Email model + var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email); + + crypto.randomBytes(64, function(err, buf) { + if (err) { fn(err); } else { - fn(null, {email: email, token: user.verificationToken, uid: user.id}); - } - }); - } -} - - -/** - * Confirm the user's identity. - * - * @param {Any} userId - * @param {String} token The validation token - * @param {String} redirect URL to redirect the user to once confirmed - * @callback {Function} callback - * @param {Error} err - */ -User.confirm = function(uid, token, redirect, fn) { - this.findById(uid, function(err, user) { - if (err) { - fn(err); - } else { - if (user && user.verificationToken === token) { - user.verificationToken = undefined; - user.emailVerified = true; + user.verificationToken = buf.toString('hex'); user.save(function(err) { if (err) { fn(err); } else { - fn(); + sendEmail(user); } }); - } else { - if (user) { - err = new Error('Invalid token: ' + token); - err.statusCode = 400; + } + }); + + // TODO - support more verification types + function sendEmail(user) { + options.verifyHref += '&token=' + user.verificationToken; + + options.text = options.text || 'Please verify your email by opening this link in a web browser:\n\t{href}'; + + options.text = options.text.replace('{href}', options.verifyHref); + + var template = loopback.template(options.template); + Email.send({ + to: options.to || user.email, + from: options.from, + subject: options.subject || 'Thanks for Registering', + text: options.text, + html: template(options), + headers: options.headers || {} + }, function(err, email) { + if (err) { + fn(err); } else { - err = new Error('User not found: ' + uid); - err.statusCode = 404; + fn(null, {email: email, token: user.verificationToken, uid: user.id}); } - fn(err); - } + }); } - }); -} + }; -/** - * Create a short lived acess token for temporary login. Allows users - * to change passwords if forgotten. - * - * @options {Object} options - * @prop {String} email The user's email address - * @callback {Function} callback - * @param {Error} err - */ - -User.resetPassword = function(options, cb) { - var UserModel = this; - var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL; - - options = options || {}; - if (typeof options.email === 'string') { - UserModel.findOne({ where: {email: options.email} }, function(err, user) { + /** + * Confirm the user's identity. + * + * @param {Any} userId + * @param {String} token The validation token + * @param {String} redirect URL to redirect the user to once confirmed + * @callback {Function} callback + * @param {Error} err + */ + User.confirm = function(uid, token, redirect, fn) { + this.findById(uid, function(err, user) { if (err) { - cb(err); - } else if (user) { - // create a short lived access token for temp login to change password - // TODO(ritch) - eventually this should only allow password change - user.accessTokens.create({ttl: ttl}, function(err, accessToken) { - if (err) { - cb(err); + fn(err); + } else { + if (user && user.verificationToken === token) { + user.verificationToken = undefined; + user.emailVerified = true; + user.save(function(err) { + if (err) { + fn(err); + } else { + fn(); + } + }); + } else { + if (user) { + err = new Error('Invalid token: ' + token); + err.statusCode = 400; } else { - cb(); - UserModel.emit('resetPasswordRequest', { - email: options.email, - accessToken: accessToken, - user: user - }); + err = new Error('User not found: ' + uid); + err.statusCode = 404; } - }) - } else { - cb(); - } - }); - } else { - var err = new Error('email is required'); - err.statusCode = 400; - - cb(err); - } -} - -/*! - * Setup an extended user model. - */ - -User.setup = function() { - // We need to call the base class's setup method - User.base.setup.call(this); - var UserModel = this; - - // max ttl - this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL; - this.settings.ttl = DEFAULT_TTL; - - UserModel.setter.password = function(plain) { - var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR); - this.$password = bcrypt.hashSync(plain, salt); - } - - // Make sure emailVerified is not set by creation - UserModel.beforeRemote('create', function(ctx, user, next) { - var body = ctx.req.body; - if (body && body.emailVerified) { - body.emailVerified = false; - } - next(); - }); - - loopback.remoteMethod( - UserModel.login, - { - description: 'Login a user with username/email and password', - accepts: [ - {arg: 'credentials', type: 'object', required: true, http: {source: 'body'}}, - {arg: 'include', type: 'string', http: {source: 'query' }, description: 'Related objects to include in the response. ' + - 'See the description of return value for more details.'} - ], - returns: { - arg: 'accessToken', type: 'object', root: true, description: 'The response body contains properties of the AccessToken created on login.\n' + - 'Depending on the value of `include` parameter, the body may contain ' + - 'additional properties:\n\n' + - ' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n' - }, - http: {verb: 'post'} - } - ); - - loopback.remoteMethod( - UserModel.logout, - { - description: 'Logout a user with access token', - accepts: [ - {arg: 'access_token', type: 'string', required: true, http: function(ctx) { - var req = ctx && ctx.req; - var accessToken = req && req.accessToken; - var tokenID = accessToken && accessToken.id; - - return tokenID; - }, description: 'Do not supply this argument, it is automatically extracted ' + - 'from request headers.' + fn(err); } - ], - http: {verb: 'all'} - } - ); - - loopback.remoteMethod( - UserModel.confirm, - { - description: 'Confirm a user registration with email verification token', - accepts: [ - {arg: 'uid', type: 'string', required: true}, - {arg: 'token', type: 'string', required: true}, - {arg: 'redirect', type: 'string', required: true} - ], - http: {verb: 'get', path: '/confirm'} - } - ); - - loopback.remoteMethod( - UserModel.resetPassword, - { - description: 'Reset password for a user with email', - accepts: [ - {arg: 'options', type: 'object', required: true, http: {source: 'body'}} - ], - http: {verb: 'post', path: '/reset'} - } - ); - - UserModel.on('attached', function() { - UserModel.afterRemote('confirm', function(ctx, inst, next) { - if (ctx.req) { - ctx.res.redirect(ctx.req.param('redirect')); - } else { - next(new Error('transport unsupported')); } }); - }); + }; - // default models - assert(loopback.Email, 'Email model must be defined before User model'); - UserModel.email = loopback.Email; + /** + * Create a short lived acess token for temporary login. Allows users + * to change passwords if forgotten. + * + * @options {Object} options + * @prop {String} email The user's email address + * @callback {Function} callback + * @param {Error} err + */ - assert(loopback.AccessToken, 'AccessToken model must be defined before User model'); - UserModel.accessToken = loopback.AccessToken; + User.resetPassword = function(options, cb) { + var UserModel = this; + var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL; - // email validation regex - var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + options = options || {}; + if (typeof options.email === 'string') { + UserModel.findOne({ where: {email: options.email} }, function(err, user) { + if (err) { + cb(err); + } else if (user) { + // create a short lived access token for temp login to change password + // TODO(ritch) - eventually this should only allow password change + user.accessTokens.create({ttl: ttl}, function(err, accessToken) { + if (err) { + cb(err); + } else { + cb(); + UserModel.emit('resetPasswordRequest', { + email: options.email, + accessToken: accessToken, + user: user + }); + } + }); + } else { + cb(); + } + }); + } else { + var err = new Error('email is required'); + err.statusCode = 400; + cb(err); + } + }; - UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'}); + /*! + * Setup an extended user model. + */ - // FIXME: We need to add support for uniqueness of composite keys in juggler - if (!(UserModel.settings.realmRequired || UserModel.settings.realmDelimiter)) { - UserModel.validatesUniquenessOf('email', {message: 'Email already exists'}); - UserModel.validatesUniquenessOf('username', {message: 'User already exists'}); - } + User.setup = function() { + // We need to call the base class's setup method + User.base.setup.call(this); + var UserModel = this; - return UserModel; -} + // max ttl + this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL; + this.settings.ttl = DEFAULT_TTL; -/*! - * Setup the base user. - */ + UserModel.setter.password = function(plain) { + var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR); + this.$password = bcrypt.hashSync(plain, salt); + }; -User.setup(); + // Make sure emailVerified is not set by creation + UserModel.beforeRemote('create', function(ctx, user, next) { + var body = ctx.req.body; + if (body && body.emailVerified) { + body.emailVerified = false; + } + next(); + }); + + loopback.remoteMethod( + UserModel.login, + { + description: 'Login a user with username/email and password', + accepts: [ + {arg: 'credentials', type: 'object', required: true, http: {source: 'body'}}, + {arg: 'include', type: 'string', http: {source: 'query' }, + description: 'Related objects to include in the response. ' + + 'See the description of return value for more details.'} + ], + returns: { + arg: 'accessToken', type: 'object', root: true, + description: + 'The response body contains properties of the AccessToken created on login.\n' + + 'Depending on the value of `include` parameter, the body may contain ' + + 'additional properties:\n\n' + + ' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n' + }, + http: {verb: 'post'} + } + ); + + loopback.remoteMethod( + UserModel.logout, + { + description: 'Logout a user with access token', + accepts: [ + {arg: 'access_token', type: 'string', required: true, http: function(ctx) { + var req = ctx && ctx.req; + var accessToken = req && req.accessToken; + var tokenID = accessToken && accessToken.id; + + return tokenID; + }, description: 'Do not supply this argument, it is automatically extracted ' + + 'from request headers.' + } + ], + http: {verb: 'all'} + } + ); + + loopback.remoteMethod( + UserModel.confirm, + { + description: 'Confirm a user registration with email verification token', + accepts: [ + {arg: 'uid', type: 'string', required: true}, + {arg: 'token', type: 'string', required: true}, + {arg: 'redirect', type: 'string', required: true} + ], + http: {verb: 'get', path: '/confirm'} + } + ); + + loopback.remoteMethod( + UserModel.resetPassword, + { + description: 'Reset password for a user with email', + accepts: [ + {arg: 'options', type: 'object', required: true, http: {source: 'body'}} + ], + http: {verb: 'post', path: '/reset'} + } + ); + + UserModel.on('attached', function() { + UserModel.afterRemote('confirm', function(ctx, inst, next) { + if (ctx.req) { + ctx.res.redirect(ctx.req.param('redirect')); + } else { + next(new Error('transport unsupported')); + } + }); + }); + + // default models + assert(loopback.Email, 'Email model must be defined before User model'); + UserModel.email = loopback.Email; + + assert(loopback.AccessToken, 'AccessToken model must be defined before User model'); + UserModel.accessToken = loopback.AccessToken; + + // email validation regex + var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + + UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'}); + + // FIXME: We need to add support for uniqueness of composite keys in juggler + if (!(UserModel.settings.realmRequired || UserModel.settings.realmDelimiter)) { + UserModel.validatesUniquenessOf('email', {message: 'Email already exists'}); + UserModel.validatesUniquenessOf('username', {message: 'User already exists'}); + } + + return UserModel; + }; + + /*! + * Setup the base user. + */ + + User.setup(); }; diff --git a/docs.json b/docs.json index c47c897f..478a791d 100644 --- a/docs.json +++ b/docs.json @@ -4,24 +4,26 @@ "lib/application.js", "lib/loopback.js", "lib/registry.js", + "lib/access-context.js", { "title": "Base models", "depth": 2 }, "lib/model.js", "lib/persisted-model.js", { "title": "Middleware", "depth": 2 }, "lib/middleware/rest.js", + "lib/middleware/static.js", "lib/middleware/status.js", "lib/middleware/token.js", "lib/middleware/urlNotFound.js", { "title": "Built-in models", "depth": 2 }, "common/models/access-token.js", "common/models/acl.js", - "common/models/scope.js", "common/models/application.js", + "common/models/change.js", "common/models/email.js", - "common/models/role-mapping.js", "common/models/role.js", - "common/models/user.js", - "common/models/change.js" + "common/models/role-mapping.js", + "common/models/scope.js", + "common/models/user.js" ], "assets": "/docs/assets" } diff --git a/example/colors/app.js b/example/colors/app.js index a7fdb01f..e182f926 100644 --- a/example/colors/app.js +++ b/example/colors/app.js @@ -9,9 +9,7 @@ var schema = { var Color = app.model('color', schema); -app.dataSource('db', {adapter: 'memory'}); - -Color.dataSource('db'); +app.dataSource('db', {adapter: 'memory'}).attach(Color); Color.create({name: 'red'}); Color.create({name: 'green'}); diff --git a/example/context/app.js b/example/context/app.js new file mode 100644 index 00000000..12cedc07 --- /dev/null +++ b/example/context/app.js @@ -0,0 +1,29 @@ +var loopback = require('../../'); +var app = loopback(); + +// Create a LoopBack context for all requests +app.use(loopback.context()); + +// Store a request property in the context +app.use(function saveHostToContext(req, res, next) { + var ns = loopback.getCurrentContext(); + ns.set('host', req.host); + next(); +}); + +app.use(loopback.rest()); + +var Color = loopback.createModel('color', { 'name': String }); +Color.beforeRemote('**', function (ctx, unused, next) { + // Inside LoopBack code, you can read the property from the context + var ns = loopback.getCurrentContext(); + console.log('Request to host', ns && ns.get('host')); + next(); +}); + +app.dataSource('db', { connector: 'memory' }); +app.model(Color, { dataSource: 'db' }); + +app.listen(3000, function() { + console.log('A list of colors is available at http://localhost:3000/colors'); +}); diff --git a/example/simple-data-source/app.js b/example/simple-data-source/app.js index 08d6d94a..3964df77 100644 --- a/example/simple-data-source/app.js +++ b/example/simple-data-source/app.js @@ -20,4 +20,4 @@ Color.all(function () { app.listen(3000); -console.log('a list of colors is available at http://localhost:300/colors'); \ No newline at end of file +console.log('a list of colors is available at http://localhost:3000/colors'); diff --git a/lib/access-context.js b/lib/access-context.js index e2d52ebb..b838bcc1 100644 --- a/lib/access-context.js +++ b/lib/access-context.js @@ -38,13 +38,13 @@ function AccessContext(context) { this.method = context.method; this.sharedMethod = context.sharedMethod; this.sharedClass = this.sharedMethod && this.sharedMethod.sharedClass; - if(this.sharedMethod) { + if (this.sharedMethod) { this.methodNames = this.sharedMethod.aliases.concat([this.sharedMethod.name]); } else { this.methodNames = []; } - if(this.sharedMethod) { + if (this.sharedMethod) { this.accessType = this.model._getAccessTypeForMethod(this.sharedMethod); } @@ -100,7 +100,7 @@ AccessContext.permissionOrder = { * @param {String} [principalName] The principal name * @returns {boolean} */ -AccessContext.prototype.addPrincipal = function (principalType, principalId, principalName) { +AccessContext.prototype.addPrincipal = function(principalType, principalId, principalName) { var principal = new Principal(principalType, principalId, principalName); for (var i = 0; i < this.principals.length; i++) { var p = this.principals[i]; @@ -126,7 +126,6 @@ AccessContext.prototype.getUserId = function() { return null; }; - /** * Get the application id * @returns {*} @@ -149,14 +148,14 @@ AccessContext.prototype.isAuthenticated = function() { return !!(this.getUserId() || this.getAppId()); }; -/** +/*! * Print debug info for access context. */ AccessContext.prototype.debug = function() { - if(debug.enabled) { + if (debug.enabled) { debug('---AccessContext---'); - if(this.principals && this.principals.length) { + if (this.principals && this.principals.length) { debug('principals:'); this.principals.forEach(function(principal) { debug('principal: %j', principal); @@ -169,7 +168,7 @@ AccessContext.prototype.debug = function() { debug('property %s', this.property); debug('method %s', this.method); debug('accessType %s', this.accessType); - if(this.accessToken) { + if (this.accessToken) { debug('accessToken:'); debug(' id %j', this.accessToken.id); debug(' ttl %j', this.accessToken.ttl); @@ -206,9 +205,9 @@ Principal.SCOPE = 'SCOPE'; /** * Compare if two principals are equal * Returns true if argument principal is equal to this principal. - * @param {Object} principal The other principal + * @param {Object} p The other principal */ -Principal.prototype.equals = function (p) { +Principal.prototype.equals = function(p) { if (p instanceof Principal) { return this.type === p.type && String(this.id) === String(p.id); } @@ -250,7 +249,7 @@ function AccessRequest(model, property, accessType, permission, methodNames) { * * @returns {Boolean} */ -AccessRequest.prototype.isWildcard = function () { +AccessRequest.prototype.isWildcard = function() { return this.model === AccessContext.ALL || this.property === AccessContext.ALL || this.accessType === AccessContext.ALL; @@ -268,7 +267,7 @@ AccessRequest.prototype.exactlyMatches = function(acl) { var matchesMethodName = this.methodNames.indexOf(acl.property) !== -1; var matchesAccessType = acl.accessType === this.accessType; - if(matchesModel && matchesAccessType) { + if (matchesModel && matchesAccessType) { return matchesProperty || matchesMethodName; } @@ -286,7 +285,7 @@ AccessRequest.prototype.isAllowed = function() { }; AccessRequest.prototype.debug = function() { - if(debug.enabled) { + if (debug.enabled) { debug('---AccessRequest---'); debug(' model %s', this.model); debug(' property %s', this.property); @@ -300,6 +299,3 @@ AccessRequest.prototype.debug = function() { module.exports.AccessContext = AccessContext; module.exports.Principal = Principal; module.exports.AccessRequest = AccessRequest; - - - diff --git a/lib/application.js b/lib/application.js index 4387b411..c66dd80e 100644 --- a/lib/application.js +++ b/lib/application.js @@ -2,15 +2,15 @@ * Module dependencies. */ -var DataSource = require('loopback-datasource-juggler').DataSource - , registry = require('./registry') - , assert = require('assert') - , fs = require('fs') - , extend = require('util')._extend - , _ = require('underscore') - , RemoteObjects = require('strong-remoting') - , stringUtils = require('underscore.string') - , path = require('path'); +var DataSource = require('loopback-datasource-juggler').DataSource; +var registry = require('./registry'); +var assert = require('assert'); +var fs = require('fs'); +var extend = require('util')._extend; +var _ = require('underscore'); +var RemoteObjects = require('strong-remoting'); +var stringUtils = require('underscore.string'); +var path = require('path'); /** * The `App` object represents a Loopback application. @@ -41,7 +41,7 @@ function App() { * Export the app prototype. */ -var app = exports = module.exports = {}; +var app = module.exports = {}; /** * Lazily load a set of [remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions). @@ -50,13 +50,13 @@ var app = exports = module.exports = {}; * @returns {RemoteObjects} */ -app.remotes = function () { - if(this._remotes) { +app.remotes = function() { + if (this._remotes) { return this._remotes; } else { var options = {}; - if(this.get) { + if (this.get) { options = this.get('remoting'); } @@ -68,10 +68,10 @@ app.remotes = function () { * Remove a route by reference. */ -app.disuse = function (route) { - if(this.stack) { +app.disuse = function(route) { + if (this.stack) { for (var i = 0; i < this.stack.length; i++) { - if(this.stack[i].route === route) { + if (this.stack[i].route === route) { this.stack.splice(i, 1); } } @@ -102,7 +102,7 @@ app.disuse = function (route) { * @returns {ModelConstructor} the model class */ -app.model = function (Model, config) { +app.model = function(Model, config) { var isPublic = true; if (arguments.length > 1) { config = config || {}; @@ -166,7 +166,7 @@ app.model = function (Model, config) { * ```js * var models = app.models(); * - * models.forEach(function (Model) { + * models.forEach(function(Model) { * console.log(Model.modelName); // color * }); * ``` @@ -205,7 +205,7 @@ app.model = function (Model, config) { * @returns {Array} Array of model classes. */ -app.models = function () { +app.models = function() { return this._models || (this._models = []); }; @@ -215,7 +215,7 @@ app.models = function () { * @param {String} name The data source name * @param {Object} config The data source config */ -app.dataSource = function (name, config) { +app.dataSource = function(name, config) { var ds = dataSourcesFromConfig(config, this.connectors); this.dataSources[name] = this.dataSources[classify(name)] = @@ -248,7 +248,7 @@ app.connector = function(name, connector) { * @returns {Object} [Remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions). */ -app.remoteObjects = function () { +app.remoteObjects = function() { var result = {}; this.remotes().classes().forEach(function(sharedClass) { @@ -263,9 +263,9 @@ app.remoteObjects = function () { * @triggers `mounted` events on shared class constructors (models) */ -app.handler = function (type, options) { +app.handler = function(type, options) { var handlers = this._handlers || (this._handlers = {}); - if(handlers[type]) { + if (handlers[type]) { return handlers[type]; } @@ -301,21 +301,21 @@ app.enableAuth = function() { var modelSettings = Model.settings || {}; var errStatusCode = modelSettings.aclErrorStatus || app.get('aclErrorStatus') || 401; - if(!req.accessToken){ + if (!req.accessToken) { errStatusCode = 401; } - if(Model.checkAccess) { + if (Model.checkAccess) { Model.checkAccess( req.accessToken, modelId, method, ctx, function(err, allowed) { - if(err) { + if (err) { console.log(err); next(err); - } else if(allowed) { + } else if (allowed) { next(); } else { @@ -358,7 +358,7 @@ function dataSourcesFromConfig(config, connectorRegistry) { assert(typeof config === 'object', 'cannont create data source without config object'); - if(typeof config.connector === 'string') { + if (typeof config.connector === 'string') { var name = config.connector; if (connectorRegistry[name]) { config.connector = connectorRegistry[name]; @@ -380,14 +380,16 @@ function configureModel(ModelCtor, config, app) { var dataSource = config.dataSource; - if(dataSource) { - if(typeof dataSource === 'string') { + if (dataSource) { + if (typeof dataSource === 'string') { dataSource = app.dataSources[dataSource]; } - assert(dataSource instanceof DataSource, - ModelCtor.modelName + ' is referencing a dataSource that does not exist: "' + - config.dataSource +'"'); + assert( + dataSource instanceof DataSource, + ModelCtor.modelName + ' is referencing a dataSource that does not exist: "' + + config.dataSource + '"' + ); } config = extend({}, config); diff --git a/lib/connectors/base-connector.js b/lib/connectors/base-connector.js index d5381a1f..c1e37b7b 100644 --- a/lib/connectors/base-connector.js +++ b/lib/connectors/base-connector.js @@ -8,11 +8,11 @@ module.exports = Connector; * Module dependencies. */ -var EventEmitter = require('events').EventEmitter - , debug = require('debug')('connector') - , util = require('util') - , inherits = util.inherits - , assert = require('assert'); +var EventEmitter = require('events').EventEmitter; +var debug = require('debug')('connector'); +var util = require('util'); +var inherits = util.inherits; +var assert = require('assert'); /** * Create a new `Connector` with the given `options`. @@ -38,9 +38,9 @@ inherits(Connector, EventEmitter); * Create an connector instance from a JugglingDB adapter. */ -Connector._createJDBAdapter = function (jdbModule) { +Connector._createJDBAdapter = function(jdbModule) { var fauxSchema = {}; - jdbModule.initialize(fauxSchema, function () { + jdbModule.initialize(fauxSchema, function() { // connected }); }; @@ -49,6 +49,6 @@ Connector._createJDBAdapter = function (jdbModule) { * Add default crud operations from a JugglingDB adapter. */ -Connector.prototype._addCrudOperationsFromJDBAdapter = function (connector) { +Connector.prototype._addCrudOperationsFromJDBAdapter = function(connector) { }; diff --git a/lib/connectors/mail.js b/lib/connectors/mail.js index 18157a8d..a36984c3 100644 --- a/lib/connectors/mail.js +++ b/lib/connectors/mail.js @@ -2,10 +2,10 @@ * Dependencies. */ -var mailer = require('nodemailer') - , assert = require('assert') - , debug = require('debug')('loopback:connector:mail') - , loopback = require('../loopback'); +var mailer = require('nodemailer'); +var assert = require('assert'); +var debug = require('debug')('loopback:connector:mail'); +var loopback = require('../loopback'); /** * Export the MailConnector class. @@ -24,19 +24,19 @@ function MailConnector(settings) { var transports = settings.transports; //if transports is not in settings object AND settings.transport exists - if(!transports && settings.transport){ + if (!transports && settings.transport) { //then wrap single transport in an array and assign to transports transports = [settings.transport]; } - if(!transports){ + if (!transports) { transports = []; } this.transportsIndex = {}; this.transports = []; - if(loopback.isServer) { + if (loopback.isServer) { transports.forEach(this.setupTransport.bind(this)); } } @@ -48,7 +48,6 @@ MailConnector.initialize = function(dataSource, callback) { MailConnector.prototype.DataAccessObject = Mailer; - /** * Add a transport to the available transports. See https://github.com/andris9/Nodemailer#setting-up-a-transport-method. * @@ -132,7 +131,7 @@ MailConnector.prototype.defaultTransport = function() { * @param {Function} callback Called after the e-mail is sent or the sending failed */ -Mailer.send = function (options, fn) { +Mailer.send = function(options, fn) { var dataSource = this.dataSource; var settings = dataSource && dataSource.settings; var connector = dataSource.connector; @@ -140,13 +139,13 @@ Mailer.send = function (options, fn) { var transport = connector.transportForName(options.transport); - if(!transport) { + if (!transport) { transport = connector.defaultTransport(); } - if(debug.enabled || settings && settings.debug) { + if (debug.enabled || settings && settings.debug) { console.log('Sending Mail:'); - if(options.transport) { + if (options.transport) { console.log('\t TRANSPORT:', options.transport); } console.log('\t TO:', options.to); @@ -156,12 +155,12 @@ Mailer.send = function (options, fn) { console.log('\t HTML:', options.html); } - if(transport) { + if (transport) { assert(transport.sendMail, 'You must supply an Email.settings.transports containing a valid transport'); transport.sendMail(options, fn); } else { - console.warn('Warning: No email transport specified for sending email.' - + ' Setup a transport to send mail messages.'); + console.warn('Warning: No email transport specified for sending email.' + + ' Setup a transport to send mail messages.'); process.nextTick(function() { fn(null, options); }); @@ -172,7 +171,7 @@ Mailer.send = function (options, fn) { * Send an email instance using `modelInstance.send()`. */ -Mailer.prototype.send = function (fn) { +Mailer.prototype.send = function(fn) { this.constructor.send(this, fn); }; diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index ddd00fb7..6a34417c 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -8,12 +8,12 @@ module.exports = Memory; * Module dependencies. */ -var Connector = require('./base-connector') - , debug = require('debug')('memory') - , util = require('util') - , inherits = util.inherits - , assert = require('assert') - , JdbMemory = require('loopback-datasource-juggler/lib/connectors/memory'); +var Connector = require('./base-connector'); +var debug = require('debug')('memory'); +var util = require('util'); +var inherits = util.inherits; +var assert = require('assert'); +var JdbMemory = require('loopback-datasource-juggler/lib/connectors/memory'); /** * Create a new `Memory` connector with the given `options`. diff --git a/lib/express-middleware.js b/lib/express-middleware.js index 5e79142b..f058a74a 100644 --- a/lib/express-middleware.js +++ b/lib/express-middleware.js @@ -1,4 +1,3 @@ -var express = require('express'); var path = require('path'); var middlewares = exports; @@ -12,7 +11,7 @@ function safeRequire(m) { } function createMiddlewareNotInstalled(memberName, moduleName) { - return function () { + return function() { var msg = 'The middleware loopback.' + memberName + ' is not installed.\n' + 'Run `npm install --save ' + moduleName + '` to fix the problem.'; throw new Error(msg); @@ -47,7 +46,7 @@ for (var m in middlewareModules) { // serve-favicon requires a path var favicon = middlewares.favicon; -middlewares.favicon = function (icon, options) { +middlewares.favicon = function(icon, options) { icon = icon || path.join(__dirname, '../favicon.ico'); return favicon(icon, options); }; diff --git a/lib/loopback.js b/lib/loopback.js index ac5bf9f2..fae80bb2 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -2,13 +2,14 @@ * Module dependencies. */ -var express = require('express') - , proto = require('./application') - , fs = require('fs') - , ejs = require('ejs') - , path = require('path') - , merge = require('util')._extend - , assert = require('assert'); +var express = require('express'); +var loopbackExpress = require('./server-app'); +var proto = require('./application'); +var fs = require('fs'); +var ejs = require('ejs'); +var path = require('path'); +var merge = require('util')._extend; +var assert = require('assert'); /** * LoopBack core module. It provides static properties and @@ -24,11 +25,13 @@ var express = require('express') * @property {String} mime * @property {Boolean} isBrowser True if running in a browser environment; false otherwise. Static read-only property. * @property {Boolean} isServer True if running in a server environment; false otherwise. Static read-only property. + * @property {String} faviconFile Path to a default favicon shipped with LoopBack. + * Use as follows: `app.use(require('serve-favicon')(loopback.faviconFile));` * @class loopback * @header loopback */ -var loopback = exports = module.exports = createApplication; +var loopback = module.exports = createApplication; /*! * Framework version. @@ -50,7 +53,7 @@ loopback.mime = express.mime; */ function createApplication() { - var app = express(); + var app = loopbackExpress(); merge(app, proto); @@ -117,13 +120,35 @@ if (loopback.isServer) { if (loopback.isServer) { fs - .readdirSync(path.join(__dirname, 'middleware')) - .filter(function (file) { + .readdirSync(path.join(__dirname, '..', 'server', 'middleware')) + .filter(function(file) { return file.match(/\.js$/); }) - .forEach(function (m) { - loopback[m.replace(/\.js$/, '')] = require('./middleware/' + m); + .forEach(function(m) { + loopback[m.replace(/\.js$/, '')] = require('../server/middleware/' + m); }); + + loopback.urlNotFound = loopback['url-not-found']; + delete loopback['url-not-found']; +} + +/* + * Expose path to the default favicon file + * + * ***only in node*** + */ + +if (loopback.isServer) { + /*! + * Path to a default favicon shipped with LoopBack. + * + * **Example** + * + * ```js + * app.use(require('serve-favicon')(loopback.faviconFile)); + * ``` + */ + loopback.faviconFile = path.resolve(__dirname, '../favicon.ico'); } /*! @@ -138,10 +163,10 @@ loopback.errorHandler.title = 'Loopback'; * @param {Object} options (optional) */ -loopback.remoteMethod = function (fn, options) { +loopback.remoteMethod = function(fn, options) { fn.shared = true; - if(typeof options === 'object') { - Object.keys(options).forEach(function (key) { + if (typeof options === 'object') { + Object.keys(options).forEach(function(key) { fn[key] = options[key]; }); } @@ -158,12 +183,16 @@ loopback.remoteMethod = function (fn, options) { * @returns {Function} */ -loopback.template = function (file) { +loopback.template = function(file) { var templates = this._templates || (this._templates = {}); var str = templates[file] || (templates[file] = fs.readFileSync(file, 'utf8')); return ejs.compile(str); }; +loopback.getCurrentContext = function() { + // A placeholder method, see lib/middleware/context.js for the real version + return null; +}; /*! * Built in models / services diff --git a/lib/model.js b/lib/model.js index 2ff0009c..f6a3e9af 100644 --- a/lib/model.js +++ b/lib/model.js @@ -57,7 +57,7 @@ var stringUtils = require('underscore.string'); * * ```js * MyModel.on('deletedAll', function(where) { - * if(where) { + * if (where) { * console.log('all models where ', where, ' have been deleted'); * // => all models where * // => {price: {gt: 100}} @@ -98,7 +98,7 @@ var Model = module.exports = registry.modelBuilder.define('Model'); * Called when a model is extended. */ -Model.setup = function () { +Model.setup = function() { var ModelCtor = this; var options = this.settings; var typeName = this.modelName; @@ -119,17 +119,17 @@ Model.setup = function () { }); // support remoting prototype methods - ModelCtor.sharedCtor = function (data, id, fn) { + ModelCtor.sharedCtor = function(data, id, fn) { var ModelCtor = this; - if(typeof data === 'function') { + if (typeof data === 'function') { fn = data; data = null; id = null; } else if (typeof id === 'function') { fn = id; - if(typeof data !== 'object') { + if (typeof data !== 'object') { id = data; data = null; } else { @@ -137,17 +137,17 @@ Model.setup = function () { } } - if(id && data) { + if (id && data) { var model = new ModelCtor(data); model.id = id; fn(null, model); - } else if(data) { + } else if (data) { fn(null, new ModelCtor(data)); - } else if(id) { - ModelCtor.findById(id, function (err, model) { - if(err) { + } else if (id) { + ModelCtor.findById(id, function(err, model) { + if (err) { fn(err); - } else if(model) { + } else if (model) { fn(null, model); } else { err = new Error('could not find a model with id ' + id); @@ -175,34 +175,34 @@ Model.setup = function () { ModelCtor.sharedCtor.returns = {root: true}; // before remote hook - ModelCtor.beforeRemote = function (name, fn) { + ModelCtor.beforeRemote = function(name, fn) { var self = this; - if(this.app) { + if (this.app) { var remotes = this.app.remotes(); var className = self.modelName; - remotes.before(className + '.' + name, function (ctx, next) { + remotes.before(className + '.' + name, function(ctx, next) { fn(ctx, ctx.result, next); }); } else { var args = arguments; - this.once('attached', function () { + this.once('attached', function() { self.beforeRemote.apply(self, args); }); } }; // after remote hook - ModelCtor.afterRemote = function (name, fn) { + ModelCtor.afterRemote = function(name, fn) { var self = this; - if(this.app) { + if (this.app) { var remotes = this.app.remotes(); var className = self.modelName; - remotes.after(className + '.' + name, function (ctx, next) { + remotes.after(className + '.' + name, function(ctx, next) { fn(ctx, ctx.result, next); }); } else { var args = arguments; - this.once('attached', function () { + this.once('attached', function() { self.afterRemote.apply(self, args); }); } @@ -246,11 +246,11 @@ Model.setup = function () { */ var _aclModel = null; Model._ACL = function getACL(ACL) { - if(ACL !== undefined) { + if (ACL !== undefined) { // The function is used as a setter _aclModel = ACL; } - if(_aclModel) { + if (_aclModel) { return _aclModel; } var aclModel = registry.getModel('ACL'); @@ -276,7 +276,7 @@ Model.checkAccess = function(token, modelId, sharedMethod, ctx, callback) { var aclModel = Model._ACL(); ctx = ctx || {}; - if(typeof ctx === 'function' && callback === undefined) { + if (typeof ctx === 'function' && callback === undefined) { callback = ctx; ctx = {}; } @@ -291,7 +291,7 @@ Model.checkAccess = function(token, modelId, sharedMethod, ctx, callback) { accessType: this._getAccessTypeForMethod(sharedMethod), remotingContext: ctx }, function(err, accessRequest) { - if(err) return callback(err); + if (err) return callback(err); callback(null, accessRequest.isAllowed()); }); }; @@ -304,7 +304,7 @@ Model.checkAccess = function(token, modelId, sharedMethod, ctx, callback) { */ Model._getAccessTypeForMethod = function(method) { - if(typeof method === 'string') { + if (typeof method === 'string') { method = {name: method}; } assert( @@ -314,7 +314,7 @@ Model._getAccessTypeForMethod = function(method) { var ACL = Model._ACL(); - switch(method.name) { + switch (method.name) { case'create': return ACL.WRITE; case 'updateOrCreate': @@ -353,7 +353,7 @@ Model._getAccessTypeForMethod = function(method) { Model.getApp = function(callback) { var Model = this; - if(this.app) { + if (this.app) { callback(null, this.app); } else { Model.once('attached', function() { @@ -378,7 +378,7 @@ Model.getApp = function(callback) { */ Model.remoteMethod = function(name, options) { - if(options.isStatic === undefined) { + if (options.isStatic === undefined) { options.isStatic = true; } this.sharedClass.defineMethod(name, options); @@ -423,7 +423,7 @@ Model.hasOneRemoting = function(relationName, relation, define) { }, fn); }; -Model.hasManyRemoting = function (relationName, relation, define) { +Model.hasManyRemoting = function(relationName, relation, define) { var pathName = (relation.options.http && relation.options.http.path) || relationName; var toModelName = relation.modelTo.modelName; @@ -457,7 +457,7 @@ Model.hasManyRemoting = function (relationName, relation, define) { description: 'Foreign key for ' + relationName, required: true, http: {source: 'path'}}, description: 'Delete a related item by id for ' + relationName, - returns: {} + returns: [] }, destroyByIdFunc); var updateByIdFunc = this.prototype['__updateById__' + relationName]; @@ -502,7 +502,7 @@ Model.hasManyRemoting = function (relationName, relation, define) { description: 'Foreign key for ' + relationName, required: true, http: {source: 'path'}}, description: 'Remove the ' + relationName + ' relation to an item by id', - returns: {} + returns: [] }, removeFunc); // FIXME: [rfeng] How to map a function with callback(err, true|false) to HEAD? @@ -519,7 +519,7 @@ Model.hasManyRemoting = function (relationName, relation, define) { rest: { // After hook to map exists to 200/404 for HEAD after: function(ctx, cb) { - if(ctx.result === false) { + if (ctx.result === false) { var modelName = ctx.method.sharedClass.name; var id = ctx.getArgByName('id'); var msg = 'Unknown "' + modelName + '" id "' + id + '".'; @@ -536,11 +536,21 @@ Model.hasManyRemoting = function (relationName, relation, define) { }; Model.scopeRemoting = function(scopeName, scope, define) { - var pathName = (scope.options && scope.options.http && scope.options.http.path) - || scopeName; + var pathName = + (scope.options && scope.options.http && scope.options.http.path) || scopeName; + var isStatic = scope.isStatic; var toModelName = scope.modelTo.modelName; + // https://github.com/strongloop/loopback/issues/811 + // Check if the scope is for a hasMany relation + var relation = this.relations[scopeName]; + if (relation && relation.modelTo) { + // For a relation with through model, the toModelName should be the one + // from the target model + toModelName = relation.modelTo.modelName; + } + define('__get__' + scopeName, { isStatic: isStatic, http: {verb: 'get', path: '/' + pathName}, @@ -592,10 +602,12 @@ Model.nestRemoting = function(relationName, options, cb) { var paramName = options.paramName || 'nk'; var http = [].concat(sharedToClass.http || [])[0]; + var httpPath; + var acceptArgs; if (relation.multiple) { - var httpPath = pathName + '/:' + paramName; - var acceptArgs = [ + httpPath = pathName + '/:' + paramName; + acceptArgs = [ { arg: paramName, type: 'any', http: { source: 'path' }, description: 'Foreign key for ' + relation.name, @@ -603,8 +615,8 @@ Model.nestRemoting = function(relationName, options, cb) { } ]; } else { - var httpPath = pathName; - var acceptArgs = []; + httpPath = pathName; + acceptArgs = []; } // A method should return the method name to use, if it is to be @@ -721,4 +733,3 @@ Model.ValidationError = require('loopback-datasource-juggler').ValidationError; // setup the initial model Model.setup(); - diff --git a/lib/persisted-model.js b/lib/persisted-model.js index 68c6a0fa..9bba528d 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -37,7 +37,7 @@ PersistedModel.setup = function setupPersistedModel() { var PersistedModel = this; // enable change tracking (usually for replication) - if(this.settings.trackChanges) { + if (this.settings.trackChanges) { PersistedModel._defineChangeModel(); PersistedModel.once('dataSourceAttached', function() { PersistedModel.enableChangeTracking(); @@ -53,9 +53,9 @@ PersistedModel.setup = function setupPersistedModel() { function throwNotAttached(modelName, methodName) { throw new Error( - 'Cannot call ' + modelName + '.'+ methodName + '().' - + ' The ' + methodName + ' method has not been setup.' - + ' The PersistedModel has not been correctly attached to a DataSource!' + 'Cannot call ' + modelName + '.' + methodName + '().' + + ' The ' + methodName + ' method has not been setup.' + + ' The PersistedModel has not been correctly attached to a DataSource!' ); } @@ -79,12 +79,12 @@ function convertNullToNotFoundError(ctx, cb) { /** * Create new instance of Model class, saved in database * - * @param {Object} data Optional data object. + * @param {Object}|[{Object}] data Optional data object. Can be either a single model instance or an array of instances. * @param {Function} cb Callback function with `cb(err, obj)` signature, * where `err` is error object and `obj` is null or Model instance. */ -PersistedModel.create = function (data, callback) { +PersistedModel.create = function(data, callback) { throwNotAttached(this.modelName, 'create'); }; @@ -226,7 +226,7 @@ PersistedModel.deleteAll = PersistedModel.destroyAll; * Example: * *```js - * Employee.update({managerId: 'x001'}, {managerId: 'x002'}, function(err) { + * Employee.updateAll({managerId: 'x001'}, {managerId: 'x002'}, function(err, count) { * ... * }); * ``` @@ -277,7 +277,7 @@ PersistedModel.deleteById = PersistedModel.destroyById; * @param {Function} cb Callback function called with (err, count). */ -PersistedModel.count = function (where, cb) { +PersistedModel.count = function(where, cb) { throwNotAttached(this.modelName, 'count'); }; @@ -290,7 +290,7 @@ PersistedModel.count = function (where, cb) { * @param {Function} [callback] Callback function called with (err, obj). */ -PersistedModel.prototype.save = function (options, callback) { +PersistedModel.prototype.save = function(options, callback) { var Model = this.constructor; if (typeof options == 'function') { @@ -298,7 +298,7 @@ PersistedModel.prototype.save = function (options, callback) { options = {}; } - callback = callback || function () { + callback = callback || function() { }; options = options || {}; @@ -322,7 +322,7 @@ PersistedModel.prototype.save = function (options, callback) { return save(); } - inst.isValid(function (valid) { + inst.isValid(function(valid) { if (valid) { save(); } else { @@ -337,12 +337,12 @@ PersistedModel.prototype.save = function (options, callback) { // then save function save() { - inst.trigger('save', function (saveDone) { - inst.trigger('update', function (updateDone) { + inst.trigger('save', function(saveDone) { + inst.trigger('update', function(updateDone) { Model.upsert(inst, function(err) { inst._initProperties(data); - updateDone.call(inst, function () { - saveDone.call(inst, function () { + updateDone.call(inst, function() { + saveDone.call(inst, function() { callback(err, inst); }); }); @@ -357,7 +357,7 @@ PersistedModel.prototype.save = function (options, callback) { * @returns {Boolean} Returns true if the data model is new; false otherwise. */ -PersistedModel.prototype.isNewRecord = function () { +PersistedModel.prototype.isNewRecord = function() { throwNotAttached(this.constructor.modelName, 'isNewRecord'); }; @@ -367,7 +367,7 @@ PersistedModel.prototype.isNewRecord = function () { * @param {Function} callback Callback function. */ -PersistedModel.prototype.destroy = function (cb) { +PersistedModel.prototype.destroy = function(cb) { throwNotAttached(this.constructor.modelName, 'destroy'); }; @@ -440,7 +440,7 @@ PersistedModel.prototype.setId = function(val) { PersistedModel.prototype.getId = function() { var data = this.toObject(); - if(!data) return; + if (!data) return; return data[this.getIdName()]; }; @@ -464,7 +464,7 @@ PersistedModel.getIdName = function() { var Model = this; var ds = Model.getDataSource(); - if(ds.idName) { + if (ds.idName) { return ds.idName(Model.modelName); } else { return 'id'; @@ -513,7 +513,7 @@ PersistedModel.setupRemoting = function() { // For GET, return {exists: true|false} as is return cb(); } - if(!ctx.result.exists) { + if (!ctx.result.exists) { var modelName = ctx.method.sharedClass.name; var id = ctx.getArgByName('id'); var msg = 'Unknown "' + modelName + '" id "' + id + '".'; @@ -594,7 +594,7 @@ PersistedModel.setupRemoting = function() { http: {verb: 'put', path: '/'} }); - if(options.trackChanges) { + if (options.trackChanges) { setRemoting(PersistedModel, 'diff', { description: 'Get a set of deltas and conflicts since the given checkpoint', accepts: [ @@ -607,8 +607,8 @@ PersistedModel.setupRemoting = function() { }); setRemoting(PersistedModel, 'changes', { - description: 'Get the changes to a model since a given checkpoint.' - + 'Provide a filter object to reduce the number of results returned.', + description: 'Get the changes to a model since a given checkpoint.' + + 'Provide a filter object to reduce the number of results returned.', accepts: [ {arg: 'since', type: 'number', description: 'Only return changes since this checkpoint'}, {arg: 'filter', type: 'object', description: 'Only include changes that match this filter'} @@ -683,12 +683,12 @@ PersistedModel.diff = function(since, remoteChanges, callback) { */ PersistedModel.changes = function(since, filter, callback) { - if(typeof since === 'function') { + if (typeof since === 'function') { filter = {}; callback = since; since = -1; } - if(typeof filter === 'function') { + if (typeof filter === 'function') { callback = filter; since = -1; filter = {}; @@ -708,18 +708,18 @@ PersistedModel.changes = function(since, filter, callback) { checkpoint: {gt: since}, modelName: this.modelName }, function(err, changes) { - if(err) return callback(err); + if (err) return callback(err); var ids = changes.map(function(change) { return change.getModelId(); }); filter.where[idName] = {inq: ids}; model.find(filter, function(err, models) { - if(err) return callback(err); + if (err) return callback(err); var modelIds = models.map(function(m) { return m[idName].toString(); }); callback(null, changes.filter(function(ch) { - if(ch.type() === Change.DELETE) return true; + if (ch.type() === Change.DELETE) return true; return modelIds.indexOf(ch.modelId) > -1; })); }); @@ -735,7 +735,7 @@ PersistedModel.changes = function(since, filter, callback) { PersistedModel.checkpoint = function(cb) { var Checkpoint = this.getChangeModel().getCheckpointModel(); this.getSourceId(function(err, sourceId) { - if(err) return cb(err); + if (err) return cb(err); Checkpoint.create({ sourceId: sourceId }, cb); @@ -772,11 +772,11 @@ PersistedModel.currentCheckpoint = function(cb) { PersistedModel.replicate = function(since, targetModel, options, callback) { var lastArg = arguments[arguments.length - 1]; - if(typeof lastArg === 'function' && arguments.length > 1) { + if (typeof lastArg === 'function' && arguments.length > 1) { callback = lastArg; } - if(typeof since === 'function' && since.modelName) { + if (typeof since === 'function' && since.modelName) { targetModel = since; since = -1; } @@ -796,7 +796,7 @@ PersistedModel.replicate = function(since, targetModel, options, callback) { ); callback = callback || function defaultReplicationCallback(err) { - if(err) throw err; + if (err) throw err; }; var tasks = [ @@ -820,7 +820,7 @@ PersistedModel.replicate = function(since, targetModel, options, callback) { function createSourceUpdates(_diff, cb) { diff = _diff; diff.conflicts = diff.conflicts || []; - if(diff && diff.deltas && diff.deltas.length) { + if (diff && diff.deltas && diff.deltas.length) { sourceModel.createUpdates(diff.deltas, cb); } else { // nothing to replicate @@ -838,7 +838,7 @@ PersistedModel.replicate = function(since, targetModel, options, callback) { } function done(err) { - if(err) return callback(err); + if (err) return callback(err); var conflicts = diff.conflicts.map(function(change) { return new Change.Conflict( @@ -846,11 +846,11 @@ PersistedModel.replicate = function(since, targetModel, options, callback) { ); }); - if(conflicts.length) { + if (conflicts.length) { sourceModel.emit('conflicts', conflicts); } - callback && callback(null, conflicts); + if (callback) callback(null, conflicts); } }; @@ -869,21 +869,21 @@ PersistedModel.createUpdates = function(deltas, cb) { var tasks = []; deltas.forEach(function(change) { - var change = new Change(change); + change = new Change(change); var type = change.type(); var update = {type: type, change: change}; - switch(type) { + switch (type) { case Change.CREATE: case Change.UPDATE: tasks.push(function(cb) { Model.findById(change.modelId, function(err, inst) { - if(err) return cb(err); - if(!inst) { + if (err) return cb(err); + if (!inst) { console.error('missing data for change:', change); - return cb && cb(new Error('missing data for change: ' - + change.modelId)); + return cb && + cb(new Error('missing data for change: ' + change.modelId)); } - if(inst.toObject) { + if (inst.toObject) { update.data = inst.toObject(); } else { update.data = inst; @@ -892,15 +892,15 @@ PersistedModel.createUpdates = function(deltas, cb) { cb(); }); }); - break; + break; case Change.DELETE: updates.push(update); - break; + break; } }); async.parallel(tasks, function(err) { - if(err) return cb(err); + if (err) return cb(err); cb(null, updates); }); }; @@ -921,7 +921,7 @@ PersistedModel.bulkUpdate = function(updates, callback) { var Change = this.getChangeModel(); updates.forEach(function(update) { - switch(update.type) { + switch (update.type) { case Change.UPDATE: case Change.CREATE: // var model = new Model(update.data); @@ -930,13 +930,13 @@ PersistedModel.bulkUpdate = function(updates, callback) { var model = new Model(update.data); model.save(cb); }); - break; + break; case Change.DELETE: var data = {}; data[idName] = update.change.modelId; var model = new Model(data); tasks.push(model.destroy.bind(model)); - break; + break; } }); @@ -969,7 +969,7 @@ PersistedModel.getChangeModel = function() { PersistedModel.getSourceId = function(cb) { var dataSource = this.dataSource; - if(!dataSource) { + if (!dataSource) { this.once('dataSourceAttached', this.getSourceId.bind(this, cb)); } assert( @@ -1005,21 +1005,21 @@ PersistedModel.enableChangeTracking = function() { Model.on('deletedAll', cleanup); - if(runtime.isServer) { + if (runtime.isServer) { // initial cleanup cleanup(); // cleanup setInterval(cleanup, cleanupInterval); + } - function cleanup() { - Model.rectifyAllChanges(function(err) { - if(err) { - console.error(Model.modelName + ' Change Cleanup Error:'); - console.error(err); - } - }); - } + function cleanup() { + Model.rectifyAllChanges(function(err) { + if (err) { + console.error(Model.modelName + ' Change Cleanup Error:'); + console.error(err); + } + }); } }; @@ -1028,12 +1028,14 @@ PersistedModel._defineChangeModel = function() { assert(BaseChangeModel, 'Change model must be defined before enabling change replication'); - return this.Change = BaseChangeModel.extend(this.modelName + '-change', + this.Change = BaseChangeModel.extend(this.modelName + '-change', {}, { trackModel: this } ); + + return this.Change; }; PersistedModel.rectifyAllChanges = function(callback) { @@ -1048,7 +1050,7 @@ PersistedModel.rectifyAllChanges = function(callback) { */ PersistedModel.handleChangeError = function(err) { - if(err) { + if (err) { console.error(Model.modelName + ' Change Tracking Error:'); console.error(err); } diff --git a/lib/registry.js b/lib/registry.js index 32e7f0aa..cf39879a 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -46,7 +46,7 @@ registry.modelBuilder = new ModelBuilder(); * 'Author', * { * firstName: 'string', - * lastName: 'string + * lastName: 'string' * }, * { * relations: { @@ -66,7 +66,7 @@ registry.modelBuilder = new ModelBuilder(); * name: 'Author', * properties: { * firstName: 'string', - * lastName: 'string + * lastName: 'string' * }, * relations: { * books: { @@ -84,7 +84,7 @@ registry.modelBuilder = new ModelBuilder(); * @header loopback.createModel */ -registry.createModel = function (name, properties, options) { +registry.createModel = function(name, properties, options) { if (arguments.length === 1 && typeof name === 'object') { var config = name; name = config.name; @@ -98,7 +98,7 @@ registry.createModel = function (name, properties, options) { options = options || {}; var BaseModel = options.base || options.super; - if(typeof BaseModel === 'string') { + if (typeof BaseModel === 'string') { var baseName = BaseModel; BaseModel = this.getModel(BaseModel); @@ -121,7 +121,7 @@ registry.createModel = function (name, properties, options) { // try to attach try { this.autoAttachModel(model); - } catch(e) {} + } catch (e) {} return model; }; @@ -145,6 +145,25 @@ function buildModelOptionsFromConfig(config) { return options; } +/* + * Add the acl entry to the acls + * @param {Object[]} acls + * @param {Object} acl + */ +function addACL(acls, acl) { + for (var i = 0, n = acls.length; i < n; i++) { + // Check if there is a matching acl to be overriden + if (acls[i].property === acl.property && + acls[i].accessType === acl.accessType && + acls[i].principalType === acl.principalType && + acls[i].principalId === acl.principalId) { + acls[i] = acl; + return; + } + } + acls.push(acl); +} + /** * Alter an existing Model class. * @param {Model} ModelCtor The model constructor to alter. @@ -157,12 +176,51 @@ function buildModelOptionsFromConfig(config) { registry.configureModel = function(ModelCtor, config) { var settings = ModelCtor.settings; + var modelName = ModelCtor.modelName; - if (config.relations) { + // Relations + if (typeof config.relations === 'object' && config.relations !== null) { var relations = settings.relations = settings.relations || {}; Object.keys(config.relations).forEach(function(key) { + // FIXME: [rfeng] We probably should check if the relation exists relations[key] = extend(relations[key] || {}, config.relations[key]); }); + } else if (config.relations != null) { + console.warn('The relations property of `%s` configuration ' + + 'must be an object', modelName); + } + + // ACLs + if (Array.isArray(config.acls)) { + var acls = settings.acls = settings.acls || []; + config.acls.forEach(function(acl) { + addACL(acls, acl); + }); + } else if (config.acls != null) { + console.warn('The acls property of `%s` configuration ' + + 'must be an array of objects', modelName); + } + + // Settings + var excludedProperties = { + base: true, + 'super': true, + relations: true, + acls: true, + dataSource: true + }; + if (typeof config.options === 'object' && config.options !== null) { + for (var p in config.options) { + if (!(p in excludedProperties)) { + settings[p] = config.options[p]; + } else { + console.warn('Property `%s` cannot be reconfigured for `%s`', + p, modelName); + } + } + } else if (config.options != null) { + console.warn('The options property of `%s` configuration ' + + 'must be an object', modelName); } // It's important to attach the datasource after we have updated @@ -173,17 +231,17 @@ registry.configureModel = function(ModelCtor, config) { ': config.dataSource must be an instance of DataSource'); ModelCtor.attachTo(config.dataSource); debug('Attached model `%s` to dataSource `%s`', - ModelCtor.definition.name, config.dataSource.name); + modelName, config.dataSource.name); } else if (config.dataSource === null) { debug('Model `%s` is not attached to any DataSource by configuration.', - ModelCtor.definition.name); + modelName); } else { debug('Model `%s` is not attached to any DataSource, possibly by a mistake.', - ModelCtor.definition.name); + modelName); console.warn( 'The configuration of `%s` is missing `dataSource` property.\n' + 'Use `null` or `false` to mark models not attached to any data source.', - ModelCtor.definition.name); + modelName); } }; @@ -228,8 +286,8 @@ registry.getModelByType = function(modelType) { assert(typeof modelType === 'function', 'The model type must be a constructor'); var models = this.modelBuilder.models; - for(var m in models) { - if(models[m].prototype instanceof modelType) { + for (var m in models) { + if (models[m].prototype instanceof modelType) { return models[m]; } } @@ -248,10 +306,10 @@ registry.getModelByType = function(modelType) { * @header loopback.createDataSource(name, options) */ -registry.createDataSource = function (name, options) { +registry.createDataSource = function(name, options) { var self = this; var ds = new DataSource(name, options, self.modelBuilder); - ds.createModel = function (name, properties, settings) { + ds.createModel = function(name, properties, settings) { settings = settings || {}; var BaseModel = settings.base || settings.super; if (!BaseModel) { @@ -270,7 +328,7 @@ registry.createDataSource = function (name, options) { return ModelCtor; }; - if(ds.settings && ds.settings.defaultForType) { + if (ds.settings && ds.settings.defaultForType) { this.setDefaultDataSourceForType(ds.settings.defaultForType, ds); } @@ -286,13 +344,13 @@ registry.createDataSource = function (name, options) { * @header loopback.memory([name]) */ -registry.memory = function (name) { +registry.memory = function(name) { name = name || 'default'; var memory = ( this._memoryDataSources || (this._memoryDataSources = {}) )[name]; - if(!memory) { + if (!memory) { memory = this._memoryDataSources[name] = this.createDataSource({ connector: 'memory' }); @@ -313,7 +371,7 @@ registry.memory = function (name) { registry.setDefaultDataSourceForType = function(type, dataSource) { var defaultDataSources = this.defaultDataSources; - if(!(dataSource instanceof DataSource)) { + if (!(dataSource instanceof DataSource)) { dataSource = this.createDataSource(dataSource); } @@ -346,19 +404,21 @@ registry.autoAttach = function() { var ModelCtor = models[modelName]; // Only auto attach if the model doesn't have an explicit data source - if(ModelCtor && (!(ModelCtor.dataSource instanceof DataSource))) { + if (ModelCtor && (!(ModelCtor.dataSource instanceof DataSource))) { this.autoAttachModel(ModelCtor); } }, this); }; registry.autoAttachModel = function(ModelCtor) { - if(ModelCtor.autoAttach) { + if (ModelCtor.autoAttach) { var ds = this.getDefaultDataSourceForType(ModelCtor.autoAttach); - assert(ds instanceof DataSource, 'cannot autoAttach model "' - + ModelCtor.modelName - + '". No dataSource found of type ' + ModelCtor.autoAttach); + assert( + ds instanceof DataSource, + 'cannot autoAttach model "' + ModelCtor.modelName + + '". No dataSource found of type ' + ModelCtor.autoAttach + ); ModelCtor.attachTo(ds); } diff --git a/lib/runtime.js b/lib/runtime.js index f179c533..7e791f5b 100644 --- a/lib/runtime.js +++ b/lib/runtime.js @@ -19,4 +19,3 @@ runtime.isBrowser = typeof window !== 'undefined'; */ runtime.isServer = !runtime.isBrowser; - diff --git a/lib/server-app.js b/lib/server-app.js new file mode 100644 index 00000000..b75835da --- /dev/null +++ b/lib/server-app.js @@ -0,0 +1,266 @@ +var assert = require('assert'); +var express = require('express'); +var merge = require('util')._extend; +var PhaseList = require('loopback-phase').PhaseList; +var debug = require('debug')('loopback:app'); +var pathToRegexp = require('path-to-regexp'); + +var proto = {}; + +module.exports = function loopbackExpress() { + var app = express(); + app.__expressLazyRouter = app.lazyrouter; + merge(app, proto); + return app; +}; + +/** + * Register a middleware using a factory function and a JSON config. + * + * **Example** + * + * ```js + * app.middlewareFromConfig(compression, { + * enabled: true, + * phase: 'initial', + * params: { + * threshold: 128 + * } + * }); + * ``` + * + * @param {function} factory The factory function creating a middleware handler. + * Typically a result of `require()` call, e.g. `require('compression')`. + * @options {Object} config The configuration. + * @property {String} phase The phase to register the middleware in. + * @property {Boolean} [enabled] Whether the middleware is enabled. + * Default: `true`. + * @property {Array|*} [params] The arguments to pass to the factory + * function. Either an array of arguments, + * or the value of the first argument when the factory expects + * a single argument only. + * @property {Array|string|RegExp} [paths] Optional list of paths limiting + * the scope of the middleware. + * + * @returns {object} this (fluent API) + * + * @header app.middlewareFromConfig(factory, config) + */ +proto.middlewareFromConfig = function(factory, config) { + assert(typeof factory === 'function', '"factory" must be a function'); + assert(typeof config === 'object', '"config" must be an object'); + assert(typeof config.phase === 'string' && config.phase, + '"config.phase" must be a non-empty string'); + + if (config.enabled === false) + return; + + var params = config.params; + if (params === undefined) { + params = []; + } else if (!Array.isArray(params)) { + params = [params]; + } + + var handler = factory.apply(null, params); + this.middleware(config.phase, config.paths || [], handler); + + return this; +}; + +/** + * Register (new) middleware phases. + * + * If all names are new, then the phases are added just before "routes" phase. + * Otherwise the provided list of names is merged with the existing phases + * in such way that the order of phases is preserved. + * + * **Examples** + * + * ```js + * // built-in phases: + * // initial, session, auth, parse, routes, files, final + * + * app.defineMiddlewarePhases('custom'); + * // new list of phases + * // initial, session, auth, parse, custom, routes, files, final + * + * app.defineMiddlewarePhases([ + * 'initial', 'postinit', 'preauth', 'routes', 'subapps' + * ]); + * // new list of phases + * // initial, postinit, preauth, session, auth, parse, custom, + * // routes, subapps, files, final + * ``` + * + * @param {string|Array.} nameOrArray A phase name or a list of phase + * names to add. + * + * @returns {object} this (fluent API) + * + * @header app.defineMiddlewarePhases(nameOrArray) + */ +proto.defineMiddlewarePhases = function(nameOrArray) { + this.lazyrouter(); + + if (Array.isArray(nameOrArray)) { + this._requestHandlingPhases.zipMerge(nameOrArray); + } else { + this._requestHandlingPhases.addBefore('routes', nameOrArray); + } + + return this; +}; + +/** + * Register a middleware handler to be executed in a given phase. + * @param {string} name The phase name, e.g. "init" or "routes". + * @param {Array|string|RegExp} [paths] Optional list of paths limiting + * the scope of the middleware. + * String paths are interpreted as expressjs path patterns, + * regular expressions are used as-is. + * @param {function} handler The middleware handler, one of + * `function(req, res, next)` or + * `function(err, req, res, next)` + * @returns {object} this (fluent API) + * + * @header app.middleware(name, handler) + */ +proto.middleware = function(name, paths, handler) { + this.lazyrouter(); + + if (handler === undefined && typeof paths === 'function') { + handler = paths; + paths = []; + } + + if (typeof paths === 'string' || paths instanceof RegExp) { + paths = [paths]; + } + + assert(typeof name === 'string' && name, '"name" must be a non-empty string'); + assert(typeof handler === 'function', '"handler" must be a function'); + assert(Array.isArray(paths), '"paths" must be an array'); + + var fullName = name; + var handlerName = handler.name || '(anonymous)'; + + var hook = 'use'; + var m = name.match(/^(.+):(before|after)$/); + if (m) { + name = m[1]; + hook = m[2]; + } + + var phase = this._requestHandlingPhases.find(name); + if (!phase) + throw new Error('Unknown middleware phase ' + name); + + var matches = createRequestMatcher(paths); + + var wrapper; + if (handler.length === 4) { + // handler is function(err, req, res, next) + debug('Add error handler %j to phase %j', handlerName, fullName); + + wrapper = function errorHandler(ctx, next) { + if (ctx.err && matches(ctx.req)) { + var err = ctx.err; + ctx.err = undefined; + handler(err, ctx.req, ctx.res, storeErrorAndContinue(ctx, next)); + } else { + next(); + } + }; + } else { + // handler is function(req, res, next) + debug('Add middleware %j to phase %j', handlerName , fullName); + wrapper = function regularHandler(ctx, next) { + if (ctx.err || !matches(ctx.req)) { + next(); + } else { + handler(ctx.req, ctx.res, storeErrorAndContinue(ctx, next)); + } + }; + } + + phase[hook](wrapper); + return this; +}; + +function createRequestMatcher(paths) { + if (!paths.length) { + return function requestMatcher(req) { return true; }; + } + + var checks = paths.map(function(p) { + return pathToRegexp(p, { + sensitive: true, + strict: false, + end: false + }); + }); + + return function requestMatcher(req) { + return checks.some(function(regex) { + return regex.test(req.url); + }); + }; +} + +function storeErrorAndContinue(ctx, next) { + return function(err) { + if (err) ctx.err = err; + next(); + }; +} + +// Install our custom PhaseList-based handler into the app +proto.lazyrouter = function() { + var self = this; + if (self._router) return; + + self.__expressLazyRouter(); + + // Storing the fn in another property of the router object + // allows us to call the method with the router as `this` + // without the need to use slow `call` or `apply`. + self._router.__expressHandle = self._router.handle; + + self._requestHandlingPhases = new PhaseList(); + self._requestHandlingPhases.add([ + 'initial', 'session', 'auth', 'parse', + 'routes', 'files', 'final' + ]); + + // In order to pass error into express router, we have + // to pass it to a middleware executed from within the router. + // This is achieved by adding a phase-handler that wraps the error + // into `req` object and then a router-handler that unwraps the error + // and calls `next(err)`. + // It is important to register these two handlers at the very beginning, + // before any other handlers are added. + self.middleware('routes', function wrapError(err, req, res, next) { + req.__err = err; + next(); + }); + + self.use(function unwrapError(req, res, next) { + var err = req.__err; + req.__err = undefined; + next(err); + }); + + self.middleware('routes', function runRootHandlers(req, res, next) { + self._router.__expressHandle(req, res, next); + }); + + // Overwrite the original handle() function provided by express, + // replace it with our implementation based on PhaseList + self._router.handle = function(req, res, next) { + var ctx = { req: req, res: res }; + self._requestHandlingPhases.run(ctx, function(err) { + next(err || ctx.err); + }); + }; +}; diff --git a/package.json b/package.json index 0cf692b2..ae7c7005 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.7.0", + "version": "2.8.0", "description": "LoopBack: Open Source Framework for Node.js", "homepage": "http://loopback.io", "keywords": [ @@ -36,13 +36,17 @@ "bcryptjs": "~2.0.2", "body-parser": "~1.8.1", "canonical-json": "0.0.4", + "continuation-local-storage": "~3.1.1", "debug": "~2.0.0", "ejs": "~1.0.0", - "express": "4.x", + "express": "^4.10.2", "inflection": "~1.4.2", "loopback-connector-remote": "^1.0.1", + "loopback-phase": "^1.0.1", "nodemailer": "~1.3.0", "nodemailer-stub-transport": "~0.1.4", + "path-to-regexp": "^1.0.1", + "serve-favicon": "^2.1.6", "strong-remoting": "^2.4.0", "uid2": "0.0.3", "underscore": "~1.7.0", @@ -53,18 +57,20 @@ }, "devDependencies": { "browserify": "~4.2.3", - "chai": "~1.9.1", + "chai": "^1.10.0", "cookie-parser": "~1.3.3", "errorhandler": "~1.2.0", "es5-shim": "^4.0.3", - "grunt": "~0.4.5", + "grunt": "^0.4.5", "grunt-browserify": "~3.0.1", "grunt-cli": "^0.1.13", "grunt-contrib-jshint": "~0.10.0", "grunt-contrib-uglify": "~0.5.1", "grunt-contrib-watch": "~0.6.1", + "grunt-jscs": "^0.8.1", "grunt-karma": "~0.9.0", "grunt-mocha-test": "^0.11.0", + "karma": "~0.12.23", "karma-browserify": "~0.2.1", "karma-chrome-launcher": "~0.1.4", "karma-firefox-launcher": "~0.1.3", @@ -79,8 +85,7 @@ "mocha": "~1.21.4", "serve-favicon": "~2.1.3", "strong-task-emitter": "0.0.x", - "supertest": "~0.13.0", - "karma": "~0.12.23" + "supertest": "~0.13.0" }, "repository": { "type": "git", @@ -88,6 +93,7 @@ }, "browser": { "express": "./lib/browser-express.js", + "./lib/server-app.js": "./lib/browser-express.js", "connect": false, "nodemailer": false }, diff --git a/server/middleware/context.js b/server/middleware/context.js new file mode 100644 index 00000000..c86903a0 --- /dev/null +++ b/server/middleware/context.js @@ -0,0 +1,118 @@ +var loopback = require('../../lib/loopback'); +var juggler = require('loopback-datasource-juggler'); +var remoting = require('strong-remoting'); +var cls = require('continuation-local-storage'); + +module.exports = context; + +var name = 'loopback'; + +function createContext(scope) { + // Make the namespace globally visible via the process.context property + process.context = process.context || {}; + var ns = process.context[scope]; + if (!ns) { + ns = cls.createNamespace(scope); + process.context[scope] = ns; + // Set up loopback.getCurrentContext() + loopback.getCurrentContext = function() { + return ns && ns.active ? ns : null; + }; + + chain(juggler); + chain(remoting); + } + return ns; +} + +/** + * Context middleware. + * ```js + * var app = loopback(); + * app.use(loopback.context(options); + * app.use(loopback.rest()); + * app.listen(); + * ``` + * @options {Object} [options] Options for context + * @property {String} name Context scope name. + * @property {Boolean} enableHttpContext Whether HTTP context is enabled. Default is false. + * @header loopback.context([options]) + */ + +function context(options) { + options = options || {}; + var scope = options.name || name; + var enableHttpContext = options.enableHttpContext || false; + var ns = createContext(scope); + // Return the middleware + return function contextHandler(req, res, next) { + if (req.loopbackContext) { + return next(); + } + req.loopbackContext = ns; + // Bind req/res event emitters to the given namespace + ns.bindEmitter(req); + ns.bindEmitter(res); + // Create namespace for the request context + ns.run(function processRequestInContext(context) { + // Run the code in the context of the namespace + if (enableHttpContext) { + ns.set('http', {req: req, res: res}); // Set up the transport context + } + next(); + }); + }; +} + +/** + * Create a chained context + * @param {Object} child The child context + * @param {Object} parent The parent context + * @private + * @constructor + */ +function ChainedContext(child, parent) { + this.child = child; + this.parent = parent; +} + +/*! + * Get the value by name from the context. If it doesn't exist in the child + * context, try the parent one + * @param {String} name Name of the context property + * @returns {*} Value of the context property + */ +ChainedContext.prototype.get = function(name) { + var val = this.child && this.child.get(name); + if (val === undefined) { + return this.parent && this.parent.get(name); + } +}; + +ChainedContext.prototype.set = function(name, val) { + if (this.child) { + return this.child.set(name, val); + } else { + return this.parent && this.parent.set(name, val); + } +}; + +ChainedContext.prototype.reset = function(name, val) { + if (this.child) { + return this.child.reset(name, val); + } else { + return this.parent && this.parent.reset(name, val); + } +}; + +function chain(child) { + if (typeof child.getCurrentContext === 'function') { + var childContext = new ChainedContext(child.getCurrentContext(), + loopback.getCurrentContext()); + child.getCurrentContext = function() { + return childContext; + }; + } else { + child.getCurrentContext = loopback.getCurrentContext; + } +} diff --git a/server/middleware/favicon.js b/server/middleware/favicon.js new file mode 100644 index 00000000..84ff0e28 --- /dev/null +++ b/server/middleware/favicon.js @@ -0,0 +1 @@ +module.exports = require('../../lib/express-middleware').favicon; diff --git a/lib/middleware/rest.js b/server/middleware/rest.js similarity index 50% rename from lib/middleware/rest.js rename to server/middleware/rest.js index 5e91c7f4..cfe9de77 100644 --- a/lib/middleware/rest.js +++ b/server/middleware/rest.js @@ -2,7 +2,8 @@ * Module dependencies. */ -var loopback = require('../loopback'); +var loopback = require('../../lib/loopback'); +var async = require('async'); /*! * Export the middleware. @@ -12,7 +13,7 @@ module.exports = rest; /** * Expose models over REST. - * + * * For example: * ```js * app.use(loopback.rest()); @@ -22,17 +23,30 @@ module.exports = rest; */ function rest() { - var tokenParser = null; - return function (req, res, next) { + return function restApiHandler(req, res, next) { var app = req.app; - var handler = app.handler('rest'); + var restHandler = app.handler('rest'); - if(req.url === '/routes') { - res.send(handler.adapter.allRoutes()); - } else if(req.url === '/models') { + if (req.url === '/routes') { + return res.send(restHandler.adapter.allRoutes()); + } else if (req.url === '/models') { return res.send(app.remotes().toJSON()); - } else if (app.isAuthEnabled) { - if (!tokenParser) { + } + + var preHandlers; + + if (!preHandlers) { + preHandlers = []; + var remotingOptions = app.get('remoting') || {}; + + var contextOptions = remotingOptions.context; + if (contextOptions !== false) { + if (typeof contextOptions !== 'object') + contextOptions = {}; + preHandlers.push(loopback.context(contextOptions)); + } + + if (app.isAuthEnabled) { // NOTE(bajtos) It would be better to search app.models for a model // of type AccessToken instead of searching all loopback models. // Unfortunately that's not supported now. @@ -40,19 +54,12 @@ function rest() { // https://github.com/strongloop/loopback/pull/167 // https://github.com/strongloop/loopback/commit/f07446a var AccessToken = loopback.getModelByType(loopback.AccessToken); - tokenParser = loopback.token({ model: AccessToken }); + preHandlers.push(loopback.token({ model: AccessToken })); } - - tokenParser(req, res, function(err) { - if (err) { - next(err); - } else { - handler(req, res, next); - } - }); - } else { - handler(req, res, next); } + + async.eachSeries(preHandlers.concat(restHandler), function(handler, done) { + handler(req, res, done); + }, next); }; } - diff --git a/server/middleware/static.js b/server/middleware/static.js new file mode 100644 index 00000000..c01a538d --- /dev/null +++ b/server/middleware/static.js @@ -0,0 +1,11 @@ +/** + * Serve static assets of a LoopBack application. + * + * @param {string} root The root directory from which the static assets are to + * be served. + * @param {object} options Refer to + * [express documentation](http://expressjs.com/4x/api.html#express.static) + * for the full list of available options. + * @header loopback.static(root, [options]) + */ +module.exports = require('express').static; diff --git a/lib/middleware/status.js b/server/middleware/status.js similarity index 99% rename from lib/middleware/status.js rename to server/middleware/status.js index c90f5efc..3e930811 100644 --- a/lib/middleware/status.js +++ b/server/middleware/status.js @@ -27,4 +27,3 @@ function status() { }); }; } - diff --git a/lib/middleware/token.js b/server/middleware/token.js similarity index 90% rename from lib/middleware/token.js rename to server/middleware/token.js index 659c9550..72eb340e 100644 --- a/lib/middleware/token.js +++ b/server/middleware/token.js @@ -2,7 +2,7 @@ * Module dependencies. */ -var loopback = require('../loopback'); +var loopback = require('../../lib/loopback'); var assert = require('assert'); /*! @@ -48,12 +48,13 @@ function token(options) { var TokenModel = options.model || loopback.AccessToken; assert(TokenModel, 'loopback.token() middleware requires a AccessToken model'); - return function (req, res, next) { + return function(req, res, next) { if (req.accessToken !== undefined) return next(); TokenModel.findForRequest(req, options, function(err, token) { req.accessToken = token || null; + var ctx = loopback.getCurrentContext(); + if (ctx) ctx.set('accessToken', token); next(err); }); }; } - diff --git a/lib/middleware/urlNotFound.js b/server/middleware/url-not-found.js similarity index 100% rename from lib/middleware/urlNotFound.js rename to server/middleware/url-not-found.js diff --git a/test/access-token.test.js b/test/access-token.test.js index 093e4dc9..14905b75 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -1,4 +1,5 @@ var loopback = require('../'); +var extend = require('util')._extend; var Token = loopback.AccessToken.extend('MyToken'); var ACL = loopback.ACL; @@ -106,6 +107,38 @@ describe('AccessToken', function () { done(); }); }); + + describe('.findForRequest()', function() { + beforeEach(createTestingToken); + + it('supports two-arg variant with no options', function(done) { + var expectedTokenId = this.token.id; + var req = mockRequest({ + headers: { 'authorization': expectedTokenId } + }); + + Token.findForRequest(req, function(err, token) { + if (err) return done(err); + expect(token.id).to.eql(expectedTokenId); + done(); + }); + }); + + function mockRequest(opts) { + return extend( + { + method: 'GET', + url: '/a-test-path', + headers: {}, + _params: {}, + + // express helpers + param: function(name) { return this._params[name]; }, + header: function(name) { return this.headers[name]; } + }, + opts); + } + }); }); describe('app.enableAuth()', function() { @@ -143,6 +176,36 @@ describe('app.enableAuth()', function() { .end(done); }); + it('stores token in the context', function(done) { + var TestModel = loopback.createModel('TestModel', { base: 'Model' }); + TestModel.getToken = function(cb) { + cb(null, loopback.getCurrentContext().get('accessToken') || null); + }; + TestModel.remoteMethod('getToken', { + returns: { arg: 'token', type: 'object' }, + http: { verb: 'GET', path: '/token' } + }); + + var app = loopback(); + app.model(TestModel, { dataSource: null }); + + app.enableAuth(); + app.use(loopback.context()); + app.use(loopback.token({ model: Token })); + app.use(loopback.rest()); + + var token = this.token; + request(app) + .get('/TestModels/token?_format=json') + .set('authorization', token.id) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if (err) return done(err); + expect(res.body.token.id).to.eql(token.id); + done(); + }); + }); }); function createTestingToken(done) { diff --git a/test/app.test.js b/test/app.test.js index 80a307ba..073bf149 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -1,5 +1,7 @@ +var async = require('async'); var path = require('path'); -var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app'); + +var http = require('http'); var loopback = require('../'); var PersistedModel = loopback.PersistedModel; @@ -7,6 +9,300 @@ var describe = require('./util/describe'); var it = require('./util/it'); describe('app', function() { + describe.onServer('.middleware(phase, handler)', function() { + var app; + var steps; + + beforeEach(function setup() { + app = loopback(); + steps = []; + }); + + it('runs middleware in phases', function(done) { + var PHASES = [ + 'initial', 'session', 'auth', 'parse', + 'routes', 'files', 'final' + ]; + + PHASES.forEach(function(name) { + app.middleware(name, namedHandler(name)); + }); + app.use(namedHandler('main')); + + executeMiddlewareHandlers(app, function(err) { + if (err) return done(err); + expect(steps).to.eql([ + 'initial', 'session', 'auth', 'parse', + 'main', 'routes', 'files', 'final' + ]); + done(); + }); + }); + + it('supports "before:" and "after:" prefixes', function(done) { + app.middleware('routes:before', namedHandler('routes:before')); + app.middleware('routes:after', namedHandler('routes:after')); + app.use(namedHandler('main')); + + executeMiddlewareHandlers(app, function(err) { + if (err) return done(err); + expect(steps).to.eql(['routes:before', 'main', 'routes:after']); + done(); + }); + }); + + it('injects error from previous phases into the router', function(done) { + var expectedError = new Error('expected error'); + + app.middleware('initial', function(req, res, next) { + steps.push('initial'); + next(expectedError); + }); + + // legacy solution for error handling + app.use(function errorHandler(err, req, res, next) { + expect(err).to.equal(expectedError); + steps.push('error'); + next(); + }); + + executeMiddlewareHandlers(app, function(err) { + if (err) return done(err); + expect(steps).to.eql(['initial', 'error']); + done(); + }); + }); + + it('passes unhandled error to callback', function(done) { + var expectedError = new Error('expected error'); + + app.middleware('initial', function(req, res, next) { + next(expectedError); + }); + + executeMiddlewareHandlers(app, function(err) { + expect(err).to.equal(expectedError); + done(); + }); + }); + + it('passes errors to error handlers in the same phase', function(done) { + var expectedError = new Error('this should be handled by middleware'); + var handledError; + + app.middleware('initial', function(req, res, next) { + // continue in the next tick, this verifies that the next + // handler waits until the previous one is done + process.nextTick(function() { + next(expectedError); + }); + }); + + app.middleware('initial', function(err, req, res, next) { + handledError = err; + next(); + }); + + executeMiddlewareHandlers(app, function(err) { + if (err) return done(err); + expect(handledError).to.equal(expectedError); + done(); + }); + }); + + it('scopes middleware to a string path', function(done) { + app.middleware('initial', '/scope', pathSavingHandler()); + + async.eachSeries( + ['/', '/scope', '/scope/item', '/other'], + function(url, next) { executeMiddlewareHandlers(app, url, next); }, + function(err) { + if (err) return done(err); + expect(steps).to.eql(['/scope', '/scope/item']); + done(); + }); + }); + + it('scopes middleware to a regex path', function(done) { + app.middleware('initial', /^\/(a|b)/, pathSavingHandler()); + + async.eachSeries( + ['/', '/a', '/b', '/c'], + function(url, next) { executeMiddlewareHandlers(app, url, next); }, + function(err) { + if (err) return done(err); + expect(steps).to.eql(['/a', '/b']); + done(); + }); + }); + + it('scopes middleware to a list of scopes', function(done) { + app.middleware('initial', ['/scope', /^\/(a|b)/], pathSavingHandler()); + + async.eachSeries( + ['/', '/a', '/b', '/c', '/scope', '/other'], + function(url, next) { executeMiddlewareHandlers(app, url, next); }, + function(err) { + if (err) return done(err); + expect(steps).to.eql(['/a', '/b', '/scope']); + done(); + }); + }); + + function namedHandler(name) { + return function(req, res, next) { + steps.push(name); + next(); + }; + } + + function pathSavingHandler() { + return function(req, res, next) { + steps.push(req.url); + next(); + }; + } + }); + + describe.onServer('.middlewareFromConfig', function() { + it('provides API for loading middleware from JSON config', function(done) { + var steps = []; + var expectedConfig = { key: 'value' }; + + var handlerFactory = function() { + var args = Array.prototype.slice.apply(arguments); + return function(req, res, next) { + steps.push(args); + next(); + }; + }; + + // Config as an object (single arg) + app.middlewareFromConfig(handlerFactory, { + enabled: true, + phase: 'session', + params: expectedConfig + }); + + // Config as a value (single arg) + app.middlewareFromConfig(handlerFactory, { + enabled: true, + phase: 'session:before', + params: 'before' + }); + + // Config as a list of args + app.middlewareFromConfig(handlerFactory, { + enabled: true, + phase: 'session:after', + params: ['after', 2] + }); + + // Disabled by configuration + app.middlewareFromConfig(handlerFactory, { + enabled: false, + phase: 'initial', + params: null + }); + + executeMiddlewareHandlers(app, function(err) { + if (err) return done(err); + expect(steps).to.eql([ + ['before'], + [expectedConfig], + ['after', 2] + ]); + done(); + }); + }); + + it('scopes middleware to a list of scopes', function(done) { + var steps = []; + app.middlewareFromConfig( + function factory() { + return function(req, res, next) { + steps.push(req.url); + next(); + }; + }, + { + phase: 'initial', + paths: ['/scope', /^\/(a|b)/] + }); + + async.eachSeries( + ['/', '/a', '/b', '/c', '/scope', '/other'], + function(url, next) { executeMiddlewareHandlers(app, url, next); }, + function(err) { + if (err) return done(err); + expect(steps).to.eql(['/a', '/b', '/scope']); + done(); + }); + }); + }); + + describe.onServer('.defineMiddlewarePhases(nameOrArray)', function() { + var app; + beforeEach(function() { + app = loopback(); + }); + + it('adds the phase just before "routes" by default', function(done) { + app.defineMiddlewarePhases('custom'); + verifyMiddlewarePhases(['custom', 'routes'], done); + }); + + it('merges phases adding to the start of the list', function(done) { + app.defineMiddlewarePhases(['first', 'routes', 'subapps']); + verifyMiddlewarePhases([ + 'first', + 'initial', // this was the original first phase + 'routes', + 'subapps' + ], done); + }); + + it('merges phases preserving the order', function(done) { + app.defineMiddlewarePhases([ + 'initial', + 'postinit', 'preauth', // add + 'auth', 'routes', + 'subapps', // add + 'final', + 'last' // add + ]); + verifyMiddlewarePhases([ + 'initial', + 'postinit', 'preauth', // new + 'auth', 'routes', + 'subapps', // new + 'files', 'final', + 'last' // new + ], done); + }); + + it('throws helpful error on ordering conflict', function() { + app.defineMiddlewarePhases(['first', 'second']); + expect(function() { app.defineMiddlewarePhases(['second', 'first']); }) + .to.throw(/ordering conflict.*first.*second/); + }); + + function verifyMiddlewarePhases(names, done) { + var steps = []; + names.forEach(function(it) { + app.middleware(it, function(req, res, next) { + steps.push(it); + next(); + }); + }); + + executeMiddlewareHandlers(app, function(err) { + if (err) return done(err); + expect(steps).to.eql(names); + done(); + }); + } + }); describe('app.model(Model)', function() { var app, db; @@ -281,7 +577,7 @@ describe('app', function() { var elapsed = Date.now() - Number(new Date(res.body.started)); // elapsed should be a positive number... - assert(elapsed > 0); + assert(elapsed >= 0); // less than 100 milliseconds assert(elapsed < 100); @@ -374,3 +670,20 @@ describe('app', function() { }); }); }); + +function executeMiddlewareHandlers(app, urlPath, callback) { + var server = http.createServer(function(req, res) { + app.handle(req, res, callback); + }); + + if (callback === undefined && typeof urlPath === 'function') { + callback = urlPath; + urlPath = '/test/url'; + } + + request(server) + .get(urlPath) + .end(function(err) { + if (err) return callback(err); + }); +} diff --git a/test/loopback.test.js b/test/loopback.test.js index d03e01e6..1bd996ed 100644 --- a/test/loopback.test.js +++ b/test/loopback.test.js @@ -1,3 +1,5 @@ +var it = require('./util/it'); + describe('loopback', function() { var nameCounter = 0; var uniqueModelName; @@ -11,6 +13,17 @@ describe('loopback', function() { expect(loopback.ValidationError).to.be.a('function') .and.have.property('name', 'ValidationError'); }); + + it.onServer('includes `faviconFile`', function() { + var file = loopback.faviconFile; + expect(file, 'faviconFile').to.not.equal(undefined); + expect(require('fs').existsSync(loopback.faviconFile), 'file exists') + .to.equal(true); + }); + + it.onServer('has `getCurrentContext` method', function() { + expect(loopback.getCurrentContext).to.be.a('function'); + }); }); describe('loopback.createDataSource(options)', function() { @@ -68,11 +81,11 @@ describe('loopback', function() { describe('loopback.remoteMethod(Model, fn, [options]);', function() { it("Setup a remote method.", function() { var Product = loopback.createModel('product', {price: Number}); - + Product.stats = function(fn) { // ... } - + loopback.remoteMethod( Product.stats, { @@ -80,7 +93,7 @@ describe('loopback', function() { http: {path: '/info', verb: 'get'} } ); - + assert.equal(Product.stats.returns.arg, 'stats'); assert.equal(Product.stats.returns.type, 'array'); assert.equal(Product.stats.http.path, '/info'); @@ -240,9 +253,119 @@ describe('loopback', function() { expect(owner, 'model.prototype.owner').to.be.a('function'); expect(owner._targetClass).to.equal('User'); }); + + it('adds new acls', function() { + var model = loopback.Model.extend(uniqueModelName, {}, { + acls: [ + { + property: 'find', + accessType: 'EXECUTE', + principalType: 'ROLE', + principalId: '$everyone', + permission: 'DENY' + } + ] + }); + + loopback.configureModel(model, { + dataSource: null, + acls: [ + { + property: 'find', + accessType: 'EXECUTE', + principalType: 'ROLE', + principalId: 'admin', + permission: 'ALLOW' + } + ] + }); + + expect(model.settings.acls).eql([ + { + property: 'find', + accessType: 'EXECUTE', + principalType: 'ROLE', + principalId: '$everyone', + permission: 'DENY' + }, + { + property: 'find', + accessType: 'EXECUTE', + principalType: 'ROLE', + principalId: 'admin', + permission: 'ALLOW' + } + ]); + }); + + it('updates existing acls', function() { + var model = loopback.Model.extend(uniqueModelName, {}, { + acls: [ + { + property: 'find', + accessType: 'EXECUTE', + principalType: 'ROLE', + principalId: '$everyone', + permission: 'DENY' + } + ] + }); + + loopback.configureModel(model, { + dataSource: null, + acls: [ + { + property: 'find', + accessType: 'EXECUTE', + principalType: 'ROLE', + principalId: '$everyone', + permission: 'ALLOW' + } + ] + }); + + expect(model.settings.acls).eql([ + { + property: 'find', + accessType: 'EXECUTE', + principalType: 'ROLE', + principalId: '$everyone', + permission: 'ALLOW' + } + ]); + }); + + it('updates existing settings', function() { + var model = loopback.Model.extend(uniqueModelName, {}, { + ttl: 10, + emailVerificationRequired: false + }); + + loopback.configureModel(model, { + dataSource: null, + options: { + ttl: 20, + realmRequired: true, + base: 'X' + } + }); + + expect(model.settings).to.have.property('ttl', 20); + expect(model.settings).to.have.property('emailVerificationRequired', + false); + expect(model.settings).to.have.property('realmRequired', true); + expect(model.settings).to.not.have.property('base'); + }); }); describe('loopback object', function() { + it('inherits properties from express', function() { + var express = require('express'); + for (var i in express) { + expect(loopback).to.have.property(i, express[i]); + } + }); + it('exports all built-in models', function() { var expectedModelNames = [ 'Email', diff --git a/test/relations.integration.js b/test/relations.integration.js index 306f949f..04460223 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -411,8 +411,8 @@ describe('relations - integration', function () { }); lt.describe.whenCalledRemotely('DELETE', '/api/physicians/:id/patients/rel/:fk', function () { - it('should succeed with statusCode 200', function () { - assert.equal(this.res.statusCode, 200); + it('should succeed with statusCode 204', function () { + assert.equal(this.res.statusCode, 204); }); it('should remove the record in appointment', function (done) { @@ -469,8 +469,8 @@ describe('relations - integration', function () { }); lt.describe.whenCalledRemotely('DELETE', '/api/physicians/:id/patients/:fk', function () { - it('should succeed with statusCode 200', function () { - assert.equal(this.res.statusCode, 200); + it('should succeed with statusCode 204', function () { + assert.equal(this.res.statusCode, 204); }); it('should remove the record in appointment', function (done) { diff --git a/test/remoting.integration.js b/test/remoting.integration.js index aa017079..0ba0e90f 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -66,33 +66,176 @@ describe('remoting - integration', function () { }); }); - describe('Model', function() { - it('has expected remote methods', function() { - var storeClass = app.handler('rest').adapter + describe('Model shared classes', function() { + function formatReturns(m) { + var returns = m.returns; + if (!returns || returns.length === 0) { + return ''; + } + var type = returns[0].type; + return type ? ':' + type : ''; + } + + function formatMethod(m) { + return [ + m.name, + '(', + m.accepts.map(function(a) { + return a.arg + ':' + a.type + }).join(','), + ')', + formatReturns(m), + ' ', + m.getHttpMethod(), + ' ', + m.getFullPath() + ].join(''); + } + + function findClass(name) { + return app.handler('rest').adapter .getClasses() - .filter(function(c) { return c.name === 'store'; })[0]; + .filter(function(c) { + return c.name === name; + })[0]; + } + + it('has expected remote methods', function() { + var storeClass = findClass('store'); var methods = storeClass.methods + .filter(function(m) { + return m.name.indexOf('__') === -1; + }) .map(function(m) { - return [ - m.name + '()', - m.getHttpMethod(), - m.getFullPath() - ].join(' '); + return formatMethod(m); }); + var expectedMethods = [ + 'create(data:object):store POST /stores', + 'upsert(data:object):store PUT /stores', + 'exists(id:any):boolean GET /stores/:id/exists', + 'findById(id:any):store GET /stores/:id', + 'find(filter:object):store GET /stores', + 'findOne(filter:object):store GET /stores/findOne', + 'updateAll(where:object,data:object) POST /stores/update', + 'deleteById(id:any) DELETE /stores/:id', + 'count(where:object):number GET /stores/count', + 'prototype.updateAttributes(data:object):store PUT /stores/:id' + ]; + // The list of methods is from docs: // http://docs.strongloop.com/display/LB/Exposing+models+over+a+REST+API - expect(methods).to.include.members([ - 'create() POST /stores', - 'upsert() PUT /stores', - 'exists() GET /stores/:id/exists', - 'findById() GET /stores/:id', - 'find() GET /stores', - 'findOne() GET /stores/findOne', - 'deleteById() DELETE /stores/:id', - 'count() GET /stores/count', - 'prototype.updateAttributes() PUT /stores/:id', - ]); + expect(methods).to.include.members(expectedMethods); + }); + + it('has expected remote methods for scopes', function() { + var storeClass = findClass('store'); + var methods = storeClass.methods + .filter(function(m) { + return m.name.indexOf('__') === 0; + }) + .map(function(m) { + return formatMethod(m); + }); + + var expectedMethods = [ + '__get__superStores(filter:object):store GET /stores/superStores', + '__create__superStores(data:store):store POST /stores/superStores', + '__delete__superStores() DELETE /stores/superStores', + '__count__superStores(where:object):number GET /stores/superStores/count' + ]; + + expect(methods).to.include.members(expectedMethods); + }); + + + it('should have correct signatures for belongsTo methods', + function() { + + var widgetClass = findClass('widget'); + var methods = widgetClass.methods + .filter(function(m) { + return m.name.indexOf('prototype.__') === 0; + }) + .map(function(m) { + return formatMethod(m); + }); + + var expectedMethods = [ + 'prototype.__get__store(refresh:boolean):store ' + + 'GET /widgets/:id/store' + ]; + expect(methods).to.include.members(expectedMethods); + }); + + + it('should have correct signatures for hasMany methods', + function() { + + var physicianClass = findClass('store'); + var methods = physicianClass.methods + .filter(function(m) { + return m.name.indexOf('prototype.__') === 0; + }) + .map(function(m) { + return formatMethod(m); + }); + + var expectedMethods = [ + 'prototype.__findById__widgets(fk:any):widget ' + + 'GET /stores/:id/widgets/:fk', + 'prototype.__destroyById__widgets(fk:any) ' + + 'DELETE /stores/:id/widgets/:fk', + 'prototype.__updateById__widgets(fk:any,data:widget):widget ' + + 'PUT /stores/:id/widgets/:fk', + 'prototype.__get__widgets(filter:object):widget ' + + 'GET /stores/:id/widgets', + 'prototype.__create__widgets(data:widget):widget ' + + 'POST /stores/:id/widgets', + 'prototype.__delete__widgets() ' + + 'DELETE /stores/:id/widgets', + 'prototype.__count__widgets(where:object):number ' + + 'GET /stores/:id/widgets/count' + ]; + expect(methods).to.include.members(expectedMethods); + }); + + it('should have correct signatures for hasMany-through methods', + function() { + + var physicianClass = findClass('physician'); + var methods = physicianClass.methods + .filter(function(m) { + return m.name.indexOf('prototype.__') === 0; + }) + .map(function(m) { + return formatMethod(m); + }); + + var expectedMethods = [ + 'prototype.__findById__patients(fk:any):patient ' + + 'GET /physicians/:id/patients/:fk', + 'prototype.__destroyById__patients(fk:any) ' + + 'DELETE /physicians/:id/patients/:fk', + 'prototype.__updateById__patients(fk:any,data:patient):patient ' + + 'PUT /physicians/:id/patients/:fk', + 'prototype.__link__patients(fk:any,data:appointment):appointment ' + + 'PUT /physicians/:id/patients/rel/:fk', + 'prototype.__unlink__patients(fk:any) ' + + 'DELETE /physicians/:id/patients/rel/:fk', + 'prototype.__exists__patients(fk:any):boolean ' + + 'HEAD /physicians/:id/patients/rel/:fk', + 'prototype.__get__patients(filter:object):patient ' + + 'GET /physicians/:id/patients', + 'prototype.__create__patients(data:patient):patient ' + + 'POST /physicians/:id/patients', + 'prototype.__delete__patients() ' + + 'DELETE /physicians/:id/patients', + 'prototype.__count__patients(where:object):number ' + + 'GET /physicians/:id/patients/count' + ]; + expect(methods).to.include.members(expectedMethods); }); }); + }); diff --git a/test/rest.middleware.test.js b/test/rest.middleware.test.js index 5b10302f..89877af7 100644 --- a/test/rest.middleware.test.js +++ b/test/rest.middleware.test.js @@ -75,6 +75,22 @@ describe('loopback.rest', function() { }); }); + it('should honour `remoting.rest.supportedTypes`', function(done) { + var app = loopback(); + + // NOTE it is crucial to set `remoting` before creating any models + var supportedTypes = ['json', 'application/javascript', 'text/javascript']; + app.set('remoting', { rest: { supportedTypes: supportedTypes } }); + + app.model(MyModel); + app.use(loopback.rest()); + + request(app).get('/mymodels') + .set('Accept', 'text/html,application/xml;q=0.9,*/*;q=0.8') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, done); + }); + it('includes loopback.token when necessary', function(done) { givenUserModelWithAuth(); app.enableAuth(); @@ -114,6 +130,107 @@ describe('loopback.rest', function() { }); }); + describe('context propagation', function() { + var User; + + beforeEach(function() { + User = givenUserModelWithAuth(); + User.getToken = function(cb) { + var context = loopback.getCurrentContext(); + var req = context.get('http').req; + expect(req).to.have.property('accessToken'); + + var juggler = require('loopback-datasource-juggler'); + expect(juggler.getCurrentContext().get('http').req) + .to.have.property('accessToken'); + + var remoting = require('strong-remoting'); + expect(remoting.getCurrentContext().get('http').req) + .to.have.property('accessToken'); + + cb(null, req && req.accessToken ? req.accessToken.id : null); + }; + // Set up the ACL + User.settings.acls.push({principalType: 'ROLE', + principalId: '$authenticated', permission: 'ALLOW', + property: 'getToken'}); + + loopback.remoteMethod(User.getToken, { + accepts: [], + returns: [ + { type: 'object', name: 'id' } + ] + }); + }); + + function invokeGetToken(done) { + givenLoggedInUser(function(err, token) { + if (err) return done(err); + request(app).get('/users/getToken') + .set('Authorization', token.id) + .expect(200) + .end(function(err, res) { + if (err) return done(err); + expect(res.body.id).to.equal(token.id); + done(); + }); + }); + } + + it('should enable context using loopback.context', function(done) { + app.use(loopback.context({ enableHttpContext: true })); + app.enableAuth(); + app.use(loopback.rest()); + + invokeGetToken(done); + }); + + it('should enable context with loopback.rest', function(done) { + app.enableAuth(); + app.set('remoting', { context: { enableHttpContext: true } }); + app.use(loopback.rest()); + + invokeGetToken(done); + }); + + it('should support explicit context', function(done) { + app.enableAuth(); + app.use(loopback.context()); + app.use(loopback.token( + { model: loopback.getModelByType(loopback.AccessToken) })); + app.use(function(req, res, next) { + loopback.getCurrentContext().set('accessToken', req.accessToken); + next(); + }); + app.use(loopback.rest()); + + User.getToken = function(cb) { + var context = loopback.getCurrentContext(); + var accessToken = context.get('accessToken'); + expect(context.get('accessToken')).to.have.property('id'); + + var juggler = require('loopback-datasource-juggler'); + context = juggler.getCurrentContext(); + expect(context.get('accessToken')).to.have.property('id'); + + var remoting = require('strong-remoting'); + context = remoting.getCurrentContext(); + expect(context.get('accessToken')).to.have.property('id'); + + cb(null, accessToken ? accessToken.id : null); + }; + + loopback.remoteMethod(User.getToken, { + accepts: [], + returns: [ + { type: 'object', name: 'id' } + ] + }); + + invokeGetToken(done); + }); + }); + function givenUserModelWithAuth() { // NOTE(bajtos) It is important to create a custom AccessToken model here, // in order to overwrite the entry created by previous tests in