diff --git a/README.md b/README.md
index d049198c..3f7bb033 100644
--- a/README.md
+++ b/README.md
@@ -41,9 +41,9 @@ as illustrated below:
* Clients
* [loopback-ios](https://github.com/strongloop/loopback-ios)
- * [loopback-remoting-ios](https://github.com/strongloop/loopback-remoting-ios)
+ * [strong-remoting-ios](https://github.com/strongloop/strong-remoting-ios)
* [loopback-android](https://github.com/strongloop/loopback-android)
- * [loopback-remoting-android](https://github.com/strongloop/loopback-remoting-android)
+ * [strong-remoting-android](https://github.com/strongloop/strong-remoting-android)
* [loopback-angular](https://github.com/strongloop/loopback-angular)
* Tools
diff --git a/docs.json b/docs.json
index 5822b153..c79bfa0d 100644
--- a/docs.json
+++ b/docs.json
@@ -5,21 +5,22 @@
"lib/loopback.js",
"lib/runtime.js",
"lib/registry.js",
- "lib/middleware/token.js",
+{ "title": "Base models", "depth": 2 },
+ "lib/models/model.js",
+ "lib/models/persisted-model.js",
+{ "title": "Middleware", "depth": 2 },
+ "lib/middleware/rest.js",
+ "lib/middleware/status.js",
+ "lib/middleware/token.js",
+ "lib/middleware/urlNotFound.js",
+{ "title": "Built-in models", "depth": 2 },
"lib/models/access-token.js",
- "lib/models/access-context.js",
"lib/models/acl.js",
"lib/models/application.js",
"lib/models/email.js",
- "lib/models/model.js",
- "lib/models/data-model.js",
"lib/models/role.js",
"lib/models/user.js",
- "lib/models/change.js",
- "docs/api-datasource.md",
- "docs/api-geopoint.md",
- "docs/api-model.md",
- "docs/api-model-remote.md"
+ "lib/models/change.js"
],
"assets": "/docs/assets"
}
diff --git a/docs/api-datasource.md b/docs/api-datasource.md
deleted file mode 100644
index 90b55a86..00000000
--- a/docs/api-datasource.md
+++ /dev/null
@@ -1,212 +0,0 @@
-## Data Source object
-
-LoopBack models can manipulate data via the DataSource object. Attaching a `DataSource` to a `Model` adds instance methods and static methods to the `Model`; some of the added methods may be remote methods.
-
-Define a data source for persisting models.
-
-```js
-var oracle = loopback.createDataSource({
- connector: 'oracle',
- host: '111.22.333.44',
- database: 'MYDB',
- username: 'username',
- password: 'password'
-});
-```
-
-### Methods
-
-#### dataSource.createModel(name, properties, options)
-
-Define a model and attach it to a `DataSource`.
-
-```js
-var Color = oracle.createModel('color', {name: String});
-```
-
-You can define an ACL when you create a new data source with the `DataSource.create()` method. For example:
-
-```js
-var Customer = ds.createModel('Customer', {
- name: {
- type: String,
- acls: [
- {principalType: ACL.USER, principalId: 'u001', accessType: ACL.WRITE, permission: ACL.DENY},
- {principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
- ]
- }
- }, {
- acls: [
- {principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
- ]
- });
-```
-
-#### dataSource.discoverModelDefinitions([username], fn)
-
-Discover a set of model definitions (table or collection names) based on tables or collections in a data source.
-
-```js
-oracle.discoverModelDefinitions(function (err, models) {
- models.forEach(function (def) {
- // def.name ~ the model name
- oracle.discoverSchema(null, def.name, function (err, schema) {
- console.log(schema);
- });
- });
-});
-```
-
-#### dataSource.discoverSchema([owner], name, fn)
-
-Discover the schema of a specific table or collection.
-
-**Example schema from oracle connector:**
-
-```js
- {
- "name": "Product",
- "options": {
- "idInjection": false,
- "oracle": {
- "schema": "BLACKPOOL",
- "table": "PRODUCT"
- }
- },
- "properties": {
- "id": {
- "type": "String",
- "required": true,
- "length": 20,
- "id": 1,
- "oracle": {
- "columnName": "ID",
- "dataType": "VARCHAR2",
- "dataLength": 20,
- "nullable": "N"
- }
- },
- "name": {
- "type": "String",
- "required": false,
- "length": 64,
- "oracle": {
- "columnName": "NAME",
- "dataType": "VARCHAR2",
- "dataLength": 64,
- "nullable": "Y"
- }
- },
- "audibleRange": {
- "type": "Number",
- "required": false,
- "length": 22,
- "oracle": {
- "columnName": "AUDIBLE_RANGE",
- "dataType": "NUMBER",
- "dataLength": 22,
- "nullable": "Y"
- }
- },
- "effectiveRange": {
- "type": "Number",
- "required": false,
- "length": 22,
- "oracle": {
- "columnName": "EFFECTIVE_RANGE",
- "dataType": "NUMBER",
- "dataLength": 22,
- "nullable": "Y"
- }
- },
- "rounds": {
- "type": "Number",
- "required": false,
- "length": 22,
- "oracle": {
- "columnName": "ROUNDS",
- "dataType": "NUMBER",
- "dataLength": 22,
- "nullable": "Y"
- }
- },
- "extras": {
- "type": "String",
- "required": false,
- "length": 64,
- "oracle": {
- "columnName": "EXTRAS",
- "dataType": "VARCHAR2",
- "dataLength": 64,
- "nullable": "Y"
- }
- },
- "fireModes": {
- "type": "String",
- "required": false,
- "length": 64,
- "oracle": {
- "columnName": "FIRE_MODES",
- "dataType": "VARCHAR2",
- "dataLength": 64,
- "nullable": "Y"
- }
- }
- }
- }
-```
-
-#### dataSource.enableRemote(operation)
-
-Enable remote access to a data source operation. Each [connector](#connector) has its own set of set remotely enabled and disabled operations. You can always list these by calling `dataSource.operations()`.
-
-
-#### dataSource.disableRemote(operation)
-
-Disable remote access to a data source operation. Each [connector](#connector) has its own set of set enabled and disabled operations. You can always list these by calling `dataSource.operations()`.
-
-```js
-// all rest data source operations are
-// disabled by default
-var oracle = loopback.createDataSource({
- connector: require('loopback-connector-oracle'),
- host: '...',
- ...
-});
-
-// or only disable it as a remote method
-oracle.disableRemote('destroyAll');
-```
-
-**Notes:**
-
- - Disabled operations will not be added to attached models.
- - Disabling the remoting for a method only affects client access (it will still be available from server models).
- - Data sources must enable / disable operations before attaching or creating models.
-
-#### dataSource.operations()
-
-List the enabled and disabled operations.
-
- console.log(oracle.operations());
-
-Output:
-
-```js
-{
- find: {
- remoteEnabled: true,
- accepts: [...],
- returns: [...]
- enabled: true
- },
- save: {
- remoteEnabled: true,
- prototype: true,
- accepts: [...],
- returns: [...],
- enabled: true
- },
- ...
-}
-```
diff --git a/docs/api-geopoint.md b/docs/api-geopoint.md
deleted file mode 100644
index 588a5246..00000000
--- a/docs/api-geopoint.md
+++ /dev/null
@@ -1,69 +0,0 @@
-## GeoPoint object
-
-The GeoPoint object represents a physical location.
-
-Use the `GeoPoint` class.
-
-```js
-var GeoPoint = require('loopback').GeoPoint;
-```
-
-Embed a latitude / longitude point in a [Model](#model).
-
-```js
-var CoffeeShop = loopback.createModel('coffee-shop', {
- location: 'GeoPoint'
-});
-```
-
-You can query LoopBack models with a GeoPoint property and an attached data source using geo-spatial filters and sorting. For example, the following code finds the three nearest coffee shops.
-
-```js
-CoffeeShop.attachTo(oracle);
-var here = new GeoPoint({lat: 10.32424, lng: 5.84978});
-CoffeeShop.find({where: {location: {near: here}}, limit:3}, function(err, nearbyShops) {
- console.info(nearbyShops); // [CoffeeShop, ...]
-});
-```
-
-### Distance Types
-
-**Note:** all distance methods use `miles` by default.
-
- - `miles`
- - `radians`
- - `kilometers`
- - `meters`
- - `miles`
- - `feet`
- - `degrees`
-
-### Methods
-
-#### geoPoint.distanceTo(geoPoint, options)
-
-Get the distance to another `GeoPoint`; for example:
-
-```js
-var here = new GeoPoint({lat: 10, lng: 10});
-var there = new GeoPoint({lat: 5, lng: 5});
-console.log(here.distanceTo(there, {type: 'miles'})); // 438
-```
-
-#### GeoPoint.distanceBetween(a, b, options)
-
-Get the distance between two points; for example:
-
-```js
-GeoPoint.distanceBetween(here, there, {type: 'miles'}) // 438
-```
-
-### Properties
-
-#### geoPoint.lat
-
-The latitude point in degrees. Range: -90 to 90.
-
-#### geoPoint.lng
-
-The longitude point in degrees. Range: -180 to 180.
diff --git a/docs/api-model-remote.md b/docs/api-model-remote.md
deleted file mode 100644
index c4dfe3ca..00000000
--- a/docs/api-model-remote.md
+++ /dev/null
@@ -1,200 +0,0 @@
-## Remote methods and hooks
-
-You can expose a Model's instance and static methods to clients. A remote method must accept a callback with the conventional `fn(err, result, ...)` signature.
-
-### Static Methods
-
-#### loopback.remoteMethod(fn, [options])
-
-Expose a remote method.
-
-```js
-Product.stats = function(fn) {
- var statsResult = {
- totalPurchased: 123456
- };
- var err = null;
-
- // callback with an error and the result
- fn(err, statsResult);
-}
-
-loopback.remoteMethod(
- Product.stats,
- {
- returns: {arg: 'stats', type: 'object'},
- http: {path: '/info', verb: 'get'}
- }
-);
-```
-
-**Options**
-
-The options argument is a JSON object, described in the following table.
-
-| Option | Required? | Description |
-| ----- | ----- | ----- |
-| accepts | No | Describes the remote method's arguments; See Argument description. The `callback` argument is assumed; do not specify. |
-| returns | No | Describes the remote method's callback arguments; See Argument description. The `err` argument is assumed; do not specify. |
-| http | No | HTTP routing information:
- **http.path**: path (relative to the model) at which the method is exposed. May be a path fragment (for example, `/:myArg`) that will be populated by an arg of the same name in the `accepts` description. For example, the `stats` method above will be at the whole path `/products/stats`.
- **http.verb**: HTTP method (verb) from which the method is available (one of: get, post, put, del, or all).
-| description | No | A text description of the method. This is used by API documentation generators like Swagger.
-
-
-**Argument description**
-
-The arguments description defines either a single argument as an object or an ordered set of arguments as an array. Each individual argument has keys for:
-
- * arg: argument name
- * type: argument datatype; must be a [loopback type](http://wiki.strongloop.com/display/DOC/LoopBack+types).
- * required: Boolean value indicating if argument is required.
- * root: For callback arguments: set this property to `true` if your function
- has a single callback argument to use as the root object
- returned to remote caller. Otherwise the root object returned is a map (argument-name to argument-value).
- * http: For input arguments: a function or an object describing mapping from HTTP request
- to the argument value, as explained below.
-
-For example, a single argument, specified as an object:
-
-```js
-{arg: 'myArg', type: 'number'}
-```
-
-Multiple arguments, specified as an array:
-
-```js
-[
- {arg: 'arg1', type: 'number', required: true},
- {arg: 'arg2', type: 'array'}
-]
-```
-
-
-**HTTP mapping of input arguments**
-
-There are two ways to specify HTTP mapping for input parameters (what the method accepts):
-
- * Provide an object with a `source` property
- * Specify a custom mapping function
-
-To use the first way to specify HTTP mapping for input parameters, provide an object with a `source` property
-that has one of the values shown in the following table.
-
-| Value of source property | Description |
-|---|---|
-| body | The whole request body is used as the value. |
-| form | The value is looked up using `req.param`, which searches route arguments, the request body and the query string.|
-| query | An alias for form (see above). |
-| path | An alias for form (see above). |
-| req | The whole HTTP reqest object is used as the value. |
-
-For example, an argument getting the whole request body as the value:
-
-```js
-{ arg: 'data', type: 'object', http: { source: 'body' } }
-```
-
-The use the second way to specify HTTP mapping for input parameters, specify a custom mapping function
-that looks like this:
-
-```js
-{
- arg: 'custom',
- type: 'number',
- http: function(ctx) {
- // ctx is LoopBack Context object
-
- // 1. Get the HTTP request object as provided by Express
- var req = ctx.req;
-
- // 2. Get 'a' and 'b' from query string or form data
- // and return their sum as the value
- return +req.param('a') + req.param('b');
- }
-}
-```
-
-If you don't specify a mapping, LoopBack will determine the value
-as follows (assuming `name` as the name of the input parameter to resolve):
-
- 1. If there is a HTTP request parameter `args` with a JSON content,
- then the value of `args['name']` is used if it is defined.
- 2. Otherwise `req.param('name')` is returned.
-
-### Remote hooks
-
-Run a function before or after a remote method is called by a client.
-
-```js
-// *.save === prototype.save
-User.beforeRemote('*.save', function(ctx, user, next) {
- if(ctx.user) {
- next();
- } else {
- next(new Error('must be logged in to update'))
- }
-});
-
-User.afterRemote('*.save', function(ctx, user, next) {
- console.log('user has been saved', user);
- next();
-});
-```
-
-Remote hooks also support wildcards. Run a function before any remote method is called.
-
-```js
-// ** will match both prototype.* and *.*
-User.beforeRemote('**', function(ctx, user, next) {
- console.log(ctx.methodString, 'was invoked remotely'); // users.prototype.save was invoked remotely
- next();
-});
-```
-
-Other wildcard examples
-
-```js
-// run before any static method eg. User.find
-User.beforeRemote('*', ...);
-
-// run before any instance method eg. User.prototype.save
-User.beforeRemote('prototype.*', ...);
-
-// prevent password hashes from being sent to clients
-User.afterRemote('**', function (ctx, user, next) {
- if(ctx.result) {
- if(Array.isArray(ctx.result)) {
- ctx.result.forEach(function (result) {
- result.password = undefined;
- });
- } else {
- ctx.result.password = undefined;
- }
- }
-
- next();
-});
-```
-
-### Context
-
-Remote hooks are provided with a Context `ctx` object which contains transport specific data (eg. for http: `req` and `res`). The `ctx` object also has a set of consistent apis across transports.
-
-#### ctx.req.accessToken
-
-The `accessToken` of the user calling the method remotely. **Note:** this is undefined if the remote method is not invoked by a logged in user (or other principal).
-
-#### ctx.result
-
-During `afterRemote` hooks, `ctx.result` will contain the data about to be sent to a client. Modify this object to transform data before it is sent.
-
-#### REST
-
-When [loopback.rest](#loopbackrest) is used the following additional `ctx` properties are available.
-
-##### ctx.req
-
-The express ServerRequest object. [See full documentation](http://expressjs.com/api.html#req).
-
-##### ctx.res
-
-The express ServerResponse object. [See full documentation](http://expressjs.com/api.html#res).
diff --git a/docs/api-model.md b/docs/api-model.md
deleted file mode 100644
index 7aaf3687..00000000
--- a/docs/api-model.md
+++ /dev/null
@@ -1,448 +0,0 @@
-## Model object
-
-A Loopback `Model` is a vanilla JavaScript class constructor with an attached set of properties and options. A `Model` instance is created by passing a data object containing properties to the `Model` constructor. A `Model` constructor will clean the object passed to it and only set the values matching the properties you define.
-
-```js
-// valid color
-var Color = loopback.createModel('color', {name: String});
-var red = new Color({name: 'red'});
-console.log(red.name); // red
-
-// invalid color
-var foo = new Color({bar: 'bat baz'});
-console.log(foo.bar); // undefined
-```
-
-**Properties**
-
-A model defines a list of property names, types and other validation metadata. A [DataSource](#data-source) uses this definition to validate a `Model` during operations such as `save()`.
-
-**Options**
-
-Some [DataSources](#data-source) may support additional `Model` options.
-
-Define A Loopbackmodel.
-
-```js
-var User = loopback.createModel('user', {
- first: String,
- last: String,
- age: Number
-});
-```
-
-### Methods
-
-#### Model.attachTo(dataSource)
-
-Attach a model to a [DataSource](#data-source). Attaching a [DataSource](#data-source) updates the model with additional methods and behaviors.
-
-```js
-var oracle = loopback.createDataSource({
- connector: require('loopback-connector-oracle'),
- host: '111.22.333.44',
- database: 'MYDB',
- username: 'username',
- password: 'password'
-});
-
-User.attachTo(oracle);
-```
-
-NOTE: until a model is attached to a data source it will not have any attached methods.
-
-### Properties
-
-#### Model.properties
-
-An object containing a normalized set of properties supplied to `loopback.createModel(name, properties)`.
-
-Example:
-
-```js
-var props = {
- a: String,
- b: {type: 'Number'},
- c: {type: 'String', min: 10, max: 100},
- d: Date,
- e: loopback.GeoPoint
-};
-
-var MyModel = loopback.createModel('foo', props);
-
-console.log(MyModel.properties);
-```
-
-Outputs:
-
-```js
-{
- "a": {type: String},
- "b": {type: Number},
- "c": {
- "type": String,
- "min": 10,
- "max": 100
- },
- "d": {type: Date},
- "e": {type: GeoPoint},
- "id": {
- "id": 1
- }
-}
-```
-
-### CRUD and Query Mixins
-
-Mixins are added by attaching a vanilla model to a [data source](#data-source) with a [connector](#connectors). Each [connector](#connectors) enables its own set of operations that are mixed into a `Model` as methods. To see available methods for a data source call `dataSource.operations()`.
-
-Log the available methods for a memory data source.
-
-```js
-var ops = loopback
- .createDataSource({connector: loopback.Memory})
- .operations();
-
-console.log(Object.keys(ops));
-```
-
-Outputs:
-
-```js
-[ 'create',
- 'updateOrCreate',
- 'upsert',
- 'findOrCreate',
- 'exists',
- 'findById',
- 'find',
- 'all',
- 'findOne',
- 'destroyAll',
- 'deleteAll',
- 'count',
- 'include',
- 'relationNameFor',
- 'hasMany',
- 'belongsTo',
- 'hasAndBelongsToMany',
- 'save',
- 'isNewRecord',
- 'destroy',
- 'delete',
- 'updateAttribute',
- 'updateAttributes',
- 'reload' ]
-```
-
-Here is the definition of the `count()` operation.
-
-```js
-{
- accepts: [ { arg: 'where', type: 'object' } ],
- http: { verb: 'get', path: '/count' },
- remoteEnabled: true,
- name: 'count'
-}
-```
-
-### Static Methods
-
-**Note:** These are the default mixin methods for a `Model` attached to a data source. See the specific connector for additional API documentation.
-
-#### Model.create(data, [callback])
-
-Create an instance of Model with given data and save to the attached data source. Callback is optional.
-
-```js
-User.create({first: 'Joe', last: 'Bob'}, function(err, user) {
- console.log(user instanceof User); // true
-});
-```
-
-**Note:** You must include a callback and use the created model provided in the callback if your code depends on your model being saved or having an `id`.
-
-#### Model.count([query], callback)
-
-Query count of Model instances in data source. Optional query param allows to count filtered set of Model instances.
-
-```js
-User.count({approved: true}, function(err, count) {
- console.log(count); // 2081
-});
-```
-
-#### Model.find(filter, callback)
-
-Find all instances of Model, matched by query. Fields used for filter and sort should be declared with `{index: true}` in model definition.
-
-**filter**
-
- - **where** `Object` { key: val, key2: {gt: 'val2'}} The search criteria
- - Format: {key: val} or {key: {op: val}}
- - Operations:
- - gt: >
- - gte: >=
- - lt: <
- - lte: <=
- - between
- - inq: IN
- - nin: NOT IN
- - neq: !=
- - like: LIKE
- - nlike: NOT LIKE
-
- - **include** `String`, `Object` or `Array` Allows you to load relations of several objects and optimize numbers of requests.
- - Format:
- - 'posts': Load posts
- - ['posts', 'passports']: Load posts and passports
- - {'owner': 'posts'}: Load owner and owner's posts
- - {'owner': ['posts', 'passports']}: Load owner, owner's posts, and owner's passports
- - {'owner': [{posts: 'images'}, 'passports']}: Load owner, owner's posts, owner's posts' images, and owner's passports
-
- - **order** `String` The sorting order
- - Format: 'key1 ASC, key2 DESC'
-
- - **limit** `Number` The maximum number of instances to be returned
- - **skip** `Number` Skip the number of instances
- - **offset** `Number` Alias for skip
-
- - **fields** `Object|Array|String` The included/excluded fields
- - `['foo']` or `'foo'` - include only the foo property
- - `['foo', 'bar']` - include the foo and bar properties
- - `{foo: true}` - include only foo
- - `{bat: false}` - include all properties, exclude bat
-
-Find the second page of 10 users over age 21 in descending order exluding the password property.
-
-```js
-User.find({
- where: {
- age: {gt: 21}},
- order: 'age DESC',
- limit: 10,
- skip: 10,
- fields: {password: false}
- },
- console.log
-);
-```
-
-**Note:** See the specific connector's [docs](#connectors) for more info.
-
-#### Model.destroyAll([where], callback)
-
-Delete all Model instances from data source. **Note:** destroyAll method does not perform destroy hooks.
-
-```js
-Product.destroyAll({price: {gt: 99}}, function(err) {
- // removed matching products
-});
-```
-
-> **NOTE:* `where` is optional and a where object... do NOT pass a filter object
-
-#### Model.findById(id, callback)
-
-Find instance by id.
-
-```js
-User.findById(23, function(err, user) {
- console.info(user.id); // 23
-});
-```
-
-#### Model.findOne(where, callback)
-
-Find a single instance that matches the given where expression.
-
-```js
-User.findOne({where: {id: 23}}, function(err, user) {
- console.info(user.id); // 23
-});
-```
-
-#### Model.upsert(data, callback)
-
-Update when record with id=data.id found, insert otherwise. **Note:** no setters, validations or hooks applied when using upsert.
-
-#### Custom static methods
-
-Define a static model method.
-
-```js
-User.login = function (username, password, fn) {
- var passwordHash = hashPassword(password);
- this.findOne({username: username}, function (err, user) {
- var failErr = new Error('login failed');
-
- if(err) {
- fn(err);
- } else if(!user) {
- fn(failErr);
- } else if(user.password === passwordHash) {
- MyAccessTokenModel.create({userId: user.id}, function (err, accessToken) {
- fn(null, accessToken.id);
- });
- } else {
- fn(failErr);
- }
- });
-}
-```
-
-Setup the static model method to be exposed to clients as a [remote method](#remote-method).
-
-```js
-loopback.remoteMethod(
- User.login,
- {
- accepts: [
- {arg: 'username', type: 'string', required: true},
- {arg: 'password', type: 'string', required: true}
- ],
- returns: {arg: 'sessionId', type: 'any'},
- http: {path: '/sign-in'}
- }
-);
-```
-
-### Instance methods
-
-**Note:** These are the default mixin methods for a `Model` attached to a data source. See the specific connector for additional API documentation.
-
-#### model.save([options], [callback])
-
-Save an instance of a Model to the attached data source.
-
-```js
-var joe = new User({first: 'Joe', last: 'Bob'});
-joe.save(function(err, user) {
- if(user.errors) {
- console.log(user.errors);
- } else {
- console.log(user.id);
- }
-});
-```
-
-#### model.updateAttributes(data, [callback])
-
-Save specified attributes to the attached data source.
-
-```js
-user.updateAttributes({
- first: 'updatedFirst',
- name: 'updatedLast'
-}, fn);
-```
-
-#### model.destroy([callback])
-
-Remove a model from the attached data source.
-
-```js
-model.destroy(function(err) {
- // model instance destroyed
-});
-```
-
-#### Custom instance methods
-
-Define an instance method.
-
-```js
-User.prototype.logout = function (fn) {
- MySessionModel.destroyAll({userId: this.id}, fn);
-}
-```
-
-Define a remote model instance method.
-
-```js
-loopback.remoteMethod(User.prototype.logout)
-```
-
-### Relationships
-
-#### Model.hasMany(Model, options)
-
-Define a "one to many" relationship.
-
-```js
-// by referencing model
-Book.hasMany(Chapter);
-// specify the name
-Book.hasMany('chapters', {model: Chapter});
-```
-
-Query and create the related models.
-
-```js
-Book.create(function(err, book) {
- // create a chapter instance
- // ready to be saved in the data source
- var chapter = book.chapters.build({name: 'Chapter 1'});
-
- // save the new chapter
- chapter.save();
-
- // you can also call the Chapter.create method with
- // the `chapters` property which will build a chapter
- // instance and save the it in the data source
- book.chapters.create({name: 'Chapter 2'}, function(err, savedChapter) {
- // this callback is optional
- });
-
- // query chapters for the book using the
- book.chapters(function(err, chapters) {
- // all chapters with bookId = book.id
- console.log(chapters);
- });
-
- book.chapters({where: {name: 'test'}, function(err, chapters) {
- // all chapters with bookId = book.id and name = 'test'
- console.log(chapters);
- });
-});
-```
-
-#### Model.belongsTo(Model, options)
-
-A `belongsTo` relation sets up a one-to-one connection with another model, such
-that each instance of the declaring model "belongs to" one instance of the other
-model. For example, if your application includes users and posts, and each post
-can be written by exactly one user.
-
-```js
- Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
-```
-
-The code above basically says Post has a reference called `author` to User using
-the `userId` property of Post as the foreign key. Now we can access the author
-in one of the following styles:
-
-```js
- post.author(callback); // Get the User object for the post author asynchronously
- post.author(); // Get the User object for the post author synchronously
- post.author(user) // Set the author to be the given user
-```
-
-#### Model.hasAndBelongsToMany(Model, options)
-
-A `hasAndBelongsToMany` relation creates a direct many-to-many connection with
-another model, with no intervening model. For example, if your application
-includes users and groups, with each group having many users and each user
-appearing in many groups, you could declare the models this way,
-
-```js
- User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'});
- user.groups(callback); // get groups of the user
- user.groups.create(data, callback); // create a new group and connect it with the user
- user.groups.add(group, callback); // connect an existing group with the user
- user.groups.remove(group, callback); // remove the user from the group
-```
-
-### Shared methods
-
-Any static or instance method can be decorated as `shared`. These methods are exposed over the provided transport (eg. [loopback.rest](#rest)).
diff --git a/docs/assets/lb-modules.png b/docs/assets/lb-modules.png
index 390b86ea..c8f750e3 100644
Binary files a/docs/assets/lb-modules.png and b/docs/assets/lb-modules.png differ
diff --git a/lib/application.js b/lib/application.js
index 1e89f6b0..f421f706 100644
--- a/lib/application.js
+++ b/lib/application.js
@@ -114,58 +114,55 @@ app.disuse = function (route) {
*/
app.model = function (Model, config) {
- if(arguments.length === 1) {
- assert(Model.prototype instanceof registry.Model,
- 'Model must be a descendant of loopback.Model');
- if(Model.sharedClass) {
- this.remotes().addClass(Model.sharedClass);
+ var isPublic = true;
+ if (arguments.length > 1) {
+ config = config || {};
+ if (typeof Model === 'string') {
+ // create & attach the model - backwards compatibility
+
+ // create config for loopback.modelFromConfig
+ var modelConfig = extend({}, config);
+ modelConfig.options = extend({}, config.options);
+ modelConfig.name = Model;
+
+ // modeller does not understand `dataSource` option
+ delete modelConfig.dataSource;
+
+ Model = registry.createModel(modelConfig);
+
+ // delete config options already applied
+ ['relations', 'base', 'acls', 'hidden'].forEach(function(prop) {
+ delete config[prop];
+ if (config.options) delete config.options[prop];
+ });
+ delete config.properties;
}
- this.models().push(Model);
- clearHandlerCache(this);
- Model.shared = true;
- Model.app = this;
- Model.emit('attached', this);
- return Model;
+
+ configureModel(Model, config, this);
+ isPublic = config.public !== false;
+ } else {
+ assert(Model.prototype instanceof loopback.Model,
+ 'Model must be a descendant of loopback.Model');
}
- config = config || {};
-
- if (typeof Model === 'string') {
- // create & attach the model - loopback 1.x compatibility
-
- // create config for loopback.modelFromConfig
- var modelConfig = extend({}, config);
- modelConfig.options = extend({}, config.options);
- modelConfig.name = Model;
-
- // modeller does not understand `dataSource` option
- delete modelConfig.dataSource;
-
- Model = registry.createModel(modelConfig);
-
- // delete config options already applied
- ['relations', 'base', 'acls', 'hidden'].forEach(function(prop) {
- delete config[prop];
- if (config.options) delete config.options[prop];
- });
- delete config.properties;
- }
-
- configureModel(Model, config, this);
-
var modelName = Model.modelName;
this.models[modelName] =
- this.models[classify(modelName)] =
- this.models[camelize(modelName)] = Model;
+ this.models[classify(modelName)] =
+ this.models[camelize(modelName)] = Model;
- if (config.public !== false) {
- this.model(Model);
+ this.models().push(Model);
+
+ if (isPublic && Model.sharedClass) {
+ this.remotes().addClass(Model.sharedClass);
+ clearHandlerCache(this);
}
+ Model.shared = isPublic;
+ Model.app = this;
+ Model.emit('attached', this);
return Model;
};
-
/**
* Get the models exported by the app. Returns only models defined using `app.model()`
*
@@ -337,7 +334,7 @@ app.enableAuth = function() {
Model.checkAccess(
req.accessToken,
modelId,
- method.name,
+ method,
function(err, allowed) {
// Emit any cached data events that fired while checking access.
req.resume();
diff --git a/lib/loopback.js b/lib/loopback.js
index 893f37d2..032f5d34 100644
--- a/lib/loopback.js
+++ b/lib/loopback.js
@@ -159,6 +159,7 @@ loopback.template = function (file) {
return ejs.compile(str);
};
+
/*!
* Built in models / services
*/
diff --git a/lib/middleware/rest.js b/lib/middleware/rest.js
index 3f87c126..5e91c7f4 100644
--- a/lib/middleware/rest.js
+++ b/lib/middleware/rest.js
@@ -1,17 +1,24 @@
-/**
+/*!
* Module dependencies.
*/
var loopback = require('../loopback');
-/**
+/*!
* Export the middleware.
*/
module.exports = rest;
/**
- * Build a temp app for mounting resources.
+ * Expose models over REST.
+ *
+ * For example:
+ * ```js
+ * app.use(loopback.rest());
+ * ```
+ * For more information, see [Exposing models over a REST API](http://docs.strongloop.com/display/DOC/Exposing+models+over+a+REST+API).
+ * @header loopback.rest()
*/
function rest() {
diff --git a/lib/middleware/status.js b/lib/middleware/status.js
index 02068548..085c5975 100644
--- a/lib/middleware/status.js
+++ b/lib/middleware/status.js
@@ -1,9 +1,22 @@
-/**
+/*!
* Export the middleware.
*/
module.exports = status;
+/**
+ * Return [HTTP response](http://expressjs.com/4x/api.html#res.send) with basic application status information:
+ * date the application was started and uptime, in JSON format.
+ * For example:
+ * ```js
+ * {
+ * "started": "2014-06-05T00:26:49.750Z",
+ * "uptime": 9.394
+ * }
+ * ```
+ *
+ * @header loopback.status()
+ */
function status() {
var started = new Date();
diff --git a/lib/middleware/token.js b/lib/middleware/token.js
index 88a93c24..6087fd31 100644
--- a/lib/middleware/token.js
+++ b/lib/middleware/token.js
@@ -12,14 +12,17 @@ var assert = require('assert');
module.exports = token;
/**
- * **Options**
+ * Check for an access token in cookies, headers, and query string parameters.
+ * This function always checks for the following:
*
- * - `cookies` - An `Array` of cookie names
- * - `headers` - An `Array` of header names
- * - `params` - An `Array` of param names
- * - `model` - Specify an AccessToken class to use
+ * - `access_token`
+ * - `X-Access-Token`
+ * - `authorization`
+ *
+ * It checks for these values in cookies, headers, and query string parameters _in addition_ to the items
+ * specified in the options parameter.
*
- * Each array is used to add additional keys to find an `accessToken` for a `request`.
+ * **NOTE:** This function only checks for [signed cookies](http://expressjs.com/api.html#req.signedCookies).
*
* The following example illustrates how to check for an `accessToken` in a custom cookie, query string parameter
* and header called `foo-auth`.
@@ -28,23 +31,16 @@ module.exports = token;
* app.use(loopback.token({
* cookies: ['foo-auth'],
* headers: ['foo-auth', 'X-Foo-Auth'],
- * cookies: ['foo-auth', 'foo_auth']
+ * params: ['foo-auth', 'foo_auth']
* }));
* ```
- *
- * **Defaults**
- *
- * By default the following names will be checked. These names are appended to any optional names. They will always
- * be checked, but any names specified will be checked first.
- *
- * - **access_token**
- * - **X-Access-Token**
- * - **authorization**
- * - **access_token**
- *
- * **NOTE:** The `loopback.token()` middleware will only check for [signed cookies](http://expressjs.com/api.html#req.signedCookies).
*
- * @header loopback.token(options)
+ * @options {Object} [options] Each option array is used to add additional keys to find an `accessToken` for a `request`.
+ * @property {Array} [cookies] Array of cookie names.
+ * @property {Array} [headers] Array of header names.
+ * @property {Array} [params] Array of param names.
+ * @property {Array} [model] An AccessToken object to use.
+ * @header loopback.token([options])
*/
function token(options) {
diff --git a/lib/middleware/urlNotFound.js b/lib/middleware/urlNotFound.js
index 9f9735e6..a73f6804 100644
--- a/lib/middleware/urlNotFound.js
+++ b/lib/middleware/urlNotFound.js
@@ -1,13 +1,14 @@
-/**
+/*!
* Export the middleware.
+ * See discussion in Connect pull request #954 for more details
+ * https://github.com/senchalabs/connect/pull/954.
*/
module.exports = urlNotFound;
/**
* Convert any request not handled so far to a 404 error
* to be handled by error-handling middleware.
- * See discussion in Connect pull request #954 for more details
- * https://github.com/senchalabs/connect/pull/954
+ * @header loopback.urlNotFound()
*/
function urlNotFound() {
return function raiseUrlNotFoundError(req, res, next) {
diff --git a/lib/models/access-context.js b/lib/models/access-context.js
index b748db1b..681c366c 100644
--- a/lib/models/access-context.js
+++ b/lib/models/access-context.js
@@ -36,7 +36,18 @@ function AccessContext(context) {
this.property = context.property || AccessContext.ALL;
this.method = context.method;
-
+ this.sharedMethod = context.sharedMethod;
+ this.sharedClass = this.sharedMethod && this.sharedMethod.sharedClass;
+ if(this.sharedMethod) {
+ this.methodNames = this.sharedMethod.aliases.concat([this.sharedMethod.name]);
+ } else {
+ this.methodNames = [];
+ }
+
+ if(this.sharedMethod) {
+ this.accessType = this.model._getAccessTypeForMethod(this.sharedMethod);
+ }
+
this.accessType = context.accessType || AccessContext.ALL;
this.accessToken = context.accessToken || AccessToken.ANONYMOUS;
@@ -79,7 +90,6 @@ AccessContext.permissionOrder = {
DENY: 4
};
-
/**
* Add a principal to the context
* @param {String} principalType The principal type
@@ -96,8 +106,6 @@ AccessContext.prototype.addPrincipal = function (principalType, principalId, pri
}
}
this.principals.push(principal);
-
- debug('adding principal %j', principal);
return true;
};
@@ -213,7 +221,7 @@ Principal.prototype.equals = function (p) {
* @returns {AccessRequest}
* @class
*/
-function AccessRequest(model, property, accessType, permission) {
+function AccessRequest(model, property, accessType, permission, methodNames) {
if (!(this instanceof AccessRequest)) {
return new AccessRequest(model, property, accessType);
}
@@ -224,26 +232,20 @@ function AccessRequest(model, property, accessType, permission) {
this.property = obj.property || AccessContext.ALL;
this.accessType = obj.accessType || AccessContext.ALL;
this.permission = obj.permission || AccessContext.DEFAULT;
+ this.methodNames = methodNames || [];
} else {
this.model = model || AccessContext.ALL;
this.property = property || AccessContext.ALL;
this.accessType = accessType || AccessContext.ALL;
this.permission = permission || AccessContext.DEFAULT;
- }
-
- if(debug.enabled) {
- debug('---AccessRequest---');
- debug(' model %s', this.model);
- debug(' property %s', this.property);
- debug(' accessType %s', this.accessType);
- debug(' permission %s', this.permission);
- debug(' isWildcard() %s', this.isWildcard());
+ this.methodNames = methodNames || [];
}
}
/**
- * Is the request a wildcard
- * @returns {boolean}
+ * Does the request contain any wildcards?
+ *
+ * @returns {Boolean}
*/
AccessRequest.prototype.isWildcard = function () {
return this.model === AccessContext.ALL ||
@@ -251,6 +253,47 @@ AccessRequest.prototype.isWildcard = function () {
this.accessType === AccessContext.ALL;
};
+/**
+ * Does the given `ACL` apply to this `AccessRequest`.
+ *
+ * @param {ACL} acl
+ */
+
+AccessRequest.prototype.exactlyMatches = function(acl) {
+ var matchesModel = acl.model === this.model;
+ var matchesProperty = acl.property === this.property;
+ var matchesMethodName = this.methodNames.indexOf(acl.property) !== -1;
+ var matchesAccessType = acl.accessType === this.accessType;
+
+ if(matchesModel && matchesAccessType) {
+ return matchesProperty || matchesMethodName;
+ }
+
+ return false;
+}
+
+/**
+ * Is the request for access allowed?
+ *
+ * @returns {Boolean}
+ */
+
+AccessRequest.prototype.isAllowed = function() {
+ return this.permission !== require('./acl').ACL.DENY;
+}
+
+AccessRequest.prototype.debug = function() {
+ if(debug.enabled) {
+ debug('---AccessRequest---');
+ debug(' model %s', this.model);
+ debug(' property %s', this.property);
+ debug(' accessType %s', this.accessType);
+ debug(' permission %s', this.permission);
+ debug(' isWildcard() %s', this.isWildcard());
+ debug(' isAllowed() %s', this.isAllowed());
+ }
+}
+
module.exports.AccessContext = AccessContext;
module.exports.Principal = Principal;
module.exports.AccessRequest = AccessRequest;
diff --git a/lib/models/acl.js b/lib/models/acl.js
index 7dc1d03c..a3126b87 100644
--- a/lib/models/acl.js
+++ b/lib/models/acl.js
@@ -61,6 +61,7 @@ var ACLSchema = {
/**
* Name of the access type - READ/WRITE/EXEC
+ * @property accessType {String} Name of the access type - READ/WRITE/EXEC
*/
accessType: String,
@@ -114,17 +115,20 @@ ACL.SCOPE = Principal.SCOPE;
* Calculate the matching score for the given rule and request
* @param {ACL} rule The ACL entry
* @param {AccessRequest} req The request
- * @returns {number}
+ * @returns {Number}
*/
ACL.getMatchingScore = function getMatchingScore(rule, req) {
var props = ['model', 'property', 'accessType'];
var score = 0;
+
for (var i = 0; i < props.length; i++) {
// Shift the score by 4 for each of the properties as the weight
score = score * 4;
var val1 = rule[props[i]] || ACL.ALL;
var val2 = req[props[i]] || ACL.ALL;
- if (val1 === val2) {
+ var isMatchingMethodName = props[i] === 'property' && req.methodNames.indexOf(val1) !== -1;
+
+ if (val1 === val2 || isMatchingMethodName) {
// Exact match
score += 3;
} else if (val1 === ACL.ALL) {
@@ -186,6 +190,16 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) {
return score;
};
+/**
+ * Get matching score for the given `AccessRequest`.
+ * @param {AccessRequest} req The request
+ * @returns {Number} score
+ */
+
+ACL.prototype.score = function(req) {
+ return this.constructor.getMatchingScore(this, req);
+}
+
/*!
* Resolve permission from the ACLs
* @param {Object[]) acls The list of ACLs
@@ -200,14 +214,13 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
acls = acls.sort(function (rule1, rule2) {
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
});
- if(debug.enabled) {
- debug('ACLs by order: %j', acls);
- }
var permission = ACL.DEFAULT;
var score = 0;
+
for (var i = 0; i < acls.length; i++) {
score = ACL.getMatchingScore(acls[i], req);
if (score < 0) {
+ // the highest scored ACL did not match
break;
}
if (!req.isWildcard()) {
@@ -215,11 +228,7 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
permission = acls[i].permission;
break;
} else {
- if(acls[i].model === req.model &&
- acls[i].property === req.property &&
- acls[i].accessType === req.accessType
- ) {
- // We should stop at the exact match
+ if(req.exactlyMatches(acls[i])) {
permission = acls[i].permission;
break;
}
@@ -231,6 +240,14 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
}
}
+ if(debug.enabled) {
+ debug('The following ACLs were searched: ');
+ acls.forEach(function(acl) {
+ acl.debug();
+ debug('with score:', acl.score(req));
+ });
+ }
+
var res = new AccessRequest(req.model, req.property, req.accessType,
permission || ACL.DEFAULT);
return res;
@@ -253,11 +270,9 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
property: acl.property || ACL.ALL,
principalType: acl.principalType,
principalId: acl.principalId, // TODO: Should it be a name?
- accessType: acl.accessType,
+ accessType: acl.accessType || ACL.ALL,
permission: acl.permission
}));
-
- staticACLs[staticACLs.length - 1].debug('Adding ACL');
});
}
var prop = modelClass &&
@@ -282,14 +297,12 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
/**
* Check if the given principal is allowed to access the model/property
- * @param {String} principalType The principal type
- * @param {String} principalId The principal id
- * @param {String} model The model name
- * @param {String} property The property/method/relation name
- * @param {String} accessType The access type
- * @param {Function} callback The callback function
- *
- * @callback callback
+ * @param {String} principalType The principal type.
+ * @param {String} principalId The principal ID.
+ * @param {String} model The model name.
+ * @param {String} property The property/method/relation name.
+ * @param {String} accessType The access type.
+ * @callback {Function} callback Callback function.
* @param {String|Error} err The error object
* @param {AccessRequest} result The access permission
*/
@@ -350,15 +363,16 @@ ACL.prototype.debug = function() {
}
/**
- * Check if the request has the permission to access
- * @param {Object} context
- * @property {Object[]} principals An array of principals
- * @property {String|Model} model The model name or model class
- * @property {*} id The model instance id
- * @property {String} property The property/method/relation name
- * @property {String} accessType The access type
- * @param {Function} callback
+ * Check if the request has the permission to access.
+ * @options {Object} context See below.
+ * @property {Object[]} principals An array of principals.
+ * @property {String|Model} model The model name or model class.
+ * @property {*} id The model instance ID.
+ * @property {String} property The property/method/relation name.
+ * @property {String} accessType The access type: READE, WRITE, or EXEC.
+ * @param {Function} callback Callback function
*/
+
ACL.checkAccessForContext = function (context, callback) {
if(!(context instanceof AccessContext)) {
context = new AccessContext(context);
@@ -367,11 +381,13 @@ ACL.checkAccessForContext = function (context, callback) {
var model = context.model;
var property = context.property;
var accessType = context.accessType;
+ var modelName = context.modelName;
- var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
+ var methodNames = context.methodNames;
+ var propertyQuery = (property === ACL.ALL) ? undefined : {inq: methodNames.concat([ACL.ALL])};
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};
- var req = new AccessRequest(model.modelName, property, accessType);
+ var req = new AccessRequest(modelName, property, accessType, ACL.DEFAULT, methodNames);
var effectiveACLs = [];
var staticACLs = this.getStaticACLs(model.modelName, property);
@@ -404,9 +420,6 @@ ACL.checkAccessForContext = function (context, callback) {
inRoleTasks.push(function (done) {
roleModel.isInRole(acl.principalId, context,
function (err, inRole) {
- if(debug.enabled) {
- debug('In role %j: %j', acl.principalId, inRole);
- }
if (!err && inRole) {
effectiveACLs.push(acl);
}
@@ -425,21 +438,20 @@ ACL.checkAccessForContext = function (context, callback) {
if(resolved && resolved.permission === ACL.DEFAULT) {
resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW;
}
- debug('checkAccessForContext() returns: %j', resolved);
+ debug('---Resolved---');
+ resolved.debug();
callback && callback(null, resolved);
});
});
};
-
/**
* Check if the given access token can invoke the method
* @param {AccessToken} token The access token
* @param {String} model The model name
* @param {*} modelId The model id
* @param {String} method The method name
- * @end
- * @callback {Function} callback
+ * @callback {Function} callback Callback function
* @param {String|Error} err The error object
* @param {Boolean} allowed is the request allowed
*/
@@ -454,10 +466,6 @@ ACL.checkAccessForToken = function (token, model, modelId, method, callback) {
modelId: modelId
});
- context.accessType = context.model._getAccessTypeForMethod(method);
-
- context.debug();
-
this.checkAccessForContext(context, function (err, access) {
if (err) {
callback && callback(err);
diff --git a/lib/models/application.js b/lib/models/application.js
index 1201fca3..f395b6a1 100644
--- a/lib/models/application.js
+++ b/lib/models/application.js
@@ -46,7 +46,7 @@ var PushNotificationSettingSchema = {
gcm: GcmSettingsSchema
};
-/**
+/*!
* Data model for Application
*/
var ApplicationSchema = {
@@ -133,10 +133,10 @@ Application.beforeCreate = function (next) {
/**
* Register a new application
- * @param owner Owner's user id
- * @param name Name of the application
- * @param options Other options
- * @param cb Callback function
+ * @param {String} owner Owner's user ID.
+ * @param {String} name Name of the application
+ * @param {Object} options Other options
+ * @param {Function} callback Callback function
*/
Application.register = function (owner, name, options, cb) {
assert(owner, 'owner is required');
diff --git a/lib/models/model.js b/lib/models/model.js
index fa94aeaa..1cfc50d4 100644
--- a/lib/models/model.js
+++ b/lib/models/model.js
@@ -7,7 +7,6 @@ var juggler = require('loopback-datasource-juggler');
var ModelBuilder = juggler.ModelBuilder;
var DataSource = juggler.DataSource;
var modeler = new ModelBuilder();
-var async = require('async');
var assert = require('assert');
var _ = require('underscore');
var SharedClass = require('strong-remoting').SharedClass;
@@ -222,26 +221,34 @@ Model._ACL = function getACL(ACL) {
return _aclModel;
};
-
/**
* Check if the given access token can invoke the method
*
* @param {AccessToken} token The access token
- * @param {*} modelId The model id
- * @param {String} method The method name
- * @param callback The callback function
- *
- * @callback {Function} callback
+ * @param {*} modelId The model ID.
+ * @param {SharedMethod} sharedMethod The method in question
+ * @callback {Function} callback The callback function
* @param {String|Error} err The error object
- * @param {Boolean} allowed is the request allowed
+ * @param {Boolean} allowed True if the request is allowed; false otherwise.
*/
-Model.checkAccess = function(token, modelId, method, callback) {
+Model.checkAccess = function(token, modelId, sharedMethod, callback) {
var ANONYMOUS = require('./access-token').ANONYMOUS;
token = token || ANONYMOUS;
var aclModel = Model._ACL();
- var methodName = 'string' === typeof method? method: method && method.name;
- aclModel.checkAccessForToken(token, this.modelName, modelId, methodName, callback);
+
+ aclModel.checkAccessForContext({
+ accessToken: token,
+ model: this,
+ property: sharedMethod.name,
+ method: sharedMethod.name,
+ sharedMethod: sharedMethod,
+ modelId: modelId,
+ accessType: this._getAccessTypeForMethod(sharedMethod)
+ }, function(err, accessRequest) {
+ if(err) return callback(err);
+ callback(null, accessRequest.isAllowed());
+ });
};
/*!
@@ -370,3 +377,4 @@ Model.scopeRemoting = function(relationName, relation, define) {
// setup the initial model
Model.setup();
+
diff --git a/lib/models/role.js b/lib/models/role.js
index 8b64796f..072b7a76 100644
--- a/lib/models/role.js
+++ b/lib/models/role.js
@@ -319,12 +319,13 @@ Role.registerResolver(Role.EVERYONE, function (role, context, callback) {
* @param {Boolean} isInRole
*/
Role.isInRole = function (role, context, callback) {
- debug('isInRole(): %s %j', role, context);
-
if (!(context instanceof AccessContext)) {
context = new AccessContext(context);
}
+ debug('isInRole(): %s', role);
+ context.debug();
+
var resolver = Role.resolvers[role];
if (resolver) {
debug('Custom resolver found for role %s', role);
@@ -409,8 +410,6 @@ Role.isInRole = function (role, context, callback) {
* @param {String[]} An array of role ids
*/
Role.getRoles = function (context, callback) {
- debug('getRoles(): %j', context);
-
if(!(context instanceof AccessContext)) {
context = new AccessContext(context);
}
diff --git a/lib/models/user.js b/lib/models/user.js
index ae3ad569..c06493ee 100644
--- a/lib/models/user.js
+++ b/lib/models/user.js
@@ -8,8 +8,6 @@ var PersistedModel = require('../loopback').PersistedModel
, SALT_WORK_FACTOR = 10
, crypto = require('crypto')
, bcrypt = require('bcryptjs')
- , passport = require('passport')
- , LocalStrategy = require('passport-local').Strategy
, BaseAccessToken = require('./access-token')
, DEFAULT_TTL = 1209600 // 2 weeks in seconds
, DEFAULT_RESET_PW_TTL = 15 * 60 // 15 mins in seconds
@@ -128,6 +126,23 @@ var options = {
var User = module.exports = PersistedModel.extend('User', properties, options);
+/**
+ * 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);
+};
+
/**
* Login a user by with the given `credentials`.
*
@@ -144,6 +159,7 @@ var User = module.exports = PersistedModel.extend('User', properties, options);
*/
User.login = function (credentials, include, fn) {
+ var self = this;
if (typeof include === 'function') {
fn = include;
include = undefined;
@@ -162,7 +178,7 @@ User.login = function (credentials, include, fn) {
return fn(err);
}
- this.findOne({where: query}, function(err, user) {
+ self.findOne({where: query}, function(err, user) {
var defaultError = new Error('login failed');
defaultError.statusCode = 401;
@@ -175,9 +191,7 @@ User.login = function (credentials, include, fn) {
debug('An error is reported from User.hasPassword: %j', err);
fn(defaultError);
} else if(isMatch) {
- user.accessTokens.create({
- ttl: Math.min(credentials.ttl || User.settings.ttl, User.settings.maxTTL)
- }, function(err, token) {
+ user.createAccessToken(credentials.ttl, function(err, token) {
if (err) return fn(err);
if (include === 'user') {
// NOTE(bajtos) We can't set token.user here:
diff --git a/package.json b/package.json
index a1601bf0..8132da28 100644
--- a/package.json
+++ b/package.json
@@ -34,10 +34,8 @@
"debug": "~0.8.1",
"express": "4.x",
"body-parser": "~1.2.2",
- "strong-remoting": "2.0.0-beta3",
+ "strong-remoting": "2.0.0-beta4",
"inflection": "~1.3.5",
- "passport": "~0.2.0",
- "passport-local": "~1.0.0",
"nodemailer": "~0.6.5",
"ejs": "~1.0.0",
"bcryptjs": "~0.7.12",
@@ -55,11 +53,11 @@
"errorhandler": "~1.0.1",
"serve-favicon": "~2.0.0",
"loopback-datasource-juggler": "2.0.0-beta1",
- "mocha": "~1.18.0",
+ "mocha": "~1.20.1",
"strong-task-emitter": "0.0.x",
"supertest": "~0.13.0",
"chai": "~1.9.1",
- "loopback-testing": "~0.1.3",
+ "loopback-testing": "~0.2.0",
"browserify": "~4.1.6",
"grunt": "~0.4.5",
"grunt-browserify": "~2.1.0",
diff --git a/test/access-control.integration.js b/test/access-control.integration.js
index 0ef59c7b..575baebd 100644
--- a/test/access-control.integration.js
+++ b/test/access-control.integration.js
@@ -70,8 +70,11 @@ describe('access control - integration', function () {
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForUser);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER,'GET', urlForUser);
- lt.it.shouldBeAllowedWhenCalledAnonymously('POST', '/api/users');
- lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users');
+ lt.it.shouldBeAllowedWhenCalledAnonymously(
+ 'POST', '/api/users', newUserData());
+
+ lt.it.shouldBeAllowedWhenCalledByUser(
+ CURRENT_USER, 'POST', '/api/users', newUserData());
lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users/logout');
@@ -117,6 +120,15 @@ describe('access control - integration', function () {
function urlForUser() {
return '/api/users/' + this.randomUser.id;
}
+
+ var userCounter;
+ function newUserData() {
+ userCounter = userCounter ? ++userCounter : 1;
+ return {
+ email: 'new-' + userCounter + '@test.test',
+ password: 'test'
+ };
+ }
});
describe('/banks', function () {
diff --git a/test/acl.test.js b/test/acl.test.js
index 3f8892bd..ba97dda9 100644
--- a/test/acl.test.js
+++ b/test/acl.test.js
@@ -102,11 +102,15 @@ describe('security ACLs', function () {
property: 'find',
accessType: 'WRITE'
};
+
+ acls = acls.map(function(a) { return new ACL(a)});
+
var perm = ACL.resolvePermission(acls, req);
assert.deepEqual(perm, { model: 'account',
property: 'find',
accessType: 'WRITE',
- permission: 'ALLOW' });
+ permission: 'ALLOW',
+ methodNames: []});
});
it("should allow access to models for the given principal by wildcard", function () {
@@ -297,7 +301,6 @@ describe('security ACLs', function () {
});
});
});
-
});
diff --git a/test/app.test.js b/test/app.test.js
index f7b6d609..8f9be923 100644
--- a/test/app.test.js
+++ b/test/app.test.js
@@ -38,6 +38,15 @@ describe('app', function() {
expect(classes).to.contain('color');
});
+ it('registers existing models to app.models', function() {
+ var Color = db.createModel('color', {name: String});
+ app.model(Color);
+ expect(Color.app).to.be.equal(app);
+ expect(Color.shared).to.equal(true);
+ expect(app.models.color).to.equal(Color);
+ expect(app.models.Color).to.equal(Color);
+ });
+
it.onServer('updates REST API when a new model is added', function(done) {
app.use(loopback.rest());
request(app).get('/colors').expect(404, function(err, res) {
@@ -120,6 +129,24 @@ describe('app', function() {
expect(app.models.foo.definition.settings.base).to.equal('Application');
});
+
+ it('honors config.public options', function() {
+ app.model('foo', {
+ dataSource: 'db',
+ public: false
+ });
+ expect(app.models.foo.app).to.equal(app);
+ expect(app.models.foo.shared).to.equal(false);
+ });
+
+ it('defaults config.public to be true', function() {
+ app.model('foo', {
+ dataSource: 'db'
+ });
+ expect(app.models.foo.app).to.equal(app);
+ expect(app.models.foo.shared).to.equal(true);
+ });
+
});
describe('app.model(ModelCtor, config)', function() {
diff --git a/test/user.test.js b/test/user.test.js
index 25b1df8f..2b3805a1 100644
--- a/test/user.test.js
+++ b/test/user.test.js
@@ -1,6 +1,5 @@
var User;
var AccessToken = loopback.AccessToken;
-var passport = require('passport');
var MailConnector = require('../lib/connectors/mail');
var userMemory = loopback.createDataSource({
@@ -9,6 +8,7 @@ var userMemory = loopback.createDataSource({
describe('User', function(){
var validCredentials = {email: 'foo@bar.com', password: 'bar'};
+ var validCredentialsWithTTL = {email: 'foo@bar.com', password: 'bar', ttl: 3600};
var invalidCredentials = {email: 'foo1@bar.com', password: 'bar1'};
var incompleteCredentials = {password: 'bar1'};
@@ -117,6 +117,44 @@ describe('User', function(){
done();
});
});
+
+ it('Login a user by providing credentials with TTL', function(done) {
+ User.login(validCredentialsWithTTL, function (err, accessToken) {
+ assert(accessToken.userId);
+ assert(accessToken.id);
+ assert.equal(accessToken.ttl, validCredentialsWithTTL.ttl);
+ assert.equal(accessToken.id.length, 64);
+
+ done();
+ });
+ });
+
+ it('Login a user using a custom createAccessToken', function(done) {
+ var createToken = User.prototype.createAccessToken; // Save the original method
+ // Override createAccessToken
+ User.prototype.createAccessToken = function(ttl, cb) {
+ // Reduce the ttl by half for testing purpose
+ this.accessTokens.create({ttl: ttl /2 }, cb);
+ };
+ User.login(validCredentialsWithTTL, function (err, accessToken) {
+ assert(accessToken.userId);
+ assert(accessToken.id);
+ assert.equal(accessToken.ttl, 1800);
+ assert.equal(accessToken.id.length, 64);
+
+ User.findById(accessToken.userId, function(err, user) {
+ user.createAccessToken(120, function (err, accessToken) {
+ assert(accessToken.userId);
+ assert(accessToken.id);
+ assert.equal(accessToken.ttl, 60);
+ assert.equal(accessToken.id.length, 64);
+ // Restore create access token
+ User.prototype.createAccessToken = createToken;
+ done();
+ });
+ });
+ });
+ });
it('Login a user over REST by providing credentials', function(done) {
request(app)