Merge branch 'release/1.3.1' into production

This commit is contained in:
Ritchie Martori 2013-12-04 14:58:40 -08:00
commit e554a07fe0
42 changed files with 2161 additions and 2281 deletions

1
.jshintignore Normal file
View File

@ -0,0 +1 @@
node_modules

13
.jshintrc Normal file
View File

@ -0,0 +1,13 @@
{
"node": true,
"camelcase" : true,
"eqnull" : true,
"indent": 2,
"undef": true,
"quotmark": "single",
"maxlen": 80,
"trailing": true,
"newcap": true,
"nonew": true,
"undef": false
}

View File

@ -2,7 +2,7 @@
## Documentation
[See the full documentation](http://docs.strongloop.com/loopback).
[See the full documentation](http://docs.strongloop.com/display/DOC/LoopBack).
## Mailing List
@ -19,3 +19,5 @@ Read about the [features of LoopBack](https://github.com/strongloop/loopback/wik
## Contributing
A brief guide for [contributing to LoopBack projects](https://github.com/strongloop/loopback/wiki/How-To-Contribute).
[![githalytics.com alpha](https://cruel-carlota.pagodabox.com/8caf6b5cce4be4d13c01ea9aafc9f407 "githalytics.com")](http://githalytics.com/strongloop/loopback)

View File

@ -1,17 +1,12 @@
{
"title": "LoopBack Documentation",
"content": [
"docs/intro.md",
"docs/quickstart.md",
"docs/apiexplorer.md",
"docs/concepts.md",
"docs/bundled-models.md",
"docs/cli.md",
"docs/types.md",
"docs/api.md",
"docs/ios.md",
"docs/js.md",
"docs/java.md",
"docs/api-app.md",
"docs/api-datasource.md",
"docs/api-geopoint.md",
"docs/api-model.md",
"docs/api-model-remote.md",
"docs/rest.md"
],
"assets": "/docs/assets"

256
docs/api-app.md Normal file
View File

@ -0,0 +1,256 @@
## App object
The App object represents a Loopback application.
The App object extends [Express](http://expressjs.com/api.html#express) and supports [Express / Connect middleware](http://expressjs.com/api.html#middleware). See [Express documentation](http://expressjs.com/api.html) for details.
```js
var loopback = require('loopback');
var app = loopback();
app.get('/', function(req, res){
res.send('hello world');
});
app.listen(3000);
```
## Properties
### models
The `app.models` object has properties for all defined models. In the following example the `Product` and `CustomerReceipt` models are accessed using the `models` object.
**NOTE:** you must call `app.boot()` to create the `app.models` object.
```js
var loopback = require('loopback');
var app = loopback();
app.boot({
dataSources: {
db: {connector: 'memory'}
}
});
app.model('product', {dataSource: 'db'});
app.model('customer-receipt', {dataSource: 'db'});
// available based on the given name
var Product = app.models.Product;
// also available as camelCase
var product = app.models.product;
// multi-word models are avaiable as pascal cased
var CustomerReceipt = app.models.CustomerReceipt;
// also available as camelCase
var customerReceipt = app.models.customerReceipt;
```
## Methods
### app.boot([options])
Initialize an application from an options object or a set of JSON and JavaScript files.
**What happens during an app _boot_?**
1. **DataSources** are created from an `options.dataSources` object or `datasources.json` in the current directory
2. **Models** are created from an `options.models` object or `models.json` in the current directory
3. Any JavaScript files in the `./models` directory are loaded with `require()`.
**Options**
- `cwd` - _optional_ - the directory to use when loading JSON and JavaScript files
- `models` - _optional_ - an object containing `Model` definitions
- `dataSources` - _optional_ - an object containing `DataSource` definitions
> **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple files may result
> in models being **undefined** due to race conditions. To avoid this when using `app.boot()`
> make sure all models are passed as part of the `models` definition.
<a name="model-definition"></a>
**Model Definitions**
The following is an example of an object containing two `Model` definitions: "location" and "inventory".
```js
{
"dealership": {
// a reference, by name, to a dataSource definition
"dataSource": "my-db",
// the options passed to Model.extend(name, properties, options)
"options": {
"relationships": {
"cars": {
"type": "hasMany",
"model": "Car",
"foreignKey": "dealerId"
}
},
"remoteMethods": {
"nearby": {
"description": "Find nearby locations around the geo point",
"accepts": [
{"arg": "here", "type": "GeoPoint", "required": true, "description": "geo location (lat & lng)"}
],
"returns": {"arg": "locations", "root": true}
}
}
},
// the properties passed to Model.extend(name, properties, options)
"properties": {
"id": {"id": true},
"name": "String",
"zip": "Number",
"address": "String"
}
},
"car": {
"dataSource": "my-db"
"properties": {
"id": {
"type": "String",
"required": true,
"id": true
},
"make": {
"type": "String",
"required": true
},
"model": {
"type": "String",
"required": true
}
}
}
}
```
**Model definition properties**
- `dataSource` - **required** - a string containing the name of the data source definition to attach the `Model` to
- `options` - _optional_ - an object containing `Model` options
- `properties` _optional_ - an object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language)
**DataSource definition properties**
- `connector` - **required** - the name of the [connector](#working-with-data-sources-and-connectors)
### model(name, definition)
Define a `Model` and export it for use by remote clients.
**definition**
- `public` - **default: true** attach the `Model` to the app and export its methods to clients
- `dataSource` - **required** the name of the `DataSource` to attach the `Model` to
Example:
```js
// declare a DataSource
app.boot({
dataSources: {
db: {
connector: 'mongodb',
url: 'mongodb://localhost:27015/my-database-name'
}
}
});
// describe a model
var modelDefinition = {dataSource: 'db'};
// create the model
var Product = app.model('product', modelDefinition);
// use the model api
Product.create({name: 'pencil', price: 0.99}, console.log);
```
**Note** - This will expose all [shared methods](#shared-methods) on the model.
You may also export an existing `Model` by calling `app.model(Model)` like the example below.
### models()
Get the app's exported models. Only models defined using `app.model()` will show up in this list.
```js
var models = app.models();
models.forEach(function (Model) {
console.log(Model.modelName); // color
});
```
### docs(options)
Enable swagger REST API documentation.
**Options**
- `basePath` The basepath for your API - eg. 'http://localhost:3000'.
**Example**
```js
// enable docs
app.docs({basePath: 'http://localhost:3000'});
```
Run your app then navigate to [the API explorer](http://petstore.swagger.wordnik.com/). Enter your API basepath to view your generated docs.
### app.use( router )
Expose models over specified router.
For example, to expose models over REST using the `loopback.rest` router:
```js
app.use(loopback.rest());
```
View generated REST documentation at [http://localhost:3000/_docs](http://localhost:3000/_docs).
### Middleware
LoopBack includes middleware similar to [Express / Connect middleware](http://expressjs.com/api.html#middleware).
#### loopback.token(options)
**Options**
- `cookies` - An `Array` of cookie names
- `headers` - An `Array` of header names
- `params` - An `Array` of param names
Each array is used to add additional keys to find an `accessToken` for a `request`.
The following example illustrates how to check for an `accessToken` in a custom cookie, query string parameter
and header called `foo-auth`.
```js
app.use(loopback.token({
cookies: ['foo-auth'],
headers: ['foo-auth', 'X-Foo-Auth'],
cookies: ['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.
```js
params.push('access_token');
headers.push('X-Access-Token');
headers.push('authorization');
cookies.push('access_token');
cookies.push('authorization');
```
> **NOTE:** The `loopback.token()` middleware will only check for [signed cookies](http://expressjs.com/api.html#req.signedCookies).

194
docs/api-datasource.md Normal file
View File

@ -0,0 +1,194 @@
## Data Source object
A Loopback `DataSource` provides [Models](#model) with the ability to manipulate data. Attaching a `DataSource` to a `Model` adds [instance methods](#instance-methods) and [static methods](#static-methods) to the `Model`. The added methods may be [remote methods](#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});
```
### 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
},
...
}
```

71
docs/api-geopoint.md Normal file
View File

@ -0,0 +1,71 @@
## 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'
});
```
Loopback models with a GeoPoint property and an attached Data Source may be queried using geo-spatial filters and sorting.
For example, the following code is an an example of finding 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`.
```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.
```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.

140
docs/api-model-remote.md Normal file
View File

@ -0,0 +1,140 @@
## 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.
### 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, as explained <a href="#argdesc">below</a>. The callback is an assumed argument; do not specify. |
| returns | No | Describes the remote method's callback arguments, as explained <a href="#argdesc">below</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>
<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.
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'}
]
```
## 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.user
A `Model` representing the user calling the method remotely. **Note:** this is undefined if the remote method is not invoked by a logged in user.
#### 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 `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).

520
docs/api-model.md Normal file
View File

@ -0,0 +1,520 @@
## 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**.
### Model.validatesFormatOf(property, options)
Require a model to include a property that matches the given format.
```js
User.validatesFormat('name', {with: /\w+/});
```
### Model.validatesPresenceOf(properties...)
Require a model to include a property to be considered valid.
```js
User.validatesPresenceOf('first', 'last', 'age');
```
### Model.validatesLengthOf(property, options)
Require a property length to be within a specified range.
```js
User.validatesLengthOf('password', {min: 5, message: {min: 'Password is too short'}});
```
### Model.validatesInclusionOf(property, options)
Require a value for `property` to be in the specified array.
```js
User.validatesInclusionOf('gender', {in: ['male', 'female']});
```
### Model.validatesExclusionOf(property, options)
Require a value for `property` to not exist in the specified array.
```js
User.validatesExclusionOf('domain', {in: ['www', 'billing', 'admin']});
```
### Model.validatesNumericalityOf(property, options)
Require a value for `property` to be a specific type of `Number`.
```js
User.validatesNumericalityOf('age', {int: true});
```
### Model.validatesUniquenessOf(property, options)
Ensure the value for `property` is unique in the collection of models.
```js
User.validatesUniquenessOf('email', {message: 'email is not unique'});
```
**Note:** not available for all [connectors](#connectors).
Currently supported in these connectors:
- [In Memory](#memory-connector)
- [Oracle](http://github.com/strongloop/loopback-connector-oracle)
- [MongoDB](http://github.com/strongloop/loopback-connector-mongodb)
### myModel.isValid()
Validate the model instance.
```js
user.isValid(function (valid) {
if (!valid) {
console.log(user.errors);
// => hash of errors
// => {
// => username: [errmessage, errmessage, ...],
// => email: ...
// => }
}
});
```
## 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(callback)
Delete all Model instances from data source. **Note:** destroyAll method does not perform destroy hooks.
### 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({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)).

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +0,0 @@
### Using the API Explorer
Follow these steps to explore the sample app's REST API:
1. Open your browser to http://localhost:3000/explorer. You'll see a list of REST API endpoints as illustrated below.
<img src="assets/explorer-listing.png" alt="API Explorer Listing" width="600" style="border: 1px solid gray; padding: 5px; margin: 10px;">
The endpoints are grouped by the model names. Each endpoint consists of a list
of operations for the model.
2. Click on one of the endpoint paths (such as /locations) to see available
operations for a given model. You'll see the CRUD operations mapped to HTTP verbs and paths.
<img src="assets/explorer-endpoint.png" alt="API Exlporer Endpoints" width="600" style="border: 1px solid gray; padding: 5px; margin: 10px;">
3. Click on a given operation to see the signature; for example, GET `/locations/{id}`:
<img src="assets/explorer-api.png" alt="API Spec" width="600" style="border: 1px solid gray; padding: 5px; margin: 10px;">
Notice that each operation has the HTTP verb, path, description, response model, and a list of request parameters.
4. Invoke an operation: fill in the parameters, then click the **Try it out!** button. You'll see something like this:
<img src="assets/explorer-req-res.png" alt="Request/Response" width="400" style="border: 1px solid gray; padding: 5px; margin: 10px 10px 10px 100px;">
You can see the request URL, the JSON in the response body, and the HTTP response code and headers.
<h3>Next Steps</h3>
To gain a deeper understanding of LoopBack and how it works, read the following sections, [Working with Models](#working-with-models) and [Working with Data Sources and Connectors](#working-with-data-sources-and-connectors).
For information on how StrongLoop Suite provides:
- Out-of-the-box scalability, see
[StrongNode](http://docs.strongloop.com/strongnode#quick-start).
- CPU profiling and path trace features, see
[StrongOps](http://docs.strongloop.com/strongops#quick-start).
- Mobile client APIs, see [LoopBack Client SDKs](http://docs.strongloop.com/loopback-clients/).

View File

@ -1,236 +0,0 @@
## Bundled Models
The Loopback library is unopinioned in the way you define your app's data and logic. Loopback also bundles useful pre-built models for common use cases.
- User - register and authenticate users of your app locally or against 3rd party services.
- Email - send emails to your app users using smtp or 3rd party services.
Defining a model with `loopback.createModel()` is really just extending the base `loopback.Model` type using `loopback.Model.extend()`. The bundled models extend from the base `loopback.Model` allowing you to extend them arbitrarily.
### User Model
Register and authenticate users of your app locally or against 3rd party services.
#### Define a User Model
Extend a vanilla Loopback model using the built in User model.
```js
// create a data source
var memory = loopback.memory();
// define a User model
var User = loopback.User.extend('user');
// attach to the memory connector
User.attachTo(memory);
// also attach the accessToken model to a data source
User.accessToken.attachTo(memory);
// expose over the app's api
app.model(User);
```
**Note:** By default the `loopback.User` model uses the `loopback.AccessToken` model to persist access tokens. You can change this by setting the `accessToken` property.
**Note:** You must attach both the `User` and `User.accessToken` model's to a data source!
#### User Creation
Create a user like any other model.
```js
// username and password are not required
User.create({email: 'foo@bar.com', password: 'bar'}, function(err, user) {
console.log(user);
});
```
#### Login a User
Create an `accessToken` for a user using the local auth strategy.
**Node.js**
```js
User.login({username: 'foo', password: 'bar'}, function(err, accessToken) {
console.log(accessToken);
});
```
**REST**
You must provide a username and password over rest. To ensure these values are encrypted, include these as part of the body and make sure you are serving your app over https (through a proxy or using the https node server).
```
POST
/users/login
...
{
"email": "foo@bar.com",
"password": "bar"
}
...
200 OK
{
"sid": "1234abcdefg",
"uid": "123"
}
```
#### Logout a User
**Node.js**
```js
// login a user and logout
User.login({"email": "foo@bar.com", "password": "bar"}, function(err, accessToken) {
User.logout(accessToken.id, function(err) {
// user logged out
});
});
// logout a user (server side only)
User.findOne({email: 'foo@bar.com'}, function(err, user) {
user.logout();
});
```
**REST**
```
POST /users/logout
...
{
"sid": "<accessToken id from user login>"
}
```
#### Verify Email Addresses
Require a user to verify their email address before being able to login. This will send an email to the user containing a link to verify their address. Once the user follows the link they will be redirected to `/` and be able to login normally.
```js
// first setup the mail datasource (see #mail-model for more info)
var mail = loopback.createDataSource({
connector: loopback.Mail,
transports: [{
type: 'smtp',
host: 'smtp.gmail.com',
secureConnection: true,
port: 465,
auth: {
user: 'you@gmail.com',
pass: 'your-password'
}
}]
});
User.email.attachTo(mail);
User.requireEmailVerfication = true;
User.afterRemote('create', function(ctx, user, next) {
var options = {
type: 'email',
to: user.email,
from: 'noreply@myapp.com',
subject: 'Thanks for Registering at FooBar',
text: 'Please verify your email address!'
template: 'verify.ejs',
redirect: '/'
};
user.verify(options, next);
});
```
#### Send Reset Password Email
Send an email to the user's supplied email address containing a link to reset their password.
```js
User.reset(email, function(err) {
console.log('email sent');
});
```
#### Remote Password Reset
The password reset email will send users to a page rendered by loopback with fields required to reset the user's password. You may customize this template by defining a `resetTemplate` setting.
```js
User.settings.resetTemplate = 'reset.ejs';
```
#### Remote Password Reset Confirmation
Confirm the password reset.
```js
User.confirmReset(token, function(err) {
console.log(err || 'your password was reset');
});
```
### AccessToken Model
Identify users by creating accessTokens when they connect to your loopback app. By default the `loopback.User` model uses the `loopback.AccessToken` model to persist accessTokens. You can change this by setting the `accessToken` property.
```js
// define a custom accessToken model
var MyAccessToken = loopback.AccessToken.extend('MyAccessToken');
// define a custom User model
var User = loopback.User.extend('user');
// use the custom accessToken model
User.accessToken = MyAccessToken;
// attach both AccessToken and User to a data source
User.attachTo(loopback.memory());
MyAccessToken.attachTo(loopback.memory());
```
### Email Model
Send emails from your loopback app.
```js
// extend a one-off model for sending email
var MyEmail = loopback.Email.extend('my-email');
// create a mail data source
var mail = loopback.createDataSource({
connector: loopback.Mail,
transports: [{
type: 'smtp',
host: 'smtp.gmail.com',
secureConnection: true,
port: 465,
auth: {
user: 'you@gmail.com',
pass: 'your-password'
}
}]
});
// attach the model
MyEmail.attachTo(mail);
// send an email
MyEmail.send({
to: 'foo@bar.com',
from: 'you@gmail.com',
subject: 'my subject',
text: 'my text',
html: 'my <em>html</em>'
}, function(err, mail) {
console.log('email sent!');
});
```
> NOTE: the mail connector uses [nodemailer](http://www.nodemailer.com/). See
> the [nodemailer docs](http://www.nodemailer.com/) for more info.

View File

@ -1,48 +0,0 @@
## Command Line Tool
StrongLoop Suite includes a command-line tool, `slc` (StrongLoop Command), for working with applications.
The `slc lb` command enables you to quickly create new LoopBack applications and models with the following sub-commands:
* [workspace](#workspace): create a new workspace, essentially a container for multiple projects.
* [project](#project): create a new application.
* [model](#model): create a new model for a LoopBack application.
For more information on the `slc` command, see [StrongLoop Control](/strongnode/#strongloop-control-slc).
### workspace
<pre>
slc lb workspace <i>wsname</i>
</pre>
Creates an empty directory named _wsname_. The argument is optional; default is "loopback-workspace".
A LoopBack workspace is essentially a container for application projects. It is not required to create an application, but may be helpful for organization.
### project
<pre>
slc lb project <i>app_name</i>
</pre>
Creates a LoopBack application called _appname_, where _appname_ is a valid JavaScript identifier.
This command creates a new directory called _appname_ in the current directory containing:
* app.js
* package.json
* modules directory, containing: <ul><li> app directory - contains config.json, index.js, and module.json files
</li>
<li> db directory - contains files index.js and module.json</li>
<li> docs directory - contains files config.json, index.js, and module.json; explorer directory</li></ul>
### model
<pre>
slc lb model <i>modelname</i>
</pre>
Creates a model named _modelname_ in an existing LoopBack application.
Provide the
`-i` or `--interactive` flag to be prompted through model
configuration. Use the `--data-source` flag to specify the name of a
custom data source; default is data source named "db".

View File

@ -1,163 +0,0 @@
## Working with Models
A LoopBack model consists of:
- Application data.
- Validation rules.
- Data access capabilities.
- Business logic.
Apps use the model API to display information to the user or trigger actions
on the models to interact with backend systems. LoopBack supports both "dynamic" schema-less models and "static", schema-driven models.
_Dynamic models_ require only a name. The format of the data are specified completely and flexibly by the client application. Well-suited for data that originates on the client, dynamic models enable you to persist data both between accessTokens and between devices without involving a schema.
_Static models_ require more code up front, with the format of the data specified completely in JSON. Well-suited to both existing data and large, intricate datasets, static models provide structure and
consistency to their data, preventing bugs that can result from unexpected data in the database.
Here is a simple example of creating and using a model.
<h3>Defining a Model</h3>
Consider an e-commerce app with `Product` and `Inventory` models.
A mobile client could use the `Product` model API to search through all of the
products in a database. A client could join the `Product` and `Inventory` data to
determine what products are in stock, or the `Product` model could provide a
server-side function (or [remote method](#remote-methods)) that aggregates this
information.
For example, the following code creates product and inventory models:
```js
var Model = require('loopback').Model;
var Product = Model.extend('product');
var Inventory = Model.extend('customer');
```
The above code creates two dynamic models, appropriate when data is "free form." However, some data sources, such as relational databases, require schemas. Additionally, schemas are valuable to enable data exchange and to validate or sanitize data from clients; see [Sanitizing and Validating Models](#sanitizing-and-validating-models).
<h3>Attaching a Model to a Data Source</h3>
A data source enables a model to access and modify data in backend system such as a relational database.
Attaching a model to a data source, enables the model to use the data source API. For example, as shown below, the [MongoDB Connector](http://docs.strongloop.com/loopback-connector-mongodb), mixes in a `create` method that you can use to store a new product in the database; for example:
```js
// Attach data sources
var db = loopback.createDataSource({
connector: require('loopback-connector-mongodb')
});
// Enable the model to use the MongoDB API
Product.attachTo(db);
// Create a new product in the database
Product.create({ name: 'widget', price: 99.99 }, function(err, widget) {
console.log(widget.id); // The product's id, added by MongoDB
});
```
Now the models have both data and behaviors. Next, you need to make the models available to mobile clients.
<h3>Exposing a Model to Mobile Clients</h3>
To expose a model to mobile clients, use one of LoopBack's remoting middleware modules.
This example uses the `app.rest` middleware to expose the `Product` Model's API over REST.
For more information on LoopBack's REST API, see [REST API](#rest-api).
```js
// Step 3: Create a LoopBack application
var app = loopback();
// Use the REST remoting middleware
app.use(loopback.rest());
// Expose the `Product` model
app.model(Product);
```
After this, you'll have the `Product` model with create, read, update, and delete (CRUD) functions working remotely
from mobile clients. At this point, the model is schema-less and the data are not checked.
<h3>Sanitizing and Validating Models</h3>
A *schema* provides a description of a model written in **LoopBack Definition Language**, a specific form of JSON. Once a schema is defined for a model, the model validates and sanitizes data before passing it on to a data store such as a database. A model with a schema is referred to as a _static model_.
For example, the following code defines a schema and assigns it to the product model. The schema defines two fields (columns): **name**, a string, and **price**, a number. The field **name** is a required value.
```js
var productSchema = {
"name": { "type": "string", "required": true },
"price": "number"
};
var Product = Model.extend('product', productSchema);
```
A schema imposes restrictions on the model: If a remote client tries to save a product with extra properties
(for example, `description`), those properties are removed before the app saves the data in the model.
Also, since `name` is a required value, the model will _only_ be saved if the product contains a value for the `name` property.
<h3>More Information</h3>
- Check out the model [REST API](#rest-api).
- Read the
[LoopBack Definition Language Guide](http://docs.strongloop.com/loopback-datasource-juggler#loopback-definition-language-guide).
- Browse the [Node.js model API](#model).
- Before you build your own, check out the [bundled models](#bundled-models).
- Expose custom behavior to clients using [remote methods](#remote-methods).
- See how to [define relationships](#relationships) between models.
## Working with Data Sources and Connectors
Data sources encapsulate business logic to exchange data between models and various back-end systems such as
relational databases, REST APIs, SOAP web services, storage services, and so on.
Data sources generally provide create, retrieve, update, and delete (CRUD) functions.
Models access data sources through _connectors_ that are extensible and customizable.
Connectors implement the data exchange logic using database drivers or other
client APIs. In general, application code does not use a connector directly.
Rather, the `DataSource` class provides an API to configure the underlying connector.
<h3> LoopBack Connectors</h3>
LoopBack provides several connectors, with more under development.
<table style="
margin-left: 1em;
border: solid 1px #AAAAAA;
border-collapse: collapse;
background-color: #F9F9F9;
font-size: 90%;
empty-cells:show;" border="1" cellpadding="5">
<thead>
<tr style="background-color: #C7C7C7;">
<th>Connector</th>
<th>GitHub Module</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="/loopback-datasource-juggler/">Memory</a></td>
<td>Built-in to <a href="https://github.com/strongloop/loopback-datasource-juggler">loopback-datasource-juggler</a></td>
</tr>
<tr>
<td><a href="/loopback-connector-mongodb/">MongoDB</a></td>
<td><a href="https://github.com/strongloop/loopback-connector-mongodb">loopback-connector-mongodb</a></td>
</tr>
<tr>
<td><a href="/loopback-connector-oracle/">Oracle</a></td>
<td><a href="https://github.com/strongloop/loopback-connector-oracle">loopback-connector-oracle</a></td>
</tr>
<tr>
<td><a href="/loopback-connector-mysql/">MySQL</a></td>
<td><a href="https://github.com/strongloop/loopback-connector-mysql">loopback-connector-mysql</a></td>
</tr>
<tr>
<td><a href="/loopback-connector-rest/">REST</a></td>
<td><a href="https://github.com/strongloop/loopback-connector-rest">loopback-connector-rest</a></td>
</tr>
</tbody>
</table>
For more information, see the [LoopBack DataSource and Connector Guide](/loopback-datasource-juggler/#loopback-datasource-and-connector-guide).

View File

@ -1,21 +0,0 @@
### Getting Started
<div class="row">
<div class="col-lg-6">
<a class="btn btn-success btn-lg btn-block" href="http://docs.strongloop.com/loopback-clients#ios">iOS SDK</a>
</div>
<div class="col-lg-6">
<a class="btn btn-success btn-lg btn-block" href="http://docs.strongloop.com/loopback-clients#android">Android SDK</a>
</div>
</div>
<br>
<div class="row">
<div class="col-lg-6">
<a class="btn btn-success btn-lg btn-block" href="http://docs.strongloop.com/loopback-clients#javascript">HTML5/JavaScript SDK</a>
</div>
<div class="col-lg-6">
<a class="btn btn-success btn-lg btn-block" href="http://docs.strongloop.com/loopback#rest">REST API</a>
</div>
</div>
---

View File

@ -1,30 +0,0 @@
<h1> LoopBack</h1>
LoopBack is a mobile backend framework that you can run in the cloud or on-premises.
It is built on [StrongNode](http://strongloop.com/strongloop-suite/strongnode/) and open-source Node.js modules. For more information on the advantages of using LoopBack, see [StrongLoop | LoopBack](http://strongloop.com/strongloop-suite/loopback/).
To gain a basic understanding of key LoopBack concepts, read the following [Overview](#overview) section. Then, dive right into creating an app in [Quick Start](#quick-start).
## Overview
LoopBack consists of:
* A library of Node.js modules for connecting mobile apps to data sources such as databases and REST APIs.
* A command line tool, `slc lb`, for creating and working with LoopBack applications.
* Client SDKs for native and web-based mobile clients.
As illustrated in the diagram below, a LoopBack application has three components:
+ **Models** that represent business data and behavior.
+ **Data sources and connectors**. Data sources are databases or other backend services such as REST APIs, SOAP web services, or storage services. Connectors provide apps access to enterprise data sources such as Oracle, MySQL, and MongoDB.
+ **Mobile clients** using the LoopBack client SDKs.
<img src="./assets/loopback-architecture.png" width="600" alt="StrongLoop Architecture"></img>
An app interacts with data sources through the LoopBack model API, available
[locally within Node.js](#model), [remotely over REST](#rest-api), and via native client
APIs for [iOS, Android, and HTML5](#mobile-clients). Using the API, apps can query databases, store data, upload files, send emails, create push notifications, register users, and perform other actions provided by data sources.
Mobile clients can call LoopBack server APIs directly using [Strong Remoting](/strong-remoting), a pluggable transport
layer that enables you to provide backend APIs over REST, WebSockets, and other transports.

View File

@ -1,3 +0,0 @@
## iOS API
See [LoopBack iOS SDK](http://docs.strongloop.com/loopback-clients/ios/api/annotated.html) for API reference documentation.

View File

@ -1,4 +0,0 @@
## Android API
For information on the LoopBack Android SDK, see [Loopback Android](http://docs.strongloop.com/loopback-android/).
For API documentation, see [LoopBack Android SDK API reference](http://docs.strongloop.com/loopback-android/api/index.html).

View File

@ -1,3 +0,0 @@
## Browser API
<h3><em>Stay tuned. Currently in development.</em></h3>

View File

@ -1,49 +0,0 @@
##Quick Start
This section will get you up and running with LoopBack and the StrongLoop sample app in just a few minutes.
### Prerequisites
You must have the `git` command-line tool installed to run the sample application.
If needed, download it at <http://git-scm.com/downloads> and install it.
On Linux systems, you must have root privileges to write to `/usr/share`.
**NOTE**: If you're using Windows or OSX and don't have a C compiler (Visual C++ on Windows or XCode on OSX) and command-line "make" tools installed, you will see errors such as these:
```sh
xcode-select: Error: No Xcode is selected. Use xcode-select -switch <path-to-xcode>,
or see the xcode-select manpage (man xcode-select) for further information.
...
Unable to load native module uvmon; some features may be unavailable without compiling it.
memwatch must be installed to use the instances feature
StrongOps not configured to monitor. Please refer to http://docs.strongloop.com/strong-agent for usage.
```
You will still be able to run the sample app, but StrongOps will not be able to collect certain statistics.
### Creating and Running the Sample App
Follow these steps to run the LoopBack sample app:
1. If you have not already done so, download and install [StrongLoop Suite](http://www.strongloop.com/get-started) or set up your cloud development platform.
2. Setup the StrongLoop Suite sample app with this command.
```sh
$ slc example
```
This command clones the sample app into a new directory
named `sls-sample-app` and installs all of its dependencies.
3. Run the sample application by entering this command:
```sh
$ cd sls-sample-app
$ slc run app
```
4. To see the app running in a browser, open <http://localhost:3000>. The app homepage lists sample requests you can make against the LoopBack REST API. Click the **GET** buttons to see the JSON data returned.
### About the sample app
The StrongLoop sample is a mobile app for "Blackpool," an imaginary military equipment rental dealer with outlets in major cities around the world. It enables customers (military commanders) to rent weapons and buy ammunition from Blackpool using their mobile phones. The app displays a map of nearby rental locations and see currently available weapons, which you can filter by price, ammo type and distance. Then, you can use the app to reserve the desired weapons and ammo.
Note that the sample app is the backend functionality only; that is, the app has a REST API, but no client app or UI to consume the interface.
For more details on the sample app, see [StrongLoop sls-sample-app](https://github.com/strongloop/sls-sample-app) in GitHub.

View File

@ -1,6 +0,0 @@
###Resources
LoopBack was created by [StrongLoop](http://www.strongloop.com). For more information, check out
the [Node Republic](http://www.strongloop.com/node-republic) - a community around Node and [StrongLoop](http://www.strongloop.com) products.
---

View File

@ -1,125 +1,26 @@
## REST API
The REST API allows clients to interact with the LoopBack models using HTTP.
The clients can be a web browser, a JavaScript program, a mobile SDK, a curl
script, or anything that can act as an HTTP client.
LoopBack automatically binds a model to a list of HTTP endpoints that provide
REST APIs for model instance data manipulations (CRUD) and other remote
operations.
We'll use a simple model called `Location` (locations for rental) to illustrate
what REST APIs are exposed by LoopBack.
By default, the REST APIs are mounted to `/<pluralFormOfTheModelName>`, for
example, `/locations`, to the base URL such as http://localhost:3000/.
### CRUD remote methods
For a model backed by a data source that supports CRUD operations, you'll see
the following endpoints:
- Model.create: POST /locations
- Model.upsert: PUT /locations
- Model.exists: GET /locations/:id/exists
- Model.findById: GET /locations/:id
- Model.find: GET /locations
- Model.findOne: GET /locations/findOne
- Model.deleteById: DELETE /locations/:id
- Model.count: GET /locations/count
- Model.prototype.updateAttributes: PUT /locations/:id
### Custom remote methods
To expose a JavaScript method as REST API, we can simply describe the method as
follows:
loopback.remoteMethod(
Location.nearby,
{
description: 'Find nearby locations around the geo point',
accepts: [
{arg: 'here', type: 'GeoPoint', required: true, description: 'geo location (lat & lng)'},
{arg: 'page', type: 'Number', description: 'number of pages (page size=10)'},
{arg: 'max', type: 'Number', description: 'max distance in miles'}
],
returns: {arg: 'locations', root: true},
http: {verb: 'POST', path: '/nearby'}
}
);
The remoting is defined using the following properties:
- description: Description of the REST API
- accepts: An array of parameters, each parameter has a name, a type, and an
optional description
- returns: Description of the return value
- http: Binding to the HTTP endpoint, including the verb and path
### Request Format
For POST and PUT requests, the request body must be JSON, with the Content-Type
header set to application/json.
#### Encode the JSON object as query string
LoopBack uses the syntax from [node-querystring](https://github.com/visionmedia/node-querystring)
to encode JSON objects or arrays as query string. For example,
user[name][first]=John&user[email]=callback@strongloop.com
==>
{ user: { name: { first: 'John' }, email: 'callback@strongloop.com' } }
user[names][]=John&user[names][]=Mary&user[email]=callback@strongloop.com
==>
{ user: { names: ['John', 'Mary'], email: 'callback@strongloop.com' }}
items=a&items=b
==> { items: ['a', 'b'] }
For more examples, please check out [node-querystring](https://github.com/visionmedia/node-querystring/blob/master/test/parse.js)
### Response Format
The response format for all requests is a JSON object or array if present. Some
responses have an empty body.
Whether a request succeeded is indicated by the HTTP status code. A 2xx status
code indicates success, whereas a 4xx status code indicates request related
issues. 5xx status code reports server side problems.
The response for an error is in the following format:
{
"error": {
"message": "could not find a model with id 1",
"stack": "Error: could not find a model with id 1\n ...",
"statusCode": 404
}
}
###Generated APIs
###create
##create
Create a new instance of the model and persist it into the data source
####Definition
**Definition**
POST /locations
####Arguments
**Arguments**
* **data** The model instance data
####Example Request
**Example**
Request:
curl -X POST -H "Content-Type:application/json" \
-d '{"name": "L1", "street": "107 S B St", "city": "San Mateo", "zipcode": "94401"}' \
http://localhost:3000/locations
####Example Response
Response:
{
"id": "96",
"street": "107 S B St",
@ -132,39 +33,32 @@ Create a new instance of the model and persist it into the data source
}
}
####Potential Errors
* None
**Errors**
None
###upsert
##upsert
Update an existing model instance or insert a new one into the data source
####Definition
**Definition**
PUT /locations
####Arguments
**Arguments**
* **data** The model instance data
**Examples**
####Example Request
Request - insert:
#####Insert
curl -X PUT -H "Content-Type:application/json" \
-d '{"name": "L1", "street": "107 S B St", "city": "San Mateo", "zipcode": "94401"}' \
http://localhost:3000/locations
#####Update
curl -X PUT -H "Content-Type:applicatin/json" \
-d '{"id": "98", "name": "L4", "street": "107 S B St", "city": "San Mateo", \
"zipcode": "94401"}' http://localhost:3000/locations
Response:
####Example Response
#####Insert
{
"id": "98",
"street": "107 S B St",
@ -177,7 +71,14 @@ Update an existing model instance or insert a new one into the data source
}
}
#####Update
Request - update:
curl -X PUT -H "Content-Type:applicatin/json" \
-d '{"id": "98", "name": "L4", "street": "107 S B St", "city": "San Mateo", \
"zipcode": "94401"}' http://localhost:3000/locations
Response:
{
"id": "98",
"street": "107 S B St",
@ -187,55 +88,57 @@ Update an existing model instance or insert a new one into the data source
}
####Potential Errors
* None
**Errors**
None
###exists
##exists
Check whether a model instance exists by ID in the data source.
Check whether a model instance exists by id in the data source
####Definition
**Definition**
GET /locations/exists
####Arguments
**Arguments**
* **id** The model id
**Example**
Request:
####Example Request
curl http://localhost:3000/locations/88/exists
####Example Response
Response:
{
"exists": true
}
####Potential Errors
* None
**Errors**
None
###findById
##findById
Find a model instance by ID from the data source.
Find a model instance by id from the data source
####Definition
**Definition**
GET /locations/{id}
####Arguments
**Arguments**
* **id** The model id
**Example**
Request:
####Example Request
curl http://localhost:3000/locations/88
####Example Response
Response:
{
"id": "88",
@ -249,53 +152,59 @@ Find a model instance by id from the data source
}
}
####Potential Errors
* None
**Errors**
None
###find
##find
Find all instances of the model matched by filter from the data source.
Find all instances of the model matched by filter from the data source
####Definition
**Definition**
GET /locations
####Arguments
* **filter** The filter that defines where, order, fields, skip, and limit
**Arguments**
- **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
Pass the arguments as the value of the `find` HTTP query parameter, as follows
- **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
/modelName?filter=[filterType1]=<val1>&filter[filterType2]=<val2>...
- **order** `String` The sorting order
- Format: 'key1 ASC, key2 DESC'
where *filterType1*, *filterType2*, and so on, are the filter types, and *val1*, *val2* are the corresponding
values, as described in the following table.
- **limit** `Number` The maximum number of instances to be returned
- **skip** `Number` Skip the number of instances
- **offset** `Number` Alias for skip
| Filter type | Type | Description |
| ------------- | ------------- | ---------------|
| where | Object | Search criteria. Format: `{key: val}` or `{key: {op: val}}` For list of valid operations, see Operations, below. |
| include | String, Object, or Array | Allows you to load relations of several objects and optimize numbers of requests. For format, see Include format, below. |
| order | String | Sort order. Format: 'key1 ASC, key2 DESC', where ASC specifies ascending and DESC specifies descending order. |
|limit| Number | Maximum number of instances to return. |
|skip (offset) | Number | Skip the specified number of instances. Use offset as alternative. |
|fields| Object, Array, or String | The included/excluded fields. For foramt, see fields below.
**Operations available in where filter**:
* gt: >
* gte: >=
* lt: <
* lte: <=
* between
* inq: IN
* nin: NOT IN
* neq: !=
* like: LIKE
* nlike: NOT LIKE
**Include 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
**Fields format**:
- **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
@ -315,17 +224,21 @@ For example,
- '/locations?filter[where][geo][near]=153.536,-28.1&filter[limit]=3': The 3 closest locations to a given geo point
####Example Request
**Example**
Request:
Find without filter:
#####Find without filter
curl http://localhost:3000/locations
#####Find with a filter
Find with a filter:
curl http://localhost:3000/locations?filter%5Blimit%5D=2
**Note**: For curl, `[` needs to be encoded as `%5B`, and `]` as `%5D`.
####Example Response
Response:
[
{
@ -352,30 +265,30 @@ For example,
}
]
####Potential Errors
* None
**Errors**
None
###findOne
##findOne
Find first instance of the model matched by filter from the data source.
Find first instance of the model matched by filter from the data source
####Definition
**Definition**
GET /locations/findOne
####Arguments
**Arguments**
* **filter** The filter that defines where, order, fields, skip, and limit. It's
same as find's filter argument. Please see [find](#find) for more details.
**Example**
Request:
####Example Request
curl http://localhost:3000/locations/findOne?filter%5Bwhere%5D%5Bcity%5D=Scottsdale
####Example Response
Response:
{
"id": "87",
@ -389,86 +302,89 @@ same as find's filter argument. Please see [find](#find) for more details.
}
}
####Potential Errors
* None
**Errors**
None
###deleteById
##deleteById
Delete a model instance by id from the data source
####Definition
**Definition**
DELETE /locations/{id}
####Arguments
**Arguments**
* **id** The model id
**Example**
Request:
####Example Request
curl -X DELETE http://localhost:3000/locations/88
####Example Response
Response:
Example TBD.
####Potential Errors
* None
**Errors**
None
###count
##count
Count instances of the model matched by where from the data source
####Definition
**Definition**
GET /locations/count
####Arguments
**Arguments**
* **where** The criteria to match model instances
**Example**
####Example Request
Request - count without "where" filter
#####Count without where
curl http://localhost:3000/locations/count
#####Count with a where filter
Request - count with a "where" filter
curl http://localhost:3000/locations/count?where%5bcity%5d=Burlingame
####Example Response
Response:
{
count: 6
}
####Potential Errors
* None
**Errors**
None
###nearby
##nearby
Find nearby locations around the geo point.
Find nearby locations around the geo point
####Definition
**Definition**
GET /locations/nearby
####Arguments
**Arguments**
* **here** geo location object with `lat` and `lng` properties
* **page** number of pages (page size=10)
* **max** max distance in miles
**Example**
Request:
####Example Request
curl http://localhost:3000/locations/nearby?here%5Blat%5D=37.587409&here%5Blng%5D=-122.338225
####Example Response
Response:
[
{
@ -495,30 +411,32 @@ Find nearby locations around the geo point
}
]
####Potential Errors
* None
**Errors**
None
###updateAttributes
##updateAttributes
Update attributes for a model instance and persist it into the data source
####Definition
**Definition**
PUT /locations/{id}
####Arguments
**Arguments**
* **data** An object containing property name/value pairs
* **id** The model id
**Example**
Request:
####Example Request
curl -X PUT -H "Content-Type:application/json" -d '{"name": "L2"}' \
http://localhost:3000/locations/88
####Example Response
Response:
{
"id": "88",
"street": "390 Lang Road",
@ -532,29 +450,30 @@ Update attributes for a model instance and persist it into the data source
"state": "CA"
}
####Potential Errors
**Errors**
* 404 No instance found for the given id
###getAssociatedModel
##getAssociatedModel
Follow the relations from one model (`location`) to another one (`inventory`) to
get instances of the associated model.
####Definition
**Definition**
GET /locations/{id}/inventory
####Arguments
**Arguments**
* **id** The id for the location model
**Example**
Request:
####Example Request
curl http://localhost:3000/locations/88/inventory
####Example Response
Response:
[
{
@ -571,9 +490,6 @@ get instances of the associated model.
}
]
####Potential Errors
* None
**Errors**
None

View File

@ -1,13 +0,0 @@
## Loopback Types
LoopBack APIs accept type descriptions [remote methods](#remote-methods), [loopback.createModel()](#model)). The following is a list of supported types.
- `null` - JSON null
- `Boolean` - JSON boolean
- `Number` - JSON number
- `String` - JSON string
- `Object` - JSON object
- `Array` - JSON array
- `Date` - a JavaScript date object
- `Buffer` - a node.js Buffer object
- [GeoPoint](#geopoint) - A Loopback GeoPoint object.

View File

@ -43,12 +43,6 @@ app.disuse = function (route) {
}
}
/**
* App models.
*/
app._models = [];
/**
* Expose a model.
*
@ -60,13 +54,14 @@ app.model = function (Model, config) {
assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor');
assert(Model.pluralModelName, 'Model must have a "pluralModelName" property');
this.remotes().exports[Model.pluralModelName] = Model;
this._models.push(Model);
this.models().push(Model);
Model.shared = true;
Model.app = this;
Model.emit('attached', this);
return;
}
var modelName = Model;
config = config || {};
assert(typeof modelName === 'string', 'app.model(name, config) => "name" name must be a string');
Model =
@ -74,25 +69,19 @@ app.model = function (Model, config) {
this.models[classify(modelName)] =
this.models[camelize(modelName)] = modelFromConfig(modelName, config, this);
this.model(Model);
if(config.public !== false) {
this.model(Model);
}
return Model;
}
/**
* Get a Model by name.
*/
app.getModel = function (modelName) {
this.models
};
/**
* Get all exposed models.
*/
app.models = function () {
return this._models;
return this._models || (this._models = []);
}
/**
@ -141,7 +130,6 @@ app.docs = function (options) {
swagger(remotes, options);
}
/*!
* Get a handler of the specified type from the handler cache.
*/
@ -163,6 +151,52 @@ app.handler = function (type) {
app.dataSources = app.datasources = {};
/**
* Enable app wide authentication.
*/
app.enableAuth = function() {
var remotes = this.remotes();
remotes.before('**', function(ctx, next, method) {
var req = ctx.req;
var Model = method.ctor;
var modelInstance = ctx.instance;
var modelId = modelInstance && modelInstance.id;
// TODO(ritch) - this fallback could be less express dependent
if(modelInstance && !modelId) {
modelId = req.param('id');
}
if(req.accessToken) {
Model.checkAccess(
req.accessToken,
modelId,
method.name,
function(err, allowed) {
if(err) {
next(err);
} else if(allowed) {
next();
} else {
var e = new Error('Access Denied');
e.statusCode = 401;
next(e);
}
}
);
} else if(method.fn && method.fn.requireToken === false) {
next();
} else {
var e = new Error('Access Denied');
e.statusCode = 401;
next(e);
}
});
}
/**
* Initialize the app using JSON and JavaScript files.
*
@ -218,6 +252,17 @@ app.boot = function(options) {
app.model(key, obj);
});
// try to attach models to dataSources by type
try {
require('./loopback').autoAttach();
} catch(e) {
if(e.name === 'AssertionError') {
console.warn(e);
} else {
throw e;
}
}
// require directories
var requiredModels = requireDir(path.join(appRootDir, 'models'));
}

View File

@ -96,6 +96,11 @@ loopback.createDataSource = function (name, options) {
ModelCtor.attachTo(ds);
return ModelCtor;
};
if(ds.settings && ds.settings.defaultForType) {
loopback.setDefaultDataSourceForType(ds.settings.defaultForType, ds);
}
return ds;
};
@ -108,7 +113,14 @@ loopback.createDataSource = function (name, options) {
*/
loopback.createModel = function (name, properties, options) {
return loopback.Model.extend(name, properties, options);
var model = loopback.Model.extend(name, properties, options);
// try to attach
try {
loopback.autoAttachModel(model);
} catch(e) {}
return model;
}
/**
@ -173,6 +185,59 @@ loopback.getModel = function(modelName) {
return loopback.Model.modelBuilder.models[modelName];
};
/**
* Set the default `dataSource` for a given `type`.
*/
loopback.setDefaultDataSourceForType = function(type, dataSource) {
var defaultDataSources = this.defaultDataSources || (this.defaultDataSources = {});
if(!(dataSource instanceof DataSource)) {
dataSource = this.createDataSource(dataSource);
}
defaultDataSources[type] = dataSource;
return dataSource;
}
/**
* Get the default `dataSource` for a given `type`.
*/
loopback.getDefaultDataSourceForType = function(type) {
return this.defaultDataSources && this.defaultDataSources[type];
}
/**
* Attach any model that does not have a dataSource to
* the default dataSource for the type the Model requests
*/
loopback.autoAttach = function() {
var models = this.Model.modelBuilder.models;
assert.equal(typeof models, 'object', 'Cannot autoAttach without a models object');
Object.keys(models).forEach(function(modelName) {
var ModelCtor = models[modelName];
if(ModelCtor) {
loopback.autoAttachModel(ModelCtor);
}
});
}
loopback.autoAttachModel = function(ModelCtor) {
if(ModelCtor.autoAttach) {
var ds = loopback.getDefaultDataSourceForType(ModelCtor.autoAttach);
assert(ds instanceof DataSource, 'cannot autoAttach model "'
+ ModelCtor.modelName
+ '". No dataSource found of type ' + ModelCtor.autoAttach);
ModelCtor.attachTo(ds);
}
}
/*
* Built in models / services
*/
@ -182,3 +247,25 @@ loopback.Email = require('./models/email');
loopback.User = require('./models/user');
loopback.Application = require('./models/application');
loopback.AccessToken = require('./models/access-token');
loopback.Role = require('./models/role').Role;
loopback.RoleMapping = require('./models/role').RoleMapping;
loopback.ACL = require('./models/acl').ACL;
loopback.Scope = require('./models/acl').Scope;
/**
* Automatically attach these models to dataSources
*/
var dataSourceTypes = {
DB: 'db',
MAIL: 'mail'
};
loopback.Email.autoAttach = dataSourceTypes.MAIL;
loopback.User.autoAttach = dataSourceTypes.DB;
loopback.AccessToken.autoAttach = dataSourceTypes.DB;
loopback.Role.autoAttach = dataSourceTypes.DB;
loopback.RoleMapping.autoAttach = dataSourceTypes.DB;
loopback.ACL.autoAttach = dataSourceTypes.DB;
loopback.Scope.autoAttach = dataSourceTypes.DB;
loopback.Application.autoAttach = dataSourceTypes.DB;

17
lib/middleware/status.js Normal file
View File

@ -0,0 +1,17 @@
/**
* Export the middleware.
*/
module.exports = status;
function status() {
var started = new Date();
return function(req, res) {
res.send({
started: started,
uptime: (Date.now() - Number(started)) / 1000
});
}
}

View File

@ -4,6 +4,7 @@
var loopback = require('../loopback');
var RemoteObjects = require('strong-remoting');
var assert = require('assert');
/**
* Export the middleware.
@ -13,7 +14,7 @@ module.exports = token;
function token(options) {
options = options || {};
var TokenModel = options.model;
var TokenModel = options.model || loopback.AccessToken;
assert(TokenModel, 'loopback.token() middleware requires a AccessToken model');
return function (req, res, next) {

View File

@ -0,0 +1,18 @@
/**
* Export the middleware.
*/
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
*/
function urlNotFound() {
return function raiseUrlNotFoundError(req, res, next) {
var error = new Error('Cannot ' + req.method + ' ' + req.url);
error.status = 404;
next(error);
}
}

View File

@ -8,7 +8,8 @@ var Model = require('../loopback').Model
, crypto = require('crypto')
, uid = require('uid2')
, DEFAULT_TTL = 1209600 // 2 weeks in seconds
, DEFAULT_TOKEN_LEN = 64;
, DEFAULT_TOKEN_LEN = 64
, ACL = require('./acl').ACL;
/**
* Default AccessToken properties.

View File

@ -32,6 +32,11 @@
*/
var loopback = require('../loopback');
var async = require('async');
var assert = require('assert');
var role = require('./role');
var Role = role.Role;
/**
* Schema for Scope which represents the permissions that are granted to client applications by the resource owner
@ -91,6 +96,7 @@ var ACL = loopback.createModel('ACL', ACLSchema);
ACL.ALL = '*';
ACL.DEFAULT = 'DEFAULT';
ACL.ALLOW = 'ALLOW';
ACL.ALARM = 'ALARM';
ACL.AUDIT = 'AUDIT';
@ -106,6 +112,7 @@ ACL.ROLE = 'ROLE';
ACL.SCOPE = 'SCOPE';
var permissionOrder = {
DEFAULT: 0,
ALLOW: 1,
ALARM: 2,
AUDIT: 3,
@ -144,62 +151,80 @@ function resolvePermission(acls, defaultPermission) {
return resolvedPermission;
}
/**
* Check if the given principal is allowed to access the model/property
/*!
* Check the LDL ACLs
* @param principalType
* @param principalId
* @param model
* @param property
* @param accessType
* @param callback
* @param {String} model The model name
* @param {String} property The property/method/relation name
* @param {String} accessType The access type
*
* @returns {{principalType: *, principalId: *, model: *, property: string, accessType: *, permission: string}}
*/
ACL.checkPermission = function (principalType, principalId, model, property, accessType, callback) {
property = property || ACL.ALL;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
accessType = accessType || ACL.ALL;
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};
function getStaticPermission(principalType, principalId, model, property, accessType) {
var modelClass = loopback.getModel(model);
var staticACLs = [];
var modelClass = loopback.getModel(model); {
if(modelClass && modelClass.settings.acls) {
modelClass.settings.acls.forEach(function(acl) {
staticACLs.push({
model: model,
property: acl.property || ACL.ALL,
principalType: acl.principalType,
principalId: acl.principalId, // TODO: Should it be a name?
accessType: acl.accessType,
permission: acl.permission
});
if (modelClass && modelClass.settings.acls) {
modelClass.settings.acls.forEach(function (acl) {
staticACLs.push({
model: model,
property: acl.property || ACL.ALL,
principalType: acl.principalType,
principalId: acl.principalId, // TODO: Should it be a name?
accessType: acl.accessType,
permission: acl.permission
});
}
var prop = modelClass &&
(modelClass.definition.properties[property] // regular property
});
}
var prop = modelClass &&
(modelClass.definition.properties[property] // regular property
|| (modelClass._scopeMeta && modelClass._scopeMeta[property]) // relation/scope
|| modelClass[property] // static method
|| modelClass.prototype[property]); // prototype method
if(prop && prop.acls) {
prop.acls.forEach(function(acl) {
staticACLs.push({
model: model,
property: property,
principalType: acl.principalType,
principalId: acl.principalId,
accessType: acl.accessType,
permission: acl.permission
});
if (prop && prop.acls) {
prop.acls.forEach(function (acl) {
staticACLs.push({
model: modelClass.modelName,
property: property,
principalType: acl.principalType,
principalId: acl.principalId,
accessType: acl.accessType,
permission: acl.permission
});
}
});
}
var defaultPermission = {principalType: principalType, principalId: principalId,
model: model, property: ACL.ALL, accessType: accessType, permission: ACL.ALLOW};
defaultPermission = resolvePermission(staticACLs, defaultPermission);
return defaultPermission;
}
/**
* 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|Error} err The error object
* @param {Object} the access permission
*/
ACL.checkPermission = function (principalType, principalId, model, property, accessType, callback) {
property = property || ACL.ALL;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
accessType = accessType || ACL.ALL;
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};
var defaultPermission = getStaticPermission(principalType, principalId, model, property, accessType);
if(defaultPermission.permission === ACL.DENY) {
// Fail fast
callback && callback(null, defaultPermission);
process.nextTick(function() {
callback && callback(null, defaultPermission);
});
return;
}
@ -211,17 +236,25 @@ ACL.checkPermission = function (principalType, principalId, model, property, acc
return;
}
var resolvedPermission = resolvePermission(acls, defaultPermission);
if(resolvedPermission.permission === ACL.DEFAULT) {
var modelClass = loopback.getModel(model);
resolvedPermission.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
}
callback && callback(null, resolvedPermission);
});
};
/**
* Check if the given scope is allowed to access the model/property
* @param scope
* @param model
* @param property
* @param accessType
* @param callback
* @param {String} scope The scope name
* @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|Error} err The error object
* @param {Object} the access permission
*/
Scope.checkPermission = function (scope, model, property, accessType, callback) {
Scope.findOne({where: {name: scope}}, function (err, scope) {
@ -233,6 +266,117 @@ Scope.checkPermission = function (scope, model, property, accessType, callback)
});
};
/**
* Check if the request has the permission to access
* @param {Object} context
* @param {Function} callback
*/
ACL.checkAccess = function (context, callback) {
context = context || {};
var principals = context.principals || [];
// add ROLE.EVERYONE
principals.unshift({principalType: ACL.ROLE, principalId: Role.EVERYONE});
var model = context.model;
model = ('string' === typeof model) ? loopback.getModel(model) : model;
var id = context.id;
var property = context.property;
var accessType = context.accessType;
property = property || ACL.ALL;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
accessType = accessType || ACL.ALL;
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};
var defaultPermission = {principalType: null, principalId: null,
model: model.modelName, property: ACL.ALL, accessType: accessType, permission: ACL.ALLOW};
// Check the LDL ACLs
principals.forEach(function(p) {
var perm = getStaticPermission(p.principalType, p.principalId, model.modelName, property, accessType);
defaultPermission = resolvePermission([perm], defaultPermission);
});
if(defaultPermission.permission === ACL.DENY) {
// Fail fast
process.nextTick(function() {
callback && callback(null, defaultPermission);
});
return;
}
ACL.find({where: {model: model.modelName, property: propertyQuery, accessType: accessTypeQuery}}, function (err, acls) {
if (err) {
callback && callback(err);
return;
}
var effectiveACLs = [];
var inRoleTasks = [];
acls.forEach(function (acl) {
principals.forEach(function (principal) {
if (principal.principalType === acl.pricipalType && principal.principalId === acl.principalId) {
effectiveACLs.push(acl);
} else if (acl.principalType === ACL.ROLE) {
inRoleTasks.push(function (done) {
Role.isInRole(acl.principalId,
{principalType: principal.principalType, principalId: acl.principalId, model: model, id: id, property: property},
function (err, inRole) {
if(!err) {
effectiveACLs.push(acl);
}
done(err, acl);
});
});
}
});
});
async.parallel(inRoleTasks, function(err, results) {
defaultPermission = resolvePermission(effectiveACLs, defaultPermission);
callback && callback(null, defaultPermission);
});
});
};
/**
* 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
* @param callback The callback function
*
* @callback callback
* @param {String|Error} err The error object
* @param {Boolean} allowed is the request allowed
*/
ACL.checkAccessForToken = function(token, model, modelId, method, callback) {
assert(token, 'Access token is required');
var principals = [];
if(token.userId) {
principals.push({principalType: ACL.USER, principalId: token.userId});
}
if(token.appId) {
principals.push({principalType: ACL.APPLICATION, principalId: token.appId});
}
var context = {
principals: principals,
model: model,
property: method,
accessType: ACL.EXECUTE,
id: modelId
};
ACL.checkAccess(context, function(err, access) {
if(err) {
callback && callback(err);
return;
}
callback && callback(null, access.permission !== ACL.DENY);
});
};
module.exports = {
ACL: ACL,

View File

@ -1,7 +1,7 @@
/**
* Module Dependencies.
*/
var loopback = require('../loopback');
var ModelBuilder = require('loopback-datasource-juggler').ModelBuilder;
var modeler = new ModelBuilder();
@ -79,7 +79,7 @@ Model.setup = function () {
self.beforeRemote.apply(self, args);
});
}
}
};
// after remote hook
ModelCtor.afterRemote = function (name, fn) {
@ -95,7 +95,7 @@ Model.setup = function () {
self.afterRemote.apply(self, args);
});
}
}
};
// Map the prototype method to /:id with data in the body
ModelCtor.sharedCtor.accepts = [
@ -110,7 +110,34 @@ Model.setup = function () {
ModelCtor.sharedCtor.returns = {root: true};
return ModelCtor;
};
/*!
* Get the reference to ACL in a lazy fashion to avoid race condition in require
*/
var ACL = null;
function getACL() {
return ACL || (ACL = require('./acl').ACL);
}
/**
* 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 callback
* @param {String|Error} err The error object
* @param {Boolean} allowed is the request allowed
*/
Model.checkAccess = function(token, modelId, method, callback) {
var ACL = getACL();
var methodName = 'string' === typeof method? method: method && method.name;
ACL.checkAccessForToken(token, this.modelName, modelId, methodName, callback);
};
// setup the initial model
Model.setup();

View File

@ -135,6 +135,7 @@ Role.once('dataSourceAttached', function () {
Role.OWNER = '$owner'; // owner of the object
Role.RELATED = "$related"; // any User with a relationship to the object
Role.AUTHENTICATED = "$authenticated"; // authenticated user
Role.UNAUTHENTICATED = "$unauthenticated"; // authenticated user
Role.EVERYONE = "$everyone"; // everyone
/**
@ -142,21 +143,104 @@ Role.EVERYONE = "$everyone"; // everyone
* @param role
* @param resolver The resolver function decides if a principal is in the role dynamically
*
* isInRole(role, context, callback)
* function(role, context, callback)
*/
Role.registerResolver = function(role, resolver) {
if(!Role.resolvers) {
Role.resolvers = {};
}
Role.resolvers[role] = resolver;
};
Role.registerResolver(Role.OWNER, function(role, context, callback) {
if(!context || !context.model || !context.id) {
process.nextTick(function() {
callback && callback(null, false);
});
return;
}
var modelClass = context.model;
var id = context.id;
var userId = context.principalId;
isOwner(modelClass, id, userId, callback);
});
function isOwner(modelClass, id, userId, callback) {
modelClass.findById(id, function(err, inst) {
if(err) {
callback && callback(err);
return;
}
if(inst.userId || inst.owner) {
callback && callback(null, (inst.userId || inst.owner) === userId);
return;
} else {
for(var r in modelClass.relations) {
var rel = modelClass.relations[r];
if(rel.type === 'belongsTo' && rel.model && rel.model.prototype instanceof loopback.User) {
callback && callback(null, rel.foreignKey === userId);
return;
}
}
callback && callback(null, false);
}
});
}
Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) {
if(!context) {
process.nextTick(function() {
callback && callback(null, false);
});
return;
}
var userId = context.principalId;
isAuthenticated(userId, callback);
});
function isAuthenticated(userId, callback) {
process.nextTick(function() {
callback && callback(null, !!userId);
});
}
Role.registerResolver(Role.UNAUTHENTICATED, function(role, context, callback) {
process.nextTick(function() {
callback && callback(null, !context || !context.principalId);
});
});
Role.registerResolver(Role.EVERYONE, function (role, context, callback) {
process.nextTick(function () {
callback && callback(null, true); // Always true
});
});
/**
* Check if a given principal is in the role
*
* @param role
* @param principalType
* @param principalId
* @param callback
* @param {String} role The role name
* @param {Object} context The context object
* @param {Function} callback
*/
Role.isInRole = function (role, principalType, principalId, callback) {
Role.isInRole = function (role, context, callback) {
var resolver = Role.resolvers[role];
if(resolver) {
resolver(role, context, callback);
return;
}
var principalType = context.principalType;
var principalId = context.principalId;
// Check if it's the same role
if(principalType === RoleMapping.ROLE && principalId === role) {
process.nextTick(function() {
callback && callback(null, true);
});
return;
}
Role.findOne({where: {name: role}}, function (err, result) {
if (err) {
callback && callback(err);

View File

@ -9,7 +9,7 @@
"Platform",
"mBaaS"
],
"version": "1.3.0",
"version": "1.3.1",
"scripts": {
"test": "mocha -R spec"
},
@ -26,7 +26,8 @@
"bcryptjs": "~0.7.10",
"underscore.string": "~2.3.3",
"underscore": "~1.5.2",
"uid2": "0.0.3"
"uid2": "0.0.3",
"async": "~0.2.9"
},
"devDependencies": {
"mocha": "~1.14.0",

View File

@ -1,8 +1,6 @@
var loopback = require('../');
var Token = loopback.AccessToken.extend('MyToken');
// attach Token to testing memory ds
Token.attachTo(loopback.memory());
var ACL = loopback.ACL;
describe('loopback.token(options)', function() {
beforeEach(createTestingToken);
@ -57,6 +55,27 @@ describe('AccessToken', function () {
});
});
describe('app.enableAuth()', function() {
this.timeout(0);
beforeEach(createTestingToken);
it('should prevent all remote method calls without an accessToken', function (done) {
createTestAppAndRequest(this.token, done)
.get('/tests')
.expect(401)
.end(done);
});
it('should prevent remote method calls if the accessToken doesnt have access', function (done) {
createTestAppAndRequest(this.token, done)
.del('/tests/123')
.expect(401)
.set('authorization', this.token.id)
.end(done);
});
});
function createTestingToken(done) {
var test = this;
Token.create({}, function (err, token) {
@ -89,6 +108,23 @@ function createTestApp(testToken, done) {
}
res.send('ok');
});
app.use(loopback.rest());
app.enableAuth();
var TestModel = loopback.Model.extend('test', {}, {
acls: [
{
principalType: "ROLE",
principalId: "$everyone",
accessType: ACL.ALL,
permission: ACL.DENY,
property: 'removeById'
}
]
});
TestModel.attachTo(loopback.memory());
app.model(TestModel);
return app;
}

View File

@ -3,7 +3,9 @@ var loopback = require('../index');
var acl = require('../lib/models/acl');
var Scope = acl.Scope;
var ACL = acl.ACL;
var ScopeACL = acl.ScopeACL;
var role = require('../lib/models/role');
var Role = role.Role;
var RoleMapping = role.RoleMapping;
var User = loopback.User;
function checkResult(err, result) {
@ -14,18 +16,10 @@ function checkResult(err, result) {
describe('security scopes', function () {
it("should allow access to models for the given scope by wildcard", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
Scope.attachTo(ds);
ACL.attachTo(ds);
// console.log(Scope.relations);
Scope.create({name: 'userScope', description: 'access user information'}, function (err, scope) {
// console.log(scope);
ACL.create({principalType: ACL.SCOPE, principalId: scope.id, model: 'User', property: ACL.ALL,
accessType: ACL.ALL, permission: ACL.ALLOW},
function (err, resource) {
// console.log(resource);
Scope.checkPermission('userScope', 'User', ACL.ALL, ACL.ALL, checkResult);
Scope.checkPermission('userScope', 'User', 'name', ACL.ALL, checkResult);
Scope.checkPermission('userScope', 'User', 'name', ACL.READ, checkResult);
@ -36,13 +30,7 @@ describe('security scopes', function () {
it("should allow access to models for the given scope", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
Scope.attachTo(ds);
ACL.attachTo(ds);
// console.log(Scope.relations);
Scope.create({name: 'userScope', description: 'access user information'}, function (err, scope) {
// console.log(scope);
ACL.create({principalType: ACL.SCOPE, principalId: scope.id,
model: 'User', property: 'name', accessType: ACL.READ, permission: ACL.ALLOW},
function (err, resource) {
@ -74,7 +62,6 @@ describe('security ACLs', function () {
it("should allow access to models for the given principal by wildcard", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
ACL.attachTo(ds);
ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL,
accessType: ACL.ALL, permission: ACL.ALLOW}, function (err, acl) {
@ -96,6 +83,39 @@ describe('security ACLs', function () {
});
it("should honor defaultPermission from the model", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
ACL.attachTo(ds);
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}
]
});
Customer.settings.defaultPermission = ACL.DENY;
ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.WRITE, function (err, perm) {
assert(perm.permission === ACL.DENY);
});
ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.READ, function (err, perm) {
assert(perm.permission === ACL.ALLOW);
});
ACL.checkPermission(ACL.USER, 'u002', 'Customer', 'name', ACL.WRITE, function (err, perm) {
assert(perm.permission === ACL.DENY);
});
});
it("should honor static ACLs from the model", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
var Customer = ds.createModel('Customer', {
@ -117,7 +137,6 @@ describe('security ACLs', function () {
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
];
*/
ACL.attachTo(ds);
ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.WRITE, function (err, perm) {
assert(perm.permission === ACL.DENY);
@ -133,6 +152,81 @@ describe('security ACLs', function () {
});
it("should check access against LDL, ACL, and Role", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
ACL.attachTo(ds);
Role.attachTo(ds);
RoleMapping.attachTo(ds);
User.attachTo(ds);
// var log = console.log;
var log = function() {};
// Create
User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) {
log('User: ', user.toObject());
// Define a model with static ACLs
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}
]
});
ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'Customer', property: ACL.ALL,
accessType: ACL.ALL, permission: ACL.ALLOW}, function (err, acl) {
log('ACL 1: ', acl.toObject());
Role.create({name: 'MyRole'}, function (err, myRole) {
log('Role: ', myRole.toObject());
myRole.principals.create({principalType: RoleMapping.USER, principalId: user.id}, function (err, p) {
log('Principal added to role: ', p.toObject());
ACL.create({principalType: ACL.ROLE, principalId: myRole.id, model: 'Customer', property: ACL.ALL,
accessType: ACL.READ, permission: ACL.DENY}, function (err, acl) {
log('ACL 2: ', acl.toObject());
ACL.checkAccess({
principals: [
{principalType: ACL.USER, principalId: 'u001'}
],
model: 'Customer',
property: 'name',
accessType: ACL.READ
}, function(err, access) {
assert(!err && access.permission === ACL.ALLOW);
});
ACL.checkAccess({
principals: [
{principalType: ACL.USER, principalId: 'u001'}
],
model: 'Customer',
accessType: ACL.READ
}, function(err, access) {
assert(!err && access.permission === ACL.DENY);
});
});
});
});
});
});
});
});

View File

@ -111,4 +111,31 @@ describe('app', function() {
assert.isFunc(app.models.Foo, 'create');
});
});
describe('app.get("/", loopback.status())', function () {
it('should return the status of the application', function (done) {
var app = loopback();
app.get('/', loopback.status());
request(app)
.get('/')
.expect(200)
.end(function(err, res) {
if(err) return done(err);
assert.equal(typeof res.body, 'object');
assert(res.body.started);
assert(res.body.uptime);
var elapsed = Date.now() - Number(new Date(res.body.started));
// elapsed should be a positive number...
assert(elapsed > 0);
// less than 100 milliseconds
assert(elapsed < 100);
done();
});
});
});
});

View File

@ -1,17 +1,8 @@
var loopback = require('../');
var MailConnector = loopback.Mail;
var MyEmail = loopback.Email.extend('my-email');
var assert = require('assert');
describe('Email and SMTP', function () {
var mail = loopback.createDataSource({
connector: MailConnector,
transports: [
{type: 'STUB'}
]
});
MyEmail.attachTo(mail);
it('should have a send method', function () {
assert(typeof MyEmail.send === 'function');

View File

@ -1,16 +1,11 @@
var loopback = require(('../'));
var assert = require('assert');
var dataSource = loopback.createDataSource('db', {connector: loopback.Memory});
var Application = loopback.Application;
Application.attachTo(dataSource);
describe('Application', function () {
var registeredApp = null;
it('Create a new application', function (done) {
Application.create({owner: 'rfeng', name: 'MyApp1', description: 'My first mobile application'}, function (err, result) {
var app = result;
assert.equal(app.owner, 'rfeng');
@ -27,8 +22,7 @@ describe('Application', function () {
});
});
it('Register a new application', function (done) {
beforeEach(function (done) {
Application.register('rfeng', 'MyApp2', {description: 'My second mobile application'}, function (err, result) {
var app = result;
assert.equal(app.owner, 'rfeng');
@ -47,7 +41,6 @@ describe('Application', function () {
});
it('Reset keys', function (done) {
Application.resetKeys(registeredApp.id, function (err, result) {
var app = result;
assert.equal(app.owner, 'rfeng');
@ -73,7 +66,6 @@ describe('Application', function () {
});
it('Authenticate with application id & clientKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.clientKey, function (err, result) {
assert.equal(result, 'clientKey');
done(err, result);
@ -81,7 +73,6 @@ describe('Application', function () {
});
it('Authenticate with application id & javaScriptKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.javaScriptKey, function (err, result) {
assert.equal(result, 'javaScriptKey');
done(err, result);
@ -116,6 +107,5 @@ describe('Application', function () {
done(err, result);
});
});
});

View File

@ -296,6 +296,13 @@ describe('Model', function() {
done();
});
});
it('Converts null result of findById to 404 Not Found', function(done) {
request(app)
.get('/users/not-found')
.expect(404)
.end(done);
});
});
describe('Model.beforeRemote(name, fn)', function(){

View File

@ -4,6 +4,7 @@ var role = require('../lib/models/role');
var Role = role.Role;
var RoleMapping = role.RoleMapping;
var User = loopback.User;
var ACL = require('../lib/models/acl');
function checkResult(err, result) {
// console.log(err, result);
@ -13,10 +14,6 @@ function checkResult(err, result) {
describe('role model', function () {
it("should define role/role relations", function () {
var ds = loopback.createDataSource({connector: 'memory'});
Role.attachTo(ds);
RoleMapping.attachTo(ds);
Role.create({name: 'user'}, function (err, userRole) {
Role.create({name: 'admin'}, function (err, adminRole) {
userRole.principals.create({principalType: RoleMapping.ROLE, principalId: adminRole.id}, function (err, mapping) {
@ -41,28 +38,23 @@ describe('role model', function () {
});
it("should define role/user relations", function () {
var ds = loopback.createDataSource({connector: 'memory'});
User.attachTo(ds);
Role.attachTo(ds);
RoleMapping.attachTo(ds);
User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) {
// console.log('User: ', user.id);
Role.create({name: 'userRole'}, function (err, role) {
role.principals.create({principalType: RoleMapping.USER, principalId: user.id}, function (err, p) {
Role.find(function(err, roles) {
Role.find(function (err, roles) {
assert(!err);
assert.equal(roles.length, 1);
assert.equal(roles[0].name, 'userRole');
});
role.principals(function(err, principals) {
role.principals(function (err, principals) {
assert(!err);
// console.log(principals);
assert.equal(principals.length, 1);
assert.equal(principals[0].principalType, RoleMapping.USER);
assert.equal(principals[0].principalId, user.id);
});
role.users(function(err, users) {
role.users(function (err, users) {
assert(!err);
assert.equal(users.length, 1);
assert.equal(users[0].principalType, RoleMapping.USER);
@ -75,37 +67,32 @@ describe('role model', function () {
});
it("should support getRoles() and isInRole()", function () {
var ds = loopback.createDataSource({connector: 'memory'});
User.attachTo(ds);
Role.attachTo(ds);
RoleMapping.attachTo(ds);
User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) {
// console.log('User: ', user.id);
Role.create({name: 'userRole'}, function (err, role) {
role.principals.create({principalType: RoleMapping.USER, principalId: user.id}, function (err, p) {
// Role.find(console.log);
// role.principals(console.log);
Role.isInRole('userRole', RoleMapping.USER, user.id, function(err, exists) {
Role.isInRole('userRole', {principalType: RoleMapping.USER, principalId: user.id}, function (err, exists) {
assert(!err && exists === true);
});
Role.isInRole('userRole', RoleMapping.APP, user.id, function(err, exists) {
Role.isInRole('userRole', {principalType: RoleMapping.APP, principalId: user.id}, function (err, exists) {
assert(!err && exists === false);
});
Role.isInRole('userRole', RoleMapping.USER, 100, function(err, exists) {
Role.isInRole('userRole', {principalType: RoleMapping.USER, principalId: 100}, function (err, exists) {
assert(!err && exists === false);
});
Role.getRoles(RoleMapping.USER, user.id, function(err, roles) {
Role.getRoles(RoleMapping.USER, user.id, function (err, roles) {
assert.equal(roles.length, 1);
assert.equal(roles[0], role.id);
});
Role.getRoles(RoleMapping.APP, user.id, function(err, roles) {
Role.getRoles(RoleMapping.APP, user.id, function (err, roles) {
assert.equal(roles.length, 0);
});
Role.getRoles(RoleMapping.USER, 100, function(err, roles) {
Role.getRoles(RoleMapping.USER, 100, function (err, roles) {
assert.equal(roles.length, 0);
});
@ -115,6 +102,63 @@ describe('role model', function () {
});
it("should support owner role resolver", function () {
var ds = loopback.createDataSource({connector: 'memory'});
User.attachTo(ds);
Role.attachTo(ds);
RoleMapping.attachTo(ds);
var Album = ds.createModel('Album', {
name: String,
userId: Number
}, {
relations: {
user: {
type: 'belongsTo',
model: 'User',
foreignKey: 'userId'
}
}
});
User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) {
Role.isInRole(Role.AUTHENTICATED, {principalType: ACL.USER, principalId: user.id}, function (err, yes) {
assert(!err && yes);
});
Role.isInRole(Role.AUTHENTICATED, {principalType: ACL.USER, principalId: null}, function (err, yes) {
assert(!err && !yes);
});
Role.isInRole(Role.UNAUTHENTICATED, {principalType: ACL.USER, principalId: user.id}, function (err, yes) {
assert(!err && !yes);
});
Role.isInRole(Role.UNAUTHENTICATED, {principalType: ACL.USER, principalId: null}, function (err, yes) {
assert(!err && yes);
});
Role.isInRole(Role.EVERYONE, {principalType: ACL.USER, principalId: user.id}, function (err, yes) {
assert(!err && yes);
});
Role.isInRole(Role.EVERYONE, {principalType: ACL.USER, principalId: null}, function (err, yes) {
assert(!err && yes);
});
// console.log('User: ', user.id);
Album.create({name: 'Album 1', userId: user.id}, function (err, album1) {
Role.isInRole(Role.OWNER, {principalType: ACL.USER, principalId: user.id, model: Album, id: album1.id}, function (err, yes) {
assert(!err && yes);
});
Album.create({name: 'Album 2'}, function (err, album2) {
Role.isInRole(Role.OWNER, {principalType: ACL.USER, principalId: user.id, model: Album, id: album2.id}, function (err, yes) {
assert(!err && !yes);
});
});
});
});
});
});

View File

@ -12,6 +12,21 @@ request = require('supertest');
beforeEach(function () {
app = loopback();
// setup default data sources
loopback.setDefaultDataSourceForType('db', {
connector: loopback.Memory
});
loopback.setDefaultDataSourceForType('mail', {
connector: loopback.Mail,
transports: [
{type: 'STUB'}
]
});
// auto attach data sources to models
loopback.autoAttach();
});
assertValidDataSource = function (dataSource) {

View File

@ -8,20 +8,13 @@ var userMemory = loopback.createDataSource({
});
describe('User', function(){
var mailDataSource = loopback.createDataSource({
connector: MailConnector,
transports: [{type: 'STUB'}]
});
User.attachTo(userMemory);
AccessToken.attachTo(userMemory);
// TODO(ritch) - this should be a default relationship
User.hasMany(AccessToken, {as: 'accessTokens', foreignKey: 'userId'});
User.email.attachTo(mailDataSource);
// allow many User.afterRemote's to be called
User.setMaxListeners(0);
before(function () {
User.hasMany(AccessToken, {as: 'accessTokens', foreignKey: 'userId'});
});
beforeEach(function (done) {
app.use(loopback.rest());
app.model(User);
@ -45,13 +38,18 @@ describe('User', function(){
});
});
it('Email is required', function(done) {
it('Email is required', function (done) {
User.create({password: '123'}, function (err) {
assert.deepEqual(err, { name: 'ValidationError',
message: 'Validation error',
statusCode: 400,
codes: { email: [ 'presence', 'format.blank', 'uniqueness' ] },
context: 'user' });
assert.deepEqual(err, {name: "ValidationError",
message: "The Model instance is not valid. See `details` "
+ "property of the error object for more info.",
statusCode: 422,
details: {
context: "user",
codes: {email: ["presence", "format.blank", "uniqueness"]},
messages: {email: ["can't be blank", "is blank",
"Email already exists"]}}}
);
done();
});
@ -263,8 +261,7 @@ describe('User', function(){
assert(result.token);
var lines = result.email.message.split('\n');
assert(lines[3].indexOf('To: bar@bat.com') === 0);
assert(~result.email.message.indexOf('To: bar@bat.com'));
done();
});
});