Merge branch 'master' into 2.0
Conflicts: docs.json lib/application.js lib/loopback.js lib/models/data-model.js lib/models/model.js lib/models/user.js lib/registry.js package.json test/app.test.js
This commit is contained in:
commit
d21669b844
|
@ -41,9 +41,9 @@ as illustrated below:
|
||||||
|
|
||||||
* Clients
|
* Clients
|
||||||
* [loopback-ios](https://github.com/strongloop/loopback-ios)
|
* [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-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)
|
* [loopback-angular](https://github.com/strongloop/loopback-angular)
|
||||||
|
|
||||||
* Tools
|
* Tools
|
||||||
|
|
19
docs.json
19
docs.json
|
@ -5,21 +5,22 @@
|
||||||
"lib/loopback.js",
|
"lib/loopback.js",
|
||||||
"lib/runtime.js",
|
"lib/runtime.js",
|
||||||
"lib/registry.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-token.js",
|
||||||
"lib/models/access-context.js",
|
|
||||||
"lib/models/acl.js",
|
"lib/models/acl.js",
|
||||||
"lib/models/application.js",
|
"lib/models/application.js",
|
||||||
"lib/models/email.js",
|
"lib/models/email.js",
|
||||||
"lib/models/model.js",
|
|
||||||
"lib/models/data-model.js",
|
|
||||||
"lib/models/role.js",
|
"lib/models/role.js",
|
||||||
"lib/models/user.js",
|
"lib/models/user.js",
|
||||||
"lib/models/change.js",
|
"lib/models/change.js"
|
||||||
"docs/api-datasource.md",
|
|
||||||
"docs/api-geopoint.md",
|
|
||||||
"docs/api-model.md",
|
|
||||||
"docs/api-model-remote.md"
|
|
||||||
],
|
],
|
||||||
"assets": "/docs/assets"
|
"assets": "/docs/assets"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
},
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -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.
|
|
|
@ -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 <a href="#argdesc">Argument description</a>. The `callback` argument is assumed; do not specify. |
|
|
||||||
| returns | No | Describes the remote method's callback arguments; See <a href="#argdesc">Argument description</a>. The `err` argument is assumed; do not specify. |
|
|
||||||
| http | No | HTTP routing information: <ul><li> **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`.</li><li> **http.verb**: HTTP method (verb) from which the method is available (one of: get, post, put, del, or all).</li></ul>
|
|
||||||
| description | No | A text description of the method. This is used by API documentation generators like Swagger.
|
|
||||||
|
|
||||||
<a name="argdesc"></a>
|
|
||||||
**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 <a href="#argdesc-http">below</a>.
|
|
||||||
|
|
||||||
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'}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
<a name="argdesc-http"></a>
|
|
||||||
**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).
|
|
|
@ -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)).
|
|
Binary file not shown.
Before Width: | Height: | Size: 601 KiB After Width: | Height: | Size: 325 KiB |
|
@ -114,58 +114,55 @@ app.disuse = function (route) {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
app.model = function (Model, config) {
|
app.model = function (Model, config) {
|
||||||
if(arguments.length === 1) {
|
var isPublic = true;
|
||||||
assert(Model.prototype instanceof registry.Model,
|
if (arguments.length > 1) {
|
||||||
'Model must be a descendant of loopback.Model');
|
config = config || {};
|
||||||
if(Model.sharedClass) {
|
if (typeof Model === 'string') {
|
||||||
this.remotes().addClass(Model.sharedClass);
|
// 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);
|
configureModel(Model, config, this);
|
||||||
Model.shared = true;
|
isPublic = config.public !== false;
|
||||||
Model.app = this;
|
} else {
|
||||||
Model.emit('attached', this);
|
assert(Model.prototype instanceof loopback.Model,
|
||||||
return 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;
|
var modelName = Model.modelName;
|
||||||
this.models[modelName] =
|
this.models[modelName] =
|
||||||
this.models[classify(modelName)] =
|
this.models[classify(modelName)] =
|
||||||
this.models[camelize(modelName)] = Model;
|
this.models[camelize(modelName)] = Model;
|
||||||
|
|
||||||
if (config.public !== false) {
|
this.models().push(Model);
|
||||||
this.model(Model);
|
|
||||||
|
if (isPublic && Model.sharedClass) {
|
||||||
|
this.remotes().addClass(Model.sharedClass);
|
||||||
|
clearHandlerCache(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Model.shared = isPublic;
|
||||||
|
Model.app = this;
|
||||||
|
Model.emit('attached', this);
|
||||||
return Model;
|
return Model;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the models exported by the app. Returns only models defined using `app.model()`
|
* Get the models exported by the app. Returns only models defined using `app.model()`
|
||||||
*
|
*
|
||||||
|
@ -337,7 +334,7 @@ app.enableAuth = function() {
|
||||||
Model.checkAccess(
|
Model.checkAccess(
|
||||||
req.accessToken,
|
req.accessToken,
|
||||||
modelId,
|
modelId,
|
||||||
method.name,
|
method,
|
||||||
function(err, allowed) {
|
function(err, allowed) {
|
||||||
// Emit any cached data events that fired while checking access.
|
// Emit any cached data events that fired while checking access.
|
||||||
req.resume();
|
req.resume();
|
||||||
|
|
|
@ -159,6 +159,7 @@ loopback.template = function (file) {
|
||||||
return ejs.compile(str);
|
return ejs.compile(str);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Built in models / services
|
* Built in models / services
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
/**
|
/*!
|
||||||
* Module dependencies.
|
* Module dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var loopback = require('../loopback');
|
var loopback = require('../loopback');
|
||||||
|
|
||||||
/**
|
/*!
|
||||||
* Export the middleware.
|
* Export the middleware.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = rest;
|
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() {
|
function rest() {
|
||||||
|
|
|
@ -1,9 +1,22 @@
|
||||||
/**
|
/*!
|
||||||
* Export the middleware.
|
* Export the middleware.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = status;
|
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() {
|
function status() {
|
||||||
var started = new Date();
|
var started = new Date();
|
||||||
|
|
||||||
|
|
|
@ -12,14 +12,17 @@ var assert = require('assert');
|
||||||
module.exports = token;
|
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
|
* - `access_token`
|
||||||
* - `headers` - An `Array` of header names
|
* - `X-Access-Token`
|
||||||
* - `params` - An `Array` of param names
|
* - `authorization`
|
||||||
* - `model` - Specify an AccessToken class to use
|
*
|
||||||
|
* 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
|
* The following example illustrates how to check for an `accessToken` in a custom cookie, query string parameter
|
||||||
* and header called `foo-auth`.
|
* and header called `foo-auth`.
|
||||||
|
@ -28,23 +31,16 @@ module.exports = token;
|
||||||
* app.use(loopback.token({
|
* app.use(loopback.token({
|
||||||
* cookies: ['foo-auth'],
|
* cookies: ['foo-auth'],
|
||||||
* headers: ['foo-auth', 'X-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) {
|
function token(options) {
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
/**
|
/*!
|
||||||
* Export the middleware.
|
* Export the middleware.
|
||||||
|
* See discussion in Connect pull request #954 for more details
|
||||||
|
* https://github.com/senchalabs/connect/pull/954.
|
||||||
*/
|
*/
|
||||||
module.exports = urlNotFound;
|
module.exports = urlNotFound;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert any request not handled so far to a 404 error
|
* Convert any request not handled so far to a 404 error
|
||||||
* to be handled by error-handling middleware.
|
* to be handled by error-handling middleware.
|
||||||
* See discussion in Connect pull request #954 for more details
|
* @header loopback.urlNotFound()
|
||||||
* https://github.com/senchalabs/connect/pull/954
|
|
||||||
*/
|
*/
|
||||||
function urlNotFound() {
|
function urlNotFound() {
|
||||||
return function raiseUrlNotFoundError(req, res, next) {
|
return function raiseUrlNotFoundError(req, res, next) {
|
||||||
|
|
|
@ -36,7 +36,18 @@ function AccessContext(context) {
|
||||||
this.property = context.property || AccessContext.ALL;
|
this.property = context.property || AccessContext.ALL;
|
||||||
|
|
||||||
this.method = context.method;
|
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.accessType = context.accessType || AccessContext.ALL;
|
||||||
this.accessToken = context.accessToken || AccessToken.ANONYMOUS;
|
this.accessToken = context.accessToken || AccessToken.ANONYMOUS;
|
||||||
|
|
||||||
|
@ -79,7 +90,6 @@ AccessContext.permissionOrder = {
|
||||||
DENY: 4
|
DENY: 4
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a principal to the context
|
* Add a principal to the context
|
||||||
* @param {String} principalType The principal type
|
* @param {String} principalType The principal type
|
||||||
|
@ -96,8 +106,6 @@ AccessContext.prototype.addPrincipal = function (principalType, principalId, pri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.principals.push(principal);
|
this.principals.push(principal);
|
||||||
|
|
||||||
debug('adding principal %j', principal);
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -213,7 +221,7 @@ Principal.prototype.equals = function (p) {
|
||||||
* @returns {AccessRequest}
|
* @returns {AccessRequest}
|
||||||
* @class
|
* @class
|
||||||
*/
|
*/
|
||||||
function AccessRequest(model, property, accessType, permission) {
|
function AccessRequest(model, property, accessType, permission, methodNames) {
|
||||||
if (!(this instanceof AccessRequest)) {
|
if (!(this instanceof AccessRequest)) {
|
||||||
return new AccessRequest(model, property, accessType);
|
return new AccessRequest(model, property, accessType);
|
||||||
}
|
}
|
||||||
|
@ -224,26 +232,20 @@ function AccessRequest(model, property, accessType, permission) {
|
||||||
this.property = obj.property || AccessContext.ALL;
|
this.property = obj.property || AccessContext.ALL;
|
||||||
this.accessType = obj.accessType || AccessContext.ALL;
|
this.accessType = obj.accessType || AccessContext.ALL;
|
||||||
this.permission = obj.permission || AccessContext.DEFAULT;
|
this.permission = obj.permission || AccessContext.DEFAULT;
|
||||||
|
this.methodNames = methodNames || [];
|
||||||
} else {
|
} else {
|
||||||
this.model = model || AccessContext.ALL;
|
this.model = model || AccessContext.ALL;
|
||||||
this.property = property || AccessContext.ALL;
|
this.property = property || AccessContext.ALL;
|
||||||
this.accessType = accessType || AccessContext.ALL;
|
this.accessType = accessType || AccessContext.ALL;
|
||||||
this.permission = permission || AccessContext.DEFAULT;
|
this.permission = permission || AccessContext.DEFAULT;
|
||||||
}
|
this.methodNames = methodNames || [];
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the request a wildcard
|
* Does the request contain any wildcards?
|
||||||
* @returns {boolean}
|
*
|
||||||
|
* @returns {Boolean}
|
||||||
*/
|
*/
|
||||||
AccessRequest.prototype.isWildcard = function () {
|
AccessRequest.prototype.isWildcard = function () {
|
||||||
return this.model === AccessContext.ALL ||
|
return this.model === AccessContext.ALL ||
|
||||||
|
@ -251,6 +253,47 @@ AccessRequest.prototype.isWildcard = function () {
|
||||||
this.accessType === AccessContext.ALL;
|
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.AccessContext = AccessContext;
|
||||||
module.exports.Principal = Principal;
|
module.exports.Principal = Principal;
|
||||||
module.exports.AccessRequest = AccessRequest;
|
module.exports.AccessRequest = AccessRequest;
|
||||||
|
|
|
@ -61,6 +61,7 @@ var ACLSchema = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the access type - READ/WRITE/EXEC
|
* Name of the access type - READ/WRITE/EXEC
|
||||||
|
* @property accessType {String} Name of the access type - READ/WRITE/EXEC
|
||||||
*/
|
*/
|
||||||
accessType: String,
|
accessType: String,
|
||||||
|
|
||||||
|
@ -114,17 +115,20 @@ ACL.SCOPE = Principal.SCOPE;
|
||||||
* Calculate the matching score for the given rule and request
|
* Calculate the matching score for the given rule and request
|
||||||
* @param {ACL} rule The ACL entry
|
* @param {ACL} rule The ACL entry
|
||||||
* @param {AccessRequest} req The request
|
* @param {AccessRequest} req The request
|
||||||
* @returns {number}
|
* @returns {Number}
|
||||||
*/
|
*/
|
||||||
ACL.getMatchingScore = function getMatchingScore(rule, req) {
|
ACL.getMatchingScore = function getMatchingScore(rule, req) {
|
||||||
var props = ['model', 'property', 'accessType'];
|
var props = ['model', 'property', 'accessType'];
|
||||||
var score = 0;
|
var score = 0;
|
||||||
|
|
||||||
for (var i = 0; i < props.length; i++) {
|
for (var i = 0; i < props.length; i++) {
|
||||||
// Shift the score by 4 for each of the properties as the weight
|
// Shift the score by 4 for each of the properties as the weight
|
||||||
score = score * 4;
|
score = score * 4;
|
||||||
var val1 = rule[props[i]] || ACL.ALL;
|
var val1 = rule[props[i]] || ACL.ALL;
|
||||||
var val2 = req[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
|
// Exact match
|
||||||
score += 3;
|
score += 3;
|
||||||
} else if (val1 === ACL.ALL) {
|
} else if (val1 === ACL.ALL) {
|
||||||
|
@ -186,6 +190,16 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) {
|
||||||
return score;
|
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
|
* Resolve permission from the ACLs
|
||||||
* @param {Object[]) acls The list of ACLs
|
* @param {Object[]) acls The list of ACLs
|
||||||
|
@ -200,14 +214,13 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
|
||||||
acls = acls.sort(function (rule1, rule2) {
|
acls = acls.sort(function (rule1, rule2) {
|
||||||
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
|
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
|
||||||
});
|
});
|
||||||
if(debug.enabled) {
|
|
||||||
debug('ACLs by order: %j', acls);
|
|
||||||
}
|
|
||||||
var permission = ACL.DEFAULT;
|
var permission = ACL.DEFAULT;
|
||||||
var score = 0;
|
var score = 0;
|
||||||
|
|
||||||
for (var i = 0; i < acls.length; i++) {
|
for (var i = 0; i < acls.length; i++) {
|
||||||
score = ACL.getMatchingScore(acls[i], req);
|
score = ACL.getMatchingScore(acls[i], req);
|
||||||
if (score < 0) {
|
if (score < 0) {
|
||||||
|
// the highest scored ACL did not match
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!req.isWildcard()) {
|
if (!req.isWildcard()) {
|
||||||
|
@ -215,11 +228,7 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
|
||||||
permission = acls[i].permission;
|
permission = acls[i].permission;
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
if(acls[i].model === req.model &&
|
if(req.exactlyMatches(acls[i])) {
|
||||||
acls[i].property === req.property &&
|
|
||||||
acls[i].accessType === req.accessType
|
|
||||||
) {
|
|
||||||
// We should stop at the exact match
|
|
||||||
permission = acls[i].permission;
|
permission = acls[i].permission;
|
||||||
break;
|
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,
|
var res = new AccessRequest(req.model, req.property, req.accessType,
|
||||||
permission || ACL.DEFAULT);
|
permission || ACL.DEFAULT);
|
||||||
return res;
|
return res;
|
||||||
|
@ -253,11 +270,9 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
|
||||||
property: acl.property || ACL.ALL,
|
property: acl.property || ACL.ALL,
|
||||||
principalType: acl.principalType,
|
principalType: acl.principalType,
|
||||||
principalId: acl.principalId, // TODO: Should it be a name?
|
principalId: acl.principalId, // TODO: Should it be a name?
|
||||||
accessType: acl.accessType,
|
accessType: acl.accessType || ACL.ALL,
|
||||||
permission: acl.permission
|
permission: acl.permission
|
||||||
}));
|
}));
|
||||||
|
|
||||||
staticACLs[staticACLs.length - 1].debug('Adding ACL');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
var prop = modelClass &&
|
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
|
* Check if the given principal is allowed to access the model/property
|
||||||
* @param {String} principalType The principal type
|
* @param {String} principalType The principal type.
|
||||||
* @param {String} principalId The principal id
|
* @param {String} principalId The principal ID.
|
||||||
* @param {String} model The model name
|
* @param {String} model The model name.
|
||||||
* @param {String} property The property/method/relation name
|
* @param {String} property The property/method/relation name.
|
||||||
* @param {String} accessType The access type
|
* @param {String} accessType The access type.
|
||||||
* @param {Function} callback The callback function
|
* @callback {Function} callback Callback function.
|
||||||
*
|
|
||||||
* @callback callback
|
|
||||||
* @param {String|Error} err The error object
|
* @param {String|Error} err The error object
|
||||||
* @param {AccessRequest} result The access permission
|
* @param {AccessRequest} result The access permission
|
||||||
*/
|
*/
|
||||||
|
@ -350,15 +363,16 @@ ACL.prototype.debug = function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the request has the permission to access
|
* Check if the request has the permission to access.
|
||||||
* @param {Object} context
|
* @options {Object} context See below.
|
||||||
* @property {Object[]} principals An array of principals
|
* @property {Object[]} principals An array of principals.
|
||||||
* @property {String|Model} model The model name or model class
|
* @property {String|Model} model The model name or model class.
|
||||||
* @property {*} id The model instance id
|
* @property {*} id The model instance ID.
|
||||||
* @property {String} property The property/method/relation name
|
* @property {String} property The property/method/relation name.
|
||||||
* @property {String} accessType The access type
|
* @property {String} accessType The access type: READE, WRITE, or EXEC.
|
||||||
* @param {Function} callback
|
* @param {Function} callback Callback function
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ACL.checkAccessForContext = function (context, callback) {
|
ACL.checkAccessForContext = function (context, callback) {
|
||||||
if(!(context instanceof AccessContext)) {
|
if(!(context instanceof AccessContext)) {
|
||||||
context = new AccessContext(context);
|
context = new AccessContext(context);
|
||||||
|
@ -367,11 +381,13 @@ ACL.checkAccessForContext = function (context, callback) {
|
||||||
var model = context.model;
|
var model = context.model;
|
||||||
var property = context.property;
|
var property = context.property;
|
||||||
var accessType = context.accessType;
|
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 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 effectiveACLs = [];
|
||||||
var staticACLs = this.getStaticACLs(model.modelName, property);
|
var staticACLs = this.getStaticACLs(model.modelName, property);
|
||||||
|
@ -404,9 +420,6 @@ ACL.checkAccessForContext = function (context, callback) {
|
||||||
inRoleTasks.push(function (done) {
|
inRoleTasks.push(function (done) {
|
||||||
roleModel.isInRole(acl.principalId, context,
|
roleModel.isInRole(acl.principalId, context,
|
||||||
function (err, inRole) {
|
function (err, inRole) {
|
||||||
if(debug.enabled) {
|
|
||||||
debug('In role %j: %j', acl.principalId, inRole);
|
|
||||||
}
|
|
||||||
if (!err && inRole) {
|
if (!err && inRole) {
|
||||||
effectiveACLs.push(acl);
|
effectiveACLs.push(acl);
|
||||||
}
|
}
|
||||||
|
@ -425,21 +438,20 @@ ACL.checkAccessForContext = function (context, callback) {
|
||||||
if(resolved && resolved.permission === ACL.DEFAULT) {
|
if(resolved && resolved.permission === ACL.DEFAULT) {
|
||||||
resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW;
|
resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW;
|
||||||
}
|
}
|
||||||
debug('checkAccessForContext() returns: %j', resolved);
|
debug('---Resolved---');
|
||||||
|
resolved.debug();
|
||||||
callback && callback(null, resolved);
|
callback && callback(null, resolved);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the given access token can invoke the method
|
* Check if the given access token can invoke the method
|
||||||
* @param {AccessToken} token The access token
|
* @param {AccessToken} token The access token
|
||||||
* @param {String} model The model name
|
* @param {String} model The model name
|
||||||
* @param {*} modelId The model id
|
* @param {*} modelId The model id
|
||||||
* @param {String} method The method name
|
* @param {String} method The method name
|
||||||
* @end
|
* @callback {Function} callback Callback function
|
||||||
* @callback {Function} callback
|
|
||||||
* @param {String|Error} err The error object
|
* @param {String|Error} err The error object
|
||||||
* @param {Boolean} allowed is the request allowed
|
* @param {Boolean} allowed is the request allowed
|
||||||
*/
|
*/
|
||||||
|
@ -454,10 +466,6 @@ ACL.checkAccessForToken = function (token, model, modelId, method, callback) {
|
||||||
modelId: modelId
|
modelId: modelId
|
||||||
});
|
});
|
||||||
|
|
||||||
context.accessType = context.model._getAccessTypeForMethod(method);
|
|
||||||
|
|
||||||
context.debug();
|
|
||||||
|
|
||||||
this.checkAccessForContext(context, function (err, access) {
|
this.checkAccessForContext(context, function (err, access) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback && callback(err);
|
callback && callback(err);
|
||||||
|
|
|
@ -46,7 +46,7 @@ var PushNotificationSettingSchema = {
|
||||||
gcm: GcmSettingsSchema
|
gcm: GcmSettingsSchema
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/*!
|
||||||
* Data model for Application
|
* Data model for Application
|
||||||
*/
|
*/
|
||||||
var ApplicationSchema = {
|
var ApplicationSchema = {
|
||||||
|
@ -133,10 +133,10 @@ Application.beforeCreate = function (next) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new application
|
* Register a new application
|
||||||
* @param owner Owner's user id
|
* @param {String} owner Owner's user ID.
|
||||||
* @param name Name of the application
|
* @param {String} name Name of the application
|
||||||
* @param options Other options
|
* @param {Object} options Other options
|
||||||
* @param cb Callback function
|
* @param {Function} callback Callback function
|
||||||
*/
|
*/
|
||||||
Application.register = function (owner, name, options, cb) {
|
Application.register = function (owner, name, options, cb) {
|
||||||
assert(owner, 'owner is required');
|
assert(owner, 'owner is required');
|
||||||
|
|
|
@ -7,7 +7,6 @@ var juggler = require('loopback-datasource-juggler');
|
||||||
var ModelBuilder = juggler.ModelBuilder;
|
var ModelBuilder = juggler.ModelBuilder;
|
||||||
var DataSource = juggler.DataSource;
|
var DataSource = juggler.DataSource;
|
||||||
var modeler = new ModelBuilder();
|
var modeler = new ModelBuilder();
|
||||||
var async = require('async');
|
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
var _ = require('underscore');
|
var _ = require('underscore');
|
||||||
var SharedClass = require('strong-remoting').SharedClass;
|
var SharedClass = require('strong-remoting').SharedClass;
|
||||||
|
@ -222,26 +221,34 @@ Model._ACL = function getACL(ACL) {
|
||||||
return _aclModel;
|
return _aclModel;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the given access token can invoke the method
|
* Check if the given access token can invoke the method
|
||||||
*
|
*
|
||||||
* @param {AccessToken} token The access token
|
* @param {AccessToken} token The access token
|
||||||
* @param {*} modelId The model id
|
* @param {*} modelId The model ID.
|
||||||
* @param {String} method The method name
|
* @param {SharedMethod} sharedMethod The method in question
|
||||||
* @param callback The callback function
|
* @callback {Function} callback The callback function
|
||||||
*
|
|
||||||
* @callback {Function} callback
|
|
||||||
* @param {String|Error} err The error object
|
* @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;
|
var ANONYMOUS = require('./access-token').ANONYMOUS;
|
||||||
token = token || ANONYMOUS;
|
token = token || ANONYMOUS;
|
||||||
var aclModel = Model._ACL();
|
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
|
// setup the initial model
|
||||||
Model.setup();
|
Model.setup();
|
||||||
|
|
||||||
|
|
|
@ -319,12 +319,13 @@ Role.registerResolver(Role.EVERYONE, function (role, context, callback) {
|
||||||
* @param {Boolean} isInRole
|
* @param {Boolean} isInRole
|
||||||
*/
|
*/
|
||||||
Role.isInRole = function (role, context, callback) {
|
Role.isInRole = function (role, context, callback) {
|
||||||
debug('isInRole(): %s %j', role, context);
|
|
||||||
|
|
||||||
if (!(context instanceof AccessContext)) {
|
if (!(context instanceof AccessContext)) {
|
||||||
context = new AccessContext(context);
|
context = new AccessContext(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug('isInRole(): %s', role);
|
||||||
|
context.debug();
|
||||||
|
|
||||||
var resolver = Role.resolvers[role];
|
var resolver = Role.resolvers[role];
|
||||||
if (resolver) {
|
if (resolver) {
|
||||||
debug('Custom resolver found for role %s', role);
|
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
|
* @param {String[]} An array of role ids
|
||||||
*/
|
*/
|
||||||
Role.getRoles = function (context, callback) {
|
Role.getRoles = function (context, callback) {
|
||||||
debug('getRoles(): %j', context);
|
|
||||||
|
|
||||||
if(!(context instanceof AccessContext)) {
|
if(!(context instanceof AccessContext)) {
|
||||||
context = new AccessContext(context);
|
context = new AccessContext(context);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,6 @@ var PersistedModel = require('../loopback').PersistedModel
|
||||||
, SALT_WORK_FACTOR = 10
|
, SALT_WORK_FACTOR = 10
|
||||||
, crypto = require('crypto')
|
, crypto = require('crypto')
|
||||||
, bcrypt = require('bcryptjs')
|
, bcrypt = require('bcryptjs')
|
||||||
, passport = require('passport')
|
|
||||||
, LocalStrategy = require('passport-local').Strategy
|
|
||||||
, BaseAccessToken = require('./access-token')
|
, BaseAccessToken = require('./access-token')
|
||||||
, DEFAULT_TTL = 1209600 // 2 weeks in seconds
|
, DEFAULT_TTL = 1209600 // 2 weeks in seconds
|
||||||
, DEFAULT_RESET_PW_TTL = 15 * 60 // 15 mins 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);
|
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`.
|
* 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) {
|
User.login = function (credentials, include, fn) {
|
||||||
|
var self = this;
|
||||||
if (typeof include === 'function') {
|
if (typeof include === 'function') {
|
||||||
fn = include;
|
fn = include;
|
||||||
include = undefined;
|
include = undefined;
|
||||||
|
@ -162,7 +178,7 @@ User.login = function (credentials, include, fn) {
|
||||||
return fn(err);
|
return fn(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.findOne({where: query}, function(err, user) {
|
self.findOne({where: query}, function(err, user) {
|
||||||
var defaultError = new Error('login failed');
|
var defaultError = new Error('login failed');
|
||||||
defaultError.statusCode = 401;
|
defaultError.statusCode = 401;
|
||||||
|
|
||||||
|
@ -175,9 +191,7 @@ User.login = function (credentials, include, fn) {
|
||||||
debug('An error is reported from User.hasPassword: %j', err);
|
debug('An error is reported from User.hasPassword: %j', err);
|
||||||
fn(defaultError);
|
fn(defaultError);
|
||||||
} else if(isMatch) {
|
} else if(isMatch) {
|
||||||
user.accessTokens.create({
|
user.createAccessToken(credentials.ttl, function(err, token) {
|
||||||
ttl: Math.min(credentials.ttl || User.settings.ttl, User.settings.maxTTL)
|
|
||||||
}, function(err, token) {
|
|
||||||
if (err) return fn(err);
|
if (err) return fn(err);
|
||||||
if (include === 'user') {
|
if (include === 'user') {
|
||||||
// NOTE(bajtos) We can't set token.user here:
|
// NOTE(bajtos) We can't set token.user here:
|
||||||
|
|
|
@ -34,10 +34,8 @@
|
||||||
"debug": "~0.8.1",
|
"debug": "~0.8.1",
|
||||||
"express": "4.x",
|
"express": "4.x",
|
||||||
"body-parser": "~1.2.2",
|
"body-parser": "~1.2.2",
|
||||||
"strong-remoting": "2.0.0-beta3",
|
"strong-remoting": "2.0.0-beta4",
|
||||||
"inflection": "~1.3.5",
|
"inflection": "~1.3.5",
|
||||||
"passport": "~0.2.0",
|
|
||||||
"passport-local": "~1.0.0",
|
|
||||||
"nodemailer": "~0.6.5",
|
"nodemailer": "~0.6.5",
|
||||||
"ejs": "~1.0.0",
|
"ejs": "~1.0.0",
|
||||||
"bcryptjs": "~0.7.12",
|
"bcryptjs": "~0.7.12",
|
||||||
|
@ -55,11 +53,11 @@
|
||||||
"errorhandler": "~1.0.1",
|
"errorhandler": "~1.0.1",
|
||||||
"serve-favicon": "~2.0.0",
|
"serve-favicon": "~2.0.0",
|
||||||
"loopback-datasource-juggler": "2.0.0-beta1",
|
"loopback-datasource-juggler": "2.0.0-beta1",
|
||||||
"mocha": "~1.18.0",
|
"mocha": "~1.20.1",
|
||||||
"strong-task-emitter": "0.0.x",
|
"strong-task-emitter": "0.0.x",
|
||||||
"supertest": "~0.13.0",
|
"supertest": "~0.13.0",
|
||||||
"chai": "~1.9.1",
|
"chai": "~1.9.1",
|
||||||
"loopback-testing": "~0.1.3",
|
"loopback-testing": "~0.2.0",
|
||||||
"browserify": "~4.1.6",
|
"browserify": "~4.1.6",
|
||||||
"grunt": "~0.4.5",
|
"grunt": "~0.4.5",
|
||||||
"grunt-browserify": "~2.1.0",
|
"grunt-browserify": "~2.1.0",
|
||||||
|
|
|
@ -70,8 +70,11 @@ describe('access control - integration', function () {
|
||||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForUser);
|
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForUser);
|
||||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER,'GET', urlForUser);
|
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER,'GET', urlForUser);
|
||||||
|
|
||||||
lt.it.shouldBeAllowedWhenCalledAnonymously('POST', '/api/users');
|
lt.it.shouldBeAllowedWhenCalledAnonymously(
|
||||||
lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users');
|
'POST', '/api/users', newUserData());
|
||||||
|
|
||||||
|
lt.it.shouldBeAllowedWhenCalledByUser(
|
||||||
|
CURRENT_USER, 'POST', '/api/users', newUserData());
|
||||||
|
|
||||||
lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users/logout');
|
lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users/logout');
|
||||||
|
|
||||||
|
@ -117,6 +120,15 @@ describe('access control - integration', function () {
|
||||||
function urlForUser() {
|
function urlForUser() {
|
||||||
return '/api/users/' + this.randomUser.id;
|
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 () {
|
describe('/banks', function () {
|
||||||
|
|
|
@ -102,11 +102,15 @@ describe('security ACLs', function () {
|
||||||
property: 'find',
|
property: 'find',
|
||||||
accessType: 'WRITE'
|
accessType: 'WRITE'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
acls = acls.map(function(a) { return new ACL(a)});
|
||||||
|
|
||||||
var perm = ACL.resolvePermission(acls, req);
|
var perm = ACL.resolvePermission(acls, req);
|
||||||
assert.deepEqual(perm, { model: 'account',
|
assert.deepEqual(perm, { model: 'account',
|
||||||
property: 'find',
|
property: 'find',
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
permission: 'ALLOW' });
|
permission: 'ALLOW',
|
||||||
|
methodNames: []});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow access to models for the given principal by wildcard", function () {
|
it("should allow access to models for the given principal by wildcard", function () {
|
||||||
|
@ -297,7 +301,6 @@ describe('security ACLs', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,15 @@ describe('app', function() {
|
||||||
expect(classes).to.contain('color');
|
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) {
|
it.onServer('updates REST API when a new model is added', function(done) {
|
||||||
app.use(loopback.rest());
|
app.use(loopback.rest());
|
||||||
request(app).get('/colors').expect(404, function(err, res) {
|
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');
|
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() {
|
describe('app.model(ModelCtor, config)', function() {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
var User;
|
var User;
|
||||||
var AccessToken = loopback.AccessToken;
|
var AccessToken = loopback.AccessToken;
|
||||||
var passport = require('passport');
|
|
||||||
var MailConnector = require('../lib/connectors/mail');
|
var MailConnector = require('../lib/connectors/mail');
|
||||||
|
|
||||||
var userMemory = loopback.createDataSource({
|
var userMemory = loopback.createDataSource({
|
||||||
|
@ -9,6 +8,7 @@ var userMemory = loopback.createDataSource({
|
||||||
|
|
||||||
describe('User', function(){
|
describe('User', function(){
|
||||||
var validCredentials = {email: 'foo@bar.com', password: 'bar'};
|
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 invalidCredentials = {email: 'foo1@bar.com', password: 'bar1'};
|
||||||
var incompleteCredentials = {password: 'bar1'};
|
var incompleteCredentials = {password: 'bar1'};
|
||||||
|
|
||||||
|
@ -117,6 +117,44 @@ describe('User', function(){
|
||||||
done();
|
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) {
|
it('Login a user over REST by providing credentials', function(done) {
|
||||||
request(app)
|
request(app)
|
||||||
|
|
Loading…
Reference in New Issue