diff --git a/docs.json b/docs.json index 8c0894b5..f05d25c8 100644 --- a/docs.json +++ b/docs.json @@ -3,18 +3,21 @@ "content": [ "lib/application.js", "lib/loopback.js", - "lib/middleware/token.js", +{ "title": "Base model", "depth": 2 }, + "lib/models/model.js", + "lib/models/data-model.js", +{ "title": "Middleware", "depth": 2 }, + "lib/middleware/rest.js", + "lib/middleware/status.js", + "lib/middleware/token.js", + "lib/middleware/urlNotFound.js", +{ "title": "Built-in models", "depth": 2 }, "lib/models/access-token.js", - "lib/models/access-context.js", "lib/models/acl.js", "lib/models/application.js", "lib/models/email.js", - "lib/models/model.js", - "lib/models/data-model.js", "lib/models/role.js", - "lib/models/user.js", - "docs/api-model.md", - "docs/api-model-remote.md" + "lib/models/user.js" ], "assets": "/docs/assets" } diff --git a/docs/api-datasource.md b/docs/api-datasource.md deleted file mode 100644 index 90b55a86..00000000 --- a/docs/api-datasource.md +++ /dev/null @@ -1,212 +0,0 @@ -## Data Source object - -LoopBack models can manipulate data via the DataSource object. Attaching a `DataSource` to a `Model` adds instance methods and static methods to the `Model`; some of the added methods may be remote methods. - -Define a data source for persisting models. - -```js -var oracle = loopback.createDataSource({ - connector: 'oracle', - host: '111.22.333.44', - database: 'MYDB', - username: 'username', - password: 'password' -}); -``` - -### Methods - -#### dataSource.createModel(name, properties, options) - -Define a model and attach it to a `DataSource`. - -```js -var Color = oracle.createModel('color', {name: String}); -``` - -You can define an ACL when you create a new data source with the `DataSource.create()` method. For example: - -```js -var Customer = ds.createModel('Customer', { - name: { - type: String, - acls: [ - {principalType: ACL.USER, principalId: 'u001', accessType: ACL.WRITE, permission: ACL.DENY}, - {principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW} - ] - } - }, { - acls: [ - {principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW} - ] - }); -``` - -#### dataSource.discoverModelDefinitions([username], fn) - -Discover a set of model definitions (table or collection names) based on tables or collections in a data source. - -```js -oracle.discoverModelDefinitions(function (err, models) { - models.forEach(function (def) { - // def.name ~ the model name - oracle.discoverSchema(null, def.name, function (err, schema) { - console.log(schema); - }); - }); -}); -``` - -#### dataSource.discoverSchema([owner], name, fn) - -Discover the schema of a specific table or collection. - -**Example schema from oracle connector:** - -```js - { - "name": "Product", - "options": { - "idInjection": false, - "oracle": { - "schema": "BLACKPOOL", - "table": "PRODUCT" - } - }, - "properties": { - "id": { - "type": "String", - "required": true, - "length": 20, - "id": 1, - "oracle": { - "columnName": "ID", - "dataType": "VARCHAR2", - "dataLength": 20, - "nullable": "N" - } - }, - "name": { - "type": "String", - "required": false, - "length": 64, - "oracle": { - "columnName": "NAME", - "dataType": "VARCHAR2", - "dataLength": 64, - "nullable": "Y" - } - }, - "audibleRange": { - "type": "Number", - "required": false, - "length": 22, - "oracle": { - "columnName": "AUDIBLE_RANGE", - "dataType": "NUMBER", - "dataLength": 22, - "nullable": "Y" - } - }, - "effectiveRange": { - "type": "Number", - "required": false, - "length": 22, - "oracle": { - "columnName": "EFFECTIVE_RANGE", - "dataType": "NUMBER", - "dataLength": 22, - "nullable": "Y" - } - }, - "rounds": { - "type": "Number", - "required": false, - "length": 22, - "oracle": { - "columnName": "ROUNDS", - "dataType": "NUMBER", - "dataLength": 22, - "nullable": "Y" - } - }, - "extras": { - "type": "String", - "required": false, - "length": 64, - "oracle": { - "columnName": "EXTRAS", - "dataType": "VARCHAR2", - "dataLength": 64, - "nullable": "Y" - } - }, - "fireModes": { - "type": "String", - "required": false, - "length": 64, - "oracle": { - "columnName": "FIRE_MODES", - "dataType": "VARCHAR2", - "dataLength": 64, - "nullable": "Y" - } - } - } - } -``` - -#### dataSource.enableRemote(operation) - -Enable remote access to a data source operation. Each [connector](#connector) has its own set of set remotely enabled and disabled operations. You can always list these by calling `dataSource.operations()`. - - -#### dataSource.disableRemote(operation) - -Disable remote access to a data source operation. Each [connector](#connector) has its own set of set enabled and disabled operations. You can always list these by calling `dataSource.operations()`. - -```js -// all rest data source operations are -// disabled by default -var oracle = loopback.createDataSource({ - connector: require('loopback-connector-oracle'), - host: '...', - ... -}); - -// or only disable it as a remote method -oracle.disableRemote('destroyAll'); -``` - -**Notes:** - - - Disabled operations will not be added to attached models. - - Disabling the remoting for a method only affects client access (it will still be available from server models). - - Data sources must enable / disable operations before attaching or creating models. - -#### dataSource.operations() - -List the enabled and disabled operations. - - console.log(oracle.operations()); - -Output: - -```js -{ - find: { - remoteEnabled: true, - accepts: [...], - returns: [...] - enabled: true - }, - save: { - remoteEnabled: true, - prototype: true, - accepts: [...], - returns: [...], - enabled: true - }, - ... -} -``` diff --git a/docs/api-geopoint.md b/docs/api-geopoint.md deleted file mode 100644 index 588a5246..00000000 --- a/docs/api-geopoint.md +++ /dev/null @@ -1,69 +0,0 @@ -## GeoPoint object - -The GeoPoint object represents a physical location. - -Use the `GeoPoint` class. - -```js -var GeoPoint = require('loopback').GeoPoint; -``` - -Embed a latitude / longitude point in a [Model](#model). - -```js -var CoffeeShop = loopback.createModel('coffee-shop', { - location: 'GeoPoint' -}); -``` - -You can query LoopBack models with a GeoPoint property and an attached data source using geo-spatial filters and sorting. For example, the following code finds the three nearest coffee shops. - -```js -CoffeeShop.attachTo(oracle); -var here = new GeoPoint({lat: 10.32424, lng: 5.84978}); -CoffeeShop.find({where: {location: {near: here}}, limit:3}, function(err, nearbyShops) { - console.info(nearbyShops); // [CoffeeShop, ...] -}); -``` - -### Distance Types - -**Note:** all distance methods use `miles` by default. - - - `miles` - - `radians` - - `kilometers` - - `meters` - - `miles` - - `feet` - - `degrees` - -### Methods - -#### geoPoint.distanceTo(geoPoint, options) - -Get the distance to another `GeoPoint`; for example: - -```js -var here = new GeoPoint({lat: 10, lng: 10}); -var there = new GeoPoint({lat: 5, lng: 5}); -console.log(here.distanceTo(there, {type: 'miles'})); // 438 -``` - -#### GeoPoint.distanceBetween(a, b, options) - -Get the distance between two points; for example: - -```js -GeoPoint.distanceBetween(here, there, {type: 'miles'}) // 438 -``` - -### Properties - -#### geoPoint.lat - -The latitude point in degrees. Range: -90 to 90. - -#### geoPoint.lng - -The longitude point in degrees. Range: -180 to 180. diff --git a/docs/api-model.md b/docs/api-model.md deleted file mode 100644 index 7aaf3687..00000000 --- a/docs/api-model.md +++ /dev/null @@ -1,448 +0,0 @@ -## Model object - -A Loopback `Model` is a vanilla JavaScript class constructor with an attached set of properties and options. A `Model` instance is created by passing a data object containing properties to the `Model` constructor. A `Model` constructor will clean the object passed to it and only set the values matching the properties you define. - -```js -// valid color -var Color = loopback.createModel('color', {name: String}); -var red = new Color({name: 'red'}); -console.log(red.name); // red - -// invalid color -var foo = new Color({bar: 'bat baz'}); -console.log(foo.bar); // undefined -``` - -**Properties** - -A model defines a list of property names, types and other validation metadata. A [DataSource](#data-source) uses this definition to validate a `Model` during operations such as `save()`. - -**Options** - -Some [DataSources](#data-source) may support additional `Model` options. - -Define A Loopbackmodel. - -```js -var User = loopback.createModel('user', { - first: String, - last: String, - age: Number -}); -``` - -### Methods - -#### Model.attachTo(dataSource) - -Attach a model to a [DataSource](#data-source). Attaching a [DataSource](#data-source) updates the model with additional methods and behaviors. - -```js -var oracle = loopback.createDataSource({ - connector: require('loopback-connector-oracle'), - host: '111.22.333.44', - database: 'MYDB', - username: 'username', - password: 'password' -}); - -User.attachTo(oracle); -``` - -NOTE: until a model is attached to a data source it will not have any attached methods. - -### Properties - -#### Model.properties - -An object containing a normalized set of properties supplied to `loopback.createModel(name, properties)`. - -Example: - -```js -var props = { - a: String, - b: {type: 'Number'}, - c: {type: 'String', min: 10, max: 100}, - d: Date, - e: loopback.GeoPoint -}; - -var MyModel = loopback.createModel('foo', props); - -console.log(MyModel.properties); -``` - -Outputs: - -```js -{ - "a": {type: String}, - "b": {type: Number}, - "c": { - "type": String, - "min": 10, - "max": 100 - }, - "d": {type: Date}, - "e": {type: GeoPoint}, - "id": { - "id": 1 - } -} -``` - -### CRUD and Query Mixins - -Mixins are added by attaching a vanilla model to a [data source](#data-source) with a [connector](#connectors). Each [connector](#connectors) enables its own set of operations that are mixed into a `Model` as methods. To see available methods for a data source call `dataSource.operations()`. - -Log the available methods for a memory data source. - -```js -var ops = loopback - .createDataSource({connector: loopback.Memory}) - .operations(); - -console.log(Object.keys(ops)); -``` - -Outputs: - -```js -[ 'create', - 'updateOrCreate', - 'upsert', - 'findOrCreate', - 'exists', - 'findById', - 'find', - 'all', - 'findOne', - 'destroyAll', - 'deleteAll', - 'count', - 'include', - 'relationNameFor', - 'hasMany', - 'belongsTo', - 'hasAndBelongsToMany', - 'save', - 'isNewRecord', - 'destroy', - 'delete', - 'updateAttribute', - 'updateAttributes', - 'reload' ] -``` - -Here is the definition of the `count()` operation. - -```js -{ - accepts: [ { arg: 'where', type: 'object' } ], - http: { verb: 'get', path: '/count' }, - remoteEnabled: true, - name: 'count' -} -``` - -### Static Methods - -**Note:** These are the default mixin methods for a `Model` attached to a data source. See the specific connector for additional API documentation. - -#### Model.create(data, [callback]) - -Create an instance of Model with given data and save to the attached data source. Callback is optional. - -```js -User.create({first: 'Joe', last: 'Bob'}, function(err, user) { - console.log(user instanceof User); // true -}); -``` - -**Note:** You must include a callback and use the created model provided in the callback if your code depends on your model being saved or having an `id`. - -#### Model.count([query], callback) - -Query count of Model instances in data source. Optional query param allows to count filtered set of Model instances. - -```js -User.count({approved: true}, function(err, count) { - console.log(count); // 2081 -}); -``` - -#### Model.find(filter, callback) - -Find all instances of Model, matched by query. Fields used for filter and sort should be declared with `{index: true}` in model definition. - -**filter** - - - **where** `Object` { key: val, key2: {gt: 'val2'}} The search criteria - - Format: {key: val} or {key: {op: val}} - - Operations: - - gt: > - - gte: >= - - lt: < - - lte: <= - - between - - inq: IN - - nin: NOT IN - - neq: != - - like: LIKE - - nlike: NOT LIKE - - - **include** `String`, `Object` or `Array` Allows you to load relations of several objects and optimize numbers of requests. - - Format: - - 'posts': Load posts - - ['posts', 'passports']: Load posts and passports - - {'owner': 'posts'}: Load owner and owner's posts - - {'owner': ['posts', 'passports']}: Load owner, owner's posts, and owner's passports - - {'owner': [{posts: 'images'}, 'passports']}: Load owner, owner's posts, owner's posts' images, and owner's passports - - - **order** `String` The sorting order - - Format: 'key1 ASC, key2 DESC' - - - **limit** `Number` The maximum number of instances to be returned - - **skip** `Number` Skip the number of instances - - **offset** `Number` Alias for skip - - - **fields** `Object|Array|String` The included/excluded fields - - `['foo']` or `'foo'` - include only the foo property - - `['foo', 'bar']` - include the foo and bar properties - - `{foo: true}` - include only foo - - `{bat: false}` - include all properties, exclude bat - -Find the second page of 10 users over age 21 in descending order exluding the password property. - -```js -User.find({ - where: { - age: {gt: 21}}, - order: 'age DESC', - limit: 10, - skip: 10, - fields: {password: false} - }, - console.log -); -``` - -**Note:** See the specific connector's [docs](#connectors) for more info. - -#### Model.destroyAll([where], callback) - -Delete all Model instances from data source. **Note:** destroyAll method does not perform destroy hooks. - -```js -Product.destroyAll({price: {gt: 99}}, function(err) { - // removed matching products -}); -``` - -> **NOTE:* `where` is optional and a where object... do NOT pass a filter object - -#### Model.findById(id, callback) - -Find instance by id. - -```js -User.findById(23, function(err, user) { - console.info(user.id); // 23 -}); -``` - -#### Model.findOne(where, callback) - -Find a single instance that matches the given where expression. - -```js -User.findOne({where: {id: 23}}, function(err, user) { - console.info(user.id); // 23 -}); -``` - -#### Model.upsert(data, callback) - -Update when record with id=data.id found, insert otherwise. **Note:** no setters, validations or hooks applied when using upsert. - -#### Custom static methods - -Define a static model method. - -```js -User.login = function (username, password, fn) { - var passwordHash = hashPassword(password); - this.findOne({username: username}, function (err, user) { - var failErr = new Error('login failed'); - - if(err) { - fn(err); - } else if(!user) { - fn(failErr); - } else if(user.password === passwordHash) { - MyAccessTokenModel.create({userId: user.id}, function (err, accessToken) { - fn(null, accessToken.id); - }); - } else { - fn(failErr); - } - }); -} -``` - -Setup the static model method to be exposed to clients as a [remote method](#remote-method). - -```js -loopback.remoteMethod( - User.login, - { - accepts: [ - {arg: 'username', type: 'string', required: true}, - {arg: 'password', type: 'string', required: true} - ], - returns: {arg: 'sessionId', type: 'any'}, - http: {path: '/sign-in'} - } -); -``` - -### Instance methods - -**Note:** These are the default mixin methods for a `Model` attached to a data source. See the specific connector for additional API documentation. - -#### model.save([options], [callback]) - -Save an instance of a Model to the attached data source. - -```js -var joe = new User({first: 'Joe', last: 'Bob'}); -joe.save(function(err, user) { - if(user.errors) { - console.log(user.errors); - } else { - console.log(user.id); - } -}); -``` - -#### model.updateAttributes(data, [callback]) - -Save specified attributes to the attached data source. - -```js -user.updateAttributes({ - first: 'updatedFirst', - name: 'updatedLast' -}, fn); -``` - -#### model.destroy([callback]) - -Remove a model from the attached data source. - -```js -model.destroy(function(err) { - // model instance destroyed -}); -``` - -#### Custom instance methods - -Define an instance method. - -```js -User.prototype.logout = function (fn) { - MySessionModel.destroyAll({userId: this.id}, fn); -} -``` - -Define a remote model instance method. - -```js -loopback.remoteMethod(User.prototype.logout) -``` - -### Relationships - -#### Model.hasMany(Model, options) - -Define a "one to many" relationship. - -```js -// by referencing model -Book.hasMany(Chapter); -// specify the name -Book.hasMany('chapters', {model: Chapter}); -``` - -Query and create the related models. - -```js -Book.create(function(err, book) { - // create a chapter instance - // ready to be saved in the data source - var chapter = book.chapters.build({name: 'Chapter 1'}); - - // save the new chapter - chapter.save(); - - // you can also call the Chapter.create method with - // the `chapters` property which will build a chapter - // instance and save the it in the data source - book.chapters.create({name: 'Chapter 2'}, function(err, savedChapter) { - // this callback is optional - }); - - // query chapters for the book using the - book.chapters(function(err, chapters) { - // all chapters with bookId = book.id - console.log(chapters); - }); - - book.chapters({where: {name: 'test'}, function(err, chapters) { - // all chapters with bookId = book.id and name = 'test' - console.log(chapters); - }); -}); -``` - -#### Model.belongsTo(Model, options) - -A `belongsTo` relation sets up a one-to-one connection with another model, such -that each instance of the declaring model "belongs to" one instance of the other -model. For example, if your application includes users and posts, and each post -can be written by exactly one user. - -```js - Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}); -``` - -The code above basically says Post has a reference called `author` to User using -the `userId` property of Post as the foreign key. Now we can access the author -in one of the following styles: - -```js - post.author(callback); // Get the User object for the post author asynchronously - post.author(); // Get the User object for the post author synchronously - post.author(user) // Set the author to be the given user -``` - -#### Model.hasAndBelongsToMany(Model, options) - -A `hasAndBelongsToMany` relation creates a direct many-to-many connection with -another model, with no intervening model. For example, if your application -includes users and groups, with each group having many users and each user -appearing in many groups, you could declare the models this way, - -```js - User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'}); - user.groups(callback); // get groups of the user - user.groups.create(data, callback); // create a new group and connect it with the user - user.groups.add(group, callback); // connect an existing group with the user - user.groups.remove(group, callback); // remove the user from the group -``` - -### Shared methods - -Any static or instance method can be decorated as `shared`. These methods are exposed over the provided transport (eg. [loopback.rest](#rest)). diff --git a/lib/middleware/rest.js b/lib/middleware/rest.js index 3f87c126..5e91c7f4 100644 --- a/lib/middleware/rest.js +++ b/lib/middleware/rest.js @@ -1,17 +1,24 @@ -/** +/*! * Module dependencies. */ var loopback = require('../loopback'); -/** +/*! * Export the middleware. */ module.exports = rest; /** - * Build a temp app for mounting resources. + * Expose models over REST. + * + * For example: + * ```js + * app.use(loopback.rest()); + * ``` + * For more information, see [Exposing models over a REST API](http://docs.strongloop.com/display/DOC/Exposing+models+over+a+REST+API). + * @header loopback.rest() */ function rest() { diff --git a/lib/middleware/status.js b/lib/middleware/status.js index 02068548..085c5975 100644 --- a/lib/middleware/status.js +++ b/lib/middleware/status.js @@ -1,9 +1,22 @@ -/** +/*! * Export the middleware. */ module.exports = status; +/** + * Return [HTTP response](http://expressjs.com/4x/api.html#res.send) with basic application status information: + * date the application was started and uptime, in JSON format. + * For example: + * ```js + * { + * "started": "2014-06-05T00:26:49.750Z", + * "uptime": 9.394 + * } + * ``` + * + * @header loopback.status() + */ function status() { var started = new Date(); diff --git a/lib/middleware/token.js b/lib/middleware/token.js index 88a93c24..6087fd31 100644 --- a/lib/middleware/token.js +++ b/lib/middleware/token.js @@ -12,14 +12,17 @@ var assert = require('assert'); module.exports = token; /** - * **Options** + * Check for an access token in cookies, headers, and query string parameters. + * This function always checks for the following: * - * - `cookies` - An `Array` of cookie names - * - `headers` - An `Array` of header names - * - `params` - An `Array` of param names - * - `model` - Specify an AccessToken class to use + * - `access_token` + * - `X-Access-Token` + * - `authorization` + * + * It checks for these values in cookies, headers, and query string parameters _in addition_ to the items + * specified in the options parameter. * - * Each array is used to add additional keys to find an `accessToken` for a `request`. + * **NOTE:** This function only checks for [signed cookies](http://expressjs.com/api.html#req.signedCookies). * * The following example illustrates how to check for an `accessToken` in a custom cookie, query string parameter * and header called `foo-auth`. @@ -28,23 +31,16 @@ module.exports = token; * app.use(loopback.token({ * cookies: ['foo-auth'], * headers: ['foo-auth', 'X-Foo-Auth'], - * cookies: ['foo-auth', 'foo_auth'] + * params: ['foo-auth', 'foo_auth'] * })); * ``` - * - * **Defaults** - * - * By default the following names will be checked. These names are appended to any optional names. They will always - * be checked, but any names specified will be checked first. - * - * - **access_token** - * - **X-Access-Token** - * - **authorization** - * - **access_token** - * - * **NOTE:** The `loopback.token()` middleware will only check for [signed cookies](http://expressjs.com/api.html#req.signedCookies). * - * @header loopback.token(options) + * @options {Object} [options] Each option array is used to add additional keys to find an `accessToken` for a `request`. + * @property {Array} [cookies] Array of cookie names. + * @property {Array} [headers] Array of header names. + * @property {Array} [params] Array of param names. + * @property {Array} [model] An AccessToken object to use. + * @header loopback.token([options]) */ function token(options) { diff --git a/lib/middleware/urlNotFound.js b/lib/middleware/urlNotFound.js index 9f9735e6..a73f6804 100644 --- a/lib/middleware/urlNotFound.js +++ b/lib/middleware/urlNotFound.js @@ -1,13 +1,14 @@ -/** +/*! * Export the middleware. + * See discussion in Connect pull request #954 for more details + * https://github.com/senchalabs/connect/pull/954. */ module.exports = urlNotFound; /** * Convert any request not handled so far to a 404 error * to be handled by error-handling middleware. - * See discussion in Connect pull request #954 for more details - * https://github.com/senchalabs/connect/pull/954 + * @header loopback.urlNotFound() */ function urlNotFound() { return function raiseUrlNotFoundError(req, res, next) { diff --git a/lib/models/data-model.js b/lib/models/data-model.js index 4c0aeafd..66060e2f 100644 --- a/lib/models/data-model.js +++ b/lib/models/data-model.js @@ -73,14 +73,12 @@ function convertNullToNotFoundError(ctx, cb) { } /** - * Create new instance of Model class, saved in database + * Create new instance of Model class, saved in database. * - * @param data [optional] - * @param callback(err, obj) - * callback called with arguments: - * - * - err (null or Error) - * - instance (null or Model) + * @param {Object} [data] Object containing model instance data. + * @callback {Function} callback Callback function; see below. + * @param {Error|null} err Error object + * @param {Model|null} Model instance */ DataModel.create = function (data, callback) { @@ -100,10 +98,15 @@ setRemoting(DataModel.create, { * @param {Function} [callback] The callback function */ -DataModel.upsert = DataModel.updateOrCreate = function upsert(data, callback) { +DataModel.upsert = function upsert(data, callback) { throwNotAttached(this.modelName, 'updateOrCreate'); }; +/** + * Alias for upsert function. + */ +DataModel.updateOrCreate = DataModel.upsert; + // upsert ~ remoting attributes setRemoting(DataModel.upsert, { description: 'Update an existing model instance or insert a new one into the data source', @@ -113,8 +116,8 @@ setRemoting(DataModel.upsert, { }); /** - * Find one record, same as `all`, limited by 1 and return object, not collection, - * if not found, create using data provided as second argument + * Find one record instance, same as `all`, limited by one and return object, not collection. + * If not found, create the record using data provided as second argument. * * @param {Object} query - search conditions: {where: {test: 'me'}}. * @param {Object} data - object to create. @@ -126,7 +129,7 @@ DataModel.findOrCreate = function findOrCreate(query, data, callback) { }; /** - * Check whether a model instance exists in database + * Check whether a model instance exists in database. * * @param {id} id - identifier of object (primary key value) * @param {Function} cb - callbacl called with (err, exists: Bool) @@ -263,10 +266,15 @@ setRemoting(DataModel.count, { }); /** - * Save instance. When instance haven't id, create method called instead. - * Triggers: validate, save, update | create - * @param options {validate: true, throws: false} [optional] - * @param callback(err, obj) + * Save instance. When instance does not have an ID, create method instead. + * Triggers: validate, save, update or create. + * + * @options [options] Options + * @property {Boolean} validate Whether to validate. + * @property {Boolean} throws + * @callback {Function} callback Callback function. + * @param {Error} err Error object + * @param {Object} */ DataModel.prototype.save = function (options, callback) { @@ -290,7 +298,7 @@ DataModel.prototype.save = function (options, callback) { /** * Determine if the data model is new. - * @returns {Boolean} + * @returns {Boolean} True if data model is new. */ DataModel.prototype.isNewRecord = function () { @@ -310,9 +318,8 @@ DataModel.prototype.destroy = function (cb) { }; /** - * Update single attribute - * - * equals to `updateAttributes({name: value}, cb) + * Update single attribute. + * Equivalent to `updateAttributes({name: value}, cb)` * * @param {String} name - name of property * @param {Mixed} value - value of property @@ -326,7 +333,7 @@ DataModel.prototype.updateAttribute = function updateAttribute(name, value, call /** * Update set of attributes * - * this method performs validation before updating + * Performs validation before updating * * @trigger `validation`, `save` and `update` hooks * @param {Object} data - data to update @@ -349,7 +356,9 @@ setRemoting(DataModel.prototype.updateAttributes, { * Reload object from persistence * * @requires `id` member of `object` to be able to call `find` - * @param {Function} callback - called with (err, instance) arguments + * @callback {Function} callback Callback function + * @param {Error} err + * @param {Object} instance */ DataModel.prototype.reload = function reload(callback) { diff --git a/lib/models/user.js b/lib/models/user.js index 73a18d39..48a15eea 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -2,14 +2,12 @@ * Module Dependencies. */ -var Model = require('../loopback').Model - , loopback = require('../loopback') +var loopback = require('../loopback') + , Model = loopback.Model , path = require('path') , SALT_WORK_FACTOR = 10 , crypto = require('crypto') , bcrypt = require('bcryptjs') - , passport = require('passport') - , LocalStrategy = require('passport-local').Strategy , BaseAccessToken = require('./access-token') , DEFAULT_TTL = 1209600 // 2 weeks in seconds , DEFAULT_RESET_PW_TTL = 15 * 60 // 15 mins in seconds @@ -128,6 +126,23 @@ var options = { var User = module.exports = Model.extend('User', properties, options); +/** + * Create access token for the logged in user. This method can be overridden to + * customize how access tokens are generated + * + * @param [Number} ttl The requested ttl + * @callack {Function} cb The callback function + * @param {String|Error} err The error string or object + * @param {AccessToken} token The generated access token object + */ +User.prototype.createAccessToken = function(ttl, cb) { + var userModel = this.constructor; + ttl = Math.min(ttl || userModel.settings.ttl, userModel.settings.maxTTL); + this.accessTokens.create({ + ttl: ttl + }, cb); +}; + /** * Login a user by with the given `credentials`. * @@ -144,6 +159,7 @@ var User = module.exports = Model.extend('User', properties, options); */ User.login = function (credentials, include, fn) { + var self = this; if (typeof include === 'function') { fn = include; include = undefined; @@ -162,7 +178,7 @@ User.login = function (credentials, include, fn) { return fn(err); } - this.findOne({where: query}, function(err, user) { + self.findOne({where: query}, function(err, user) { var defaultError = new Error('login failed'); defaultError.statusCode = 401; @@ -175,9 +191,7 @@ User.login = function (credentials, include, fn) { debug('An error is reported from User.hasPassword: %j', err); fn(defaultError); } else if(isMatch) { - user.accessTokens.create({ - ttl: Math.min(credentials.ttl || User.settings.ttl, User.settings.maxTTL) - }, function(err, token) { + user.createAccessToken(credentials.ttl, function(err, token) { if (err) return fn(err); if (include === 'user') { // NOTE(bajtos) We can't set token.user here: diff --git a/package.json b/package.json index bf72e84f..1c7b6e29 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "mobile", "mBaaS" ], - "version": "1.8.6", + "version": "1.8.7", "scripts": { "test": "mocha -R spec" }, @@ -35,8 +35,6 @@ "express": "~3.5.0", "strong-remoting": "~1.5.0", "inflection": "~1.3.5", - "passport": "~0.2.0", - "passport-local": "~1.0.0", "nodemailer": "~0.6.5", "ejs": "~1.0.0", "bcryptjs": "~0.7.12", diff --git a/test/user.test.js b/test/user.test.js index 25b1df8f..2b3805a1 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1,6 +1,5 @@ var User; var AccessToken = loopback.AccessToken; -var passport = require('passport'); var MailConnector = require('../lib/connectors/mail'); var userMemory = loopback.createDataSource({ @@ -9,6 +8,7 @@ var userMemory = loopback.createDataSource({ describe('User', function(){ var validCredentials = {email: 'foo@bar.com', password: 'bar'}; + var validCredentialsWithTTL = {email: 'foo@bar.com', password: 'bar', ttl: 3600}; var invalidCredentials = {email: 'foo1@bar.com', password: 'bar1'}; var incompleteCredentials = {password: 'bar1'}; @@ -117,6 +117,44 @@ describe('User', function(){ done(); }); }); + + it('Login a user by providing credentials with TTL', function(done) { + User.login(validCredentialsWithTTL, function (err, accessToken) { + assert(accessToken.userId); + assert(accessToken.id); + assert.equal(accessToken.ttl, validCredentialsWithTTL.ttl); + assert.equal(accessToken.id.length, 64); + + done(); + }); + }); + + it('Login a user using a custom createAccessToken', function(done) { + var createToken = User.prototype.createAccessToken; // Save the original method + // Override createAccessToken + User.prototype.createAccessToken = function(ttl, cb) { + // Reduce the ttl by half for testing purpose + this.accessTokens.create({ttl: ttl /2 }, cb); + }; + User.login(validCredentialsWithTTL, function (err, accessToken) { + assert(accessToken.userId); + assert(accessToken.id); + assert.equal(accessToken.ttl, 1800); + assert.equal(accessToken.id.length, 64); + + User.findById(accessToken.userId, function(err, user) { + user.createAccessToken(120, function (err, accessToken) { + assert(accessToken.userId); + assert(accessToken.id); + assert.equal(accessToken.ttl, 60); + assert.equal(accessToken.id.length, 64); + // Restore create access token + User.prototype.createAccessToken = createToken; + done(); + }); + }); + }); + }); it('Login a user over REST by providing credentials', function(done) { request(app)