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
|
||||
* [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
|
||||
|
|
19
docs.json
19
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"
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
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();
|
||||
|
|
|
@ -159,6 +159,7 @@ loopback.template = function (file) {
|
|||
return ejs.compile(str);
|
||||
};
|
||||
|
||||
|
||||
/*!
|
||||
* Built in models / services
|
||||
*/
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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 () {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue