From 9c2098cd352ee6b24633ac3a5a45c1792565ddd6 Mon Sep 17 00:00:00 2001 From: crandmck Date: Wed, 12 Mar 2014 16:28:46 -0700 Subject: [PATCH 1/8] Updates to JSDoc comments for API doc --- lib/dao.js | 95 ++++++++------- lib/datasource.js | 270 ++++++++++++++++++++++++++++++------------- lib/geo.js | 73 +++++++++--- lib/include.js | 38 +++--- lib/model-builder.js | 73 ++++++------ lib/model.js | 38 +++--- lib/relations.js | 47 +++++--- lib/sql.js | 10 +- lib/validations.js | 36 +++--- 9 files changed, 426 insertions(+), 254 deletions(-) diff --git a/lib/dao.js b/lib/dao.js index 9025cbf7..2df44e62 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -1,9 +1,9 @@ -/** +/*! * Module exports class Model */ module.exports = DataAccessObject; -/** +/*! * Module dependencies */ var jutil = require('./jutil'); @@ -19,15 +19,14 @@ var fieldsToArray = utils.fieldsToArray; var removeUndefined = utils.removeUndefined; /** - * DAO class - base class for all persist objects - * provides **common API** to access any database connector. - * This class describes only abstract behavior layer, refer to `lib/connectors/*.js` - * to learn more about specific connector implementations + * Base class for all persistent objects. + * Provides a common API to access any database connector. + * This class describes only abstract behavior. Refer to the specific connector (`lib/connectors/*.js`) for details. * - * `DataAccessObject` mixes `Inclusion` classes methods + * `DataAccessObject` mixes `Inclusion` classes methods. * - * @constructor - * @param {Object} data - initial object data + * @class DataAccessObject + * @param {Object} data Initial object data */ function DataAccessObject() { if (DataAccessObject._mixins) { @@ -71,14 +70,15 @@ DataAccessObject._forDB = function (data) { }; /** - * Create new instance of Model class, saved in database - * - * @param data [optional] - * @param callback(err, obj) - * callback called with arguments: + * Create new instance of Model class, saved in database. + * The callback function is called with arguments: * * - err (null or Error) * - instance (null or Model) + * + * @param data {Object} Optional data object + * @param callback {Function} Callback function + */ DataAccessObject.create = function (data, callback) { if (stillConnecting(this.getDataSource(), this, arguments)) return; @@ -208,9 +208,9 @@ function stillConnecting(dataSource, obj, args) { } /** - * Update or insert a model instance + * Update or insert a model instance. * @param {Object} data The model instance data - * @param {Function} [callback] The callback function + * @param {Function} callback The callback function (optional). */ DataAccessObject.upsert = DataAccessObject.updateOrCreate = function upsert(data, callback) { if (stillConnecting(this.getDataSource(), this, arguments)) return; @@ -254,9 +254,9 @@ setRemoting(DataAccessObject.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 * - * @param {Object} query - search conditions: {where: {test: 'me'}}. - * @param {Object} data - object to create. - * @param {Function} cb - callback called with (err, instance) + * @param {Object} query Search conditions: {where: {test: 'me'}}. + * @param {Object} data Object to create. + * @param {Function} cb Callback called with (err, instance) */ DataAccessObject.findOrCreate = function findOrCreate(query, data, callback) { if (query === undefined) { @@ -282,8 +282,8 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, callback) { /** * 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) + * @param {id} id Identifier of object (primary key value) + * @param {Function} cb Callback function called with (err, exists: Bool) */ DataAccessObject.exists = function exists(id, cb) { if (stillConnecting(this.getDataSource(), this, arguments)) return; @@ -306,8 +306,8 @@ setRemoting(DataAccessObject.exists, { /** * Find object by id * - * @param {*} id - primary key value - * @param {Function} cb - callback called with (err, instance) + * @param {*} id Primary key value + * @param {Function} cb Callback called with (err, instance) */ DataAccessObject.findById = function find(id, cb) { if (stillConnecting(this.getDataSource(), this, arguments)) return; @@ -458,20 +458,16 @@ DataAccessObject._coerce = function (where) { /** * Find all instances of Model, matched by query - * make sure you have marked as `index: true` fields for filter or sort - * - * @param {Object} params (optional) - * + * make sure you have marked as `index: true` fields for filter or sort. + * The params object: * - where: Object `{ key: val, key2: {gt: 'val2'}}` - * - include: String, Object or Array. See DataAccessObject.include documentation. + * - include: String, Object or Array. See `DataAccessObject.include()`. * - order: String * - limit: Number * - skip: Number - * - * @param {Function} callback (required) called with arguments: - * - * - err (null or Error) - * - Array of instances + * + * @param {Object} params (optional) + * @param {Function} callback (required) called with two arguments: err (null or Error), array of instances */ DataAccessObject.find = function find(params, cb) { @@ -598,8 +594,8 @@ setRemoting(DataAccessObject.find, { /** * Find one record, same as `all`, limited by 1 and return object, not collection * - * @param {Object} params - search conditions: {where: {test: 'me'}} - * @param {Function} cb - callback called with (err, instance) + * @param {Object} params Search conditions: {where: {test: 'me'}} + * @param {Function} cb Callback called with (err, instance) */ DataAccessObject.findOne = function findOne(params, cb) { if (stillConnecting(this.getDataSource(), this, arguments)) return; @@ -626,7 +622,7 @@ setRemoting(DataAccessObject.findOne, { /** * Destroy all matching records * @param {Object} [where] An object that defines the criteria - * @param {Function} [cb] - callback called with (err) + * @param {Function} [cb] Callback called with (err) */ DataAccessObject.remove = DataAccessObject.deleteAll = @@ -657,7 +653,7 @@ DataAccessObject.remove = /** * Destroy a record by id * @param {*} id The id value - * @param {Function} cb - callback called with (err) + * @param {Function} cb Callback called with (err) */ DataAccessObject.removeById = DataAccessObject.deleteById = @@ -683,8 +679,8 @@ setRemoting(DataAccessObject.deleteById, { /** * Return count of matched records * - * @param {Object} where - search conditions (optional) - * @param {Function} cb - callback, called with (err, count) + * @param {Object} where Search conditions (optional) + * @param {Function} cb Callback, called with (err, count) */ DataAccessObject.count = function (where, cb) { if (stillConnecting(this.getDataSource(), this, arguments)) return; @@ -797,7 +793,7 @@ DataAccessObject.prototype._adapter = function () { /** * Delete object from persistence * - * @triggers `destroy` hook (async) before and after destroying object + * Triggers `destroy` hook (async) before and after destroying object */ DataAccessObject.prototype.remove = DataAccessObject.prototype.delete = @@ -825,9 +821,9 @@ DataAccessObject.prototype.remove = * * equals to `updateAttributes({name: value}, cb) * - * @param {String} name - name of property - * @param {Mixed} value - value of property - * @param {Function} callback - callback called with (err, instance) + * @param {String} name Name of property + * @param {Mixed} value Value of property + * @param {Function} callback Callback function called with (err, instance) */ DataAccessObject.prototype.updateAttribute = function updateAttribute(name, value, callback) { var data = {}; @@ -841,8 +837,8 @@ DataAccessObject.prototype.updateAttribute = function updateAttribute(name, valu * this method performs validation before updating * * @trigger `validation`, `save` and `update` hooks - * @param {Object} data - data to update - * @param {Function} callback - callback called with (err, instance) + * @param {Object} data Data to update + * @param {Function} callback Callback function called with (err, instance) */ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb) { if (stillConnecting(this.getDataSource(), this, arguments)) return; @@ -911,9 +907,8 @@ setRemoting(DataAccessObject.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 + * Requires `id` member of `object` to be able to call `find` + * @param {Function} callback Called with (err, instance) arguments */ DataAccessObject.prototype.reload = function reload(callback) { if (stillConnecting(this.getDataSource(), this, arguments)) return; @@ -946,8 +941,8 @@ function defineReadonlyProp(obj, key, value) { var defineScope = require('./scope.js').defineScope; -/** - * Define scope +/*! + * Define scope. N.B. Not clear if this needs to be exposed in API doc. */ DataAccessObject.scope = function (name, filter, targetClass) { defineScope(this, targetClass || this, name, filter); diff --git a/lib/datasource.js b/lib/datasource.js index 42c612f6..32a0ee4b 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -19,7 +19,7 @@ if (process.env.DEBUG === 'loopback') { } var debug = require('debug')('loopback:datasource'); -/** +/*! * Export public API */ exports.DataSource = DataSource; @@ -30,23 +30,32 @@ exports.DataSource = DataSource; var slice = Array.prototype.slice; /** - * DataSource - connector-specific classes factory. + * 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. * - * All classes in single dataSource shares same connector type and - * one database connection - * - * @param {String} name - type of dataSource connector (mysql, mongoose, oracle, redis) - * @param {Object} settings - any database-specific settings which we need to - * establish connection (of course it depends on specific connector) + * Define a data source for persisting models. + * Typically, you create a DataSource by calling createDataSource() on the LoopBack object; for example: + * ```js + * var oracle = loopback.createDataSource({ + * connector: 'oracle', + * host: '111.22.333.44', + * database: 'MYDB', + * username: 'username', + * password: 'password' + * }); + * ``` * + * All classes in single dataSource share same the connector type and + * one database connection. The `settings` argument is an object that can have the following properties: * - host * - port * - username * - password * - database - * - debug {Boolean} = false + * - debug (Boolean, default is false) * - * @example DataSource creation, waiting for connection callback + * @desc For example, the following creates a DataSource, and waits for a connection callback. * ``` * var dataSource = new DataSource('mysql', { database: 'myapp_test' }); * dataSource.define(...); @@ -54,6 +63,9 @@ var slice = Array.prototype.slice; * // work with database * }); * ``` + * @class Define new DataSource + * @param {String} name Type of dataSource connector (mysql, mongoose, oracle, redis) + * @param {Object} settings Database-specific settings to establish connection (settings depend on specific connector). See above. */ function DataSource(name, settings, modelBuilder) { if (!(this instanceof DataSource)) { @@ -477,28 +489,19 @@ DataSource.prototype.setupDataAccess = function (modelClass, settings) { }; /** - * Define a model class - * - * @param {String} className - * @param {Object} properties - hash of class properties in format - * `{property: Type, property2: Type2, ...}` - * or - * `{property: {type: Type}, property2: {type: Type2}, ...}` - * @param {Object} settings - other configuration of class - * @return newly created class - * - * @example simple case + * Define a model class. + * Simple example: * ``` - * var User = dataSource.define('User', { + * var User = dataSource.createModel('User', { * email: String, * password: String, * birthDate: Date, * activated: Boolean * }); * ``` - * @example more advanced case + * More advanced example * ``` - * var User = dataSource.define('User', { + * var User = dataSource.createModel('User', { * email: { type: String, limit: 150, index: true }, * password: { type: String, limit: 50 }, * birthDate: Date, @@ -506,6 +509,29 @@ DataSource.prototype.setupDataAccess = function (modelClass, settings) { * activated: { type: Boolean, default: false } * }); * ``` + * You can also 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} + * ] + * }); + * ``` + * + * @param {String} className Name of the model to create. + * @param {Object} properties Hash of class properties in format `{property: Type, property2: Type2, ...}` or `{property: {type: Type}, property2: {type: Type2}, ...}` + * @param {Object} settings Other configuration settings. + * @returns newly created class + * */ DataSource.prototype.createModel = DataSource.prototype.define = function defineClass(className, properties, settings) { @@ -591,14 +617,14 @@ DataSource.prototype.mixin = function (ModelCtor) { }; /** - * @see ModelBuilder.prototype.getModel + * See ModelBuilder.getModel */ DataSource.prototype.getModel = function (name, forceCreate) { return this.modelBuilder.getModel(name, forceCreate); }; /** - * @see ModelBuilder.prototype.getModelDefinition + * See ModelBuilder.getModelDefinition */ DataSource.prototype.getModelDefinition = function (name) { return this.modelBuilder.getModelDefinition(name); @@ -618,9 +644,9 @@ DataSource.prototype.getTypes = function () { }; /** - * Check the data source supports the given types - * @param String|String[]) types A type name or an array of type names - * @return {Boolean} true if all types are supported by the data source + * Check the data source supports the specified types. + * @param {String} types Type name or an array of type names. Can also be array of Strings. + * @returns {Boolean} true if all types are supported by the data source */ DataSource.prototype.supportTypes = function (types) { var supportedTypes = this.getTypes(); @@ -670,9 +696,9 @@ DataSource.prototype.attach = function (modelClass) { /** * Define single property named `prop` on `model` * - * @param {String} model - name of model - * @param {String} prop - name of property - * @param {Object} params - property settings + * @param {String} model Name of model + * @param {String} prop Name of property + * @param {Object} params Property settings */ DataSource.prototype.defineProperty = function (model, prop, params) { this.modelBuilder.defineProperty(model, prop, params); @@ -685,12 +711,12 @@ DataSource.prototype.defineProperty = function (model, prop, params) { /** * Drop each model table and re-create. - * This method make sense only for sql connectors. + * This method applies only to SQL connectors. * - * @param {String} or {[String]} Models to be migrated, if not present, apply to all models - * @param {Function} [cb] The callback function + * @param {String} Models to be migrated, if not present, apply to all models. This can also be an array of Strings. + * @param {Function} cb Callback function. Optional. * - * @warning All data will be lost! Use autoupdate if you need your data. + * WARNING: Calling this function will cause all data to be lost! Use autoupdate if you need to preserve data. */ DataSource.prototype.automigrate = function (models, cb) { this.freeze(); @@ -709,7 +735,7 @@ DataSource.prototype.automigrate = function (models, cb) { * Update existing database tables. * This method make sense only for sql connectors. * - * @param {String} or {[String]} Models to be migrated, if not present, apply to all models + * @param {String} Models to be migrated, if not present, apply to all models. This can also be an array of Strings. * @param {Function} [cb] The callback function */ DataSource.prototype.autoupdate = function (models, cb) { @@ -729,15 +755,15 @@ DataSource.prototype.autoupdate = function (models, cb) { * Discover existing database tables. * This method returns an array of model objects, including {type, name, onwer} * - * `options` + * Kyes in options object: * - * all: true - Discovering all models, false - Discovering the models owned by the current user - * views: true - Including views, false - only tables - * limit: The page size - * offset: The starting index + * - all: true - Discovering all models, false - Discovering the models owned by the current user + * - views: true - Including views, false - only tables + * - limit: The page size + * - offset: The starting index * * @param {Object} options The options - * @param {Function} [cb] The callback function + * @param {Function} Callback function. Optional. * */ DataSource.prototype.discoverModelDefinitions = function (options, cb) { @@ -765,24 +791,25 @@ DataSource.prototype.discoverModelDefinitionsSync = function (options) { /** * Discover properties for a given model. * - * `property description` + * property description: * - * owner {String} The database owner or schema - * tableName {String} The table/view name - * columnName {String} The column name - * dataType {String} The data type - * dataLength {Number} The data length - * dataPrecision {Number} The numeric data precision - * dataScale {Number} The numeric data scale - * nullable {Boolean} If the data can be null +*| Key | Type | Description | +*|-----|------|-------------| +*|owner | String | Database owner or schema| +*|tableName | String | Table/view name| +*|columnName | String | Column name| +*|dataType | String | Data type| +*|dataLength | Number | Data length| +*|dataPrecision | Number | Numeric data precision| +*|dataScale |Number | Numeric data scale| +*|nullable |Boolean | If true, then the data can be null| * - * `options` - * - * owner/schema The database owner/schema + * Options: + * - owner/schema The database owner/schema * * @param {String} modelName The table/view name * @param {Object} options The options - * @param {Function} [cb] The callback function + * @param {Function} cb Callback function. Optional * */ DataSource.prototype.discoverModelProperties = function (modelName, options, cb) { @@ -809,21 +836,20 @@ DataSource.prototype.discoverModelPropertiesSync = function (modelName, options) }; /** - * Discover primary keys for a given owner/modelName + * Discover primary keys for a given owner/modelName. * * Each primary key column description has the following columns: * - * owner {String} => table schema (may be null) - * tableName {String} => table name - * columnName {String} => column name - * keySeq {Number} => sequence number within primary key( a value of 1 represents the first column of the primary key, a value of 2 would represent the second column within the primary key). - * pkName {String} => primary key name (may be null) + * - owner {String} => table schema (may be null) + * - tableName {String} => table name + * - columnName {String} => column name + * - keySeq {Number} => sequence number within primary key( a value of 1 represents the first column of the primary key, a value of 2 would represent the second column within the primary key). + * - pkName {String} => primary key name (may be null) * - * The owner, default to current user + * The owner defaults to current user. * - * `options` - * - * owner/schema The database owner/schema + * Options: + * - owner/schema The database owner/schema * * @param {String} modelName The model name * @param {Object} options The options @@ -968,7 +994,58 @@ function fromDBName(dbName, camelCase) { } /** - * Discover one schema from the given model without following the relations + * Discover one schema from the given model without following the relations. +**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" + * } + * }, + * ... + * "fireModes": { + * "type": "String", + * "required": false, + * "length": 64, + * "oracle": { + * "columnName": "FIRE_MODES", + * "dataType": "VARCHAR2", + * "dataLength": 64, + * "nullable": "Y" + * } + * } + * } + * } + * ``` * * @param {String} modelName The model name * @param {Object} [options] The options @@ -1489,7 +1566,7 @@ DataSource.prototype.idNames = function (modelName) { /** * Define foreign key to another model * @param {String} className The model name that owns the key - * @param {String} key - name of key field + * @param {String} key Name of key field * @param {String} foreignClassName The foreign model name */ DataSource.prototype.defineForeignKey = function defineForeignKey(className, key, foreignClassName) { @@ -1528,7 +1605,7 @@ DataSource.prototype.defineForeignKey = function defineForeignKey(className, key /** * Close database connection - * @param {Fucntion} [cb] The callback function + * @param {Fucntion} cb The callback function. Optional. */ DataSource.prototype.disconnect = function disconnect(cb) { var self = this; @@ -1545,7 +1622,7 @@ DataSource.prototype.disconnect = function disconnect(cb) { }; /** - * Copy the model from Master + * Copy the model from Master. * @param {Function} Master The model constructor * @returns {Function} The copy of the model constructor * @@ -1625,7 +1702,8 @@ DataSource.prototype.transaction = function () { }; /** - * Enable a data source operation to be remote. + * Enable remote access to a data source operation. Each [connector](#connector) has its own set of set + * remotely enabled and disabled operations. To list the operations, call `dataSource.operations()`. * @param {String} operation The operation name */ @@ -1639,7 +1717,23 @@ DataSource.prototype.enableRemote = function (operation) { } /** - * Disable a data source operation to be remote. + * Disable remote access to a data source operation. Each [connector](#connector) has its own set of set enabled + * and disabled operations. To list the operations, call `dataSource.operations()`. + * + *```js + * + * var oracle = loopback.createDataSource({ + * connector: require('loopback-connector-oracle'), + * host: '...', + * ... + * }); + * 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. * @param {String} operation The operation name */ @@ -1671,7 +1765,27 @@ DataSource.prototype.getOperation = function (operation) { } /** - * Get all operations. + * Return JSON object describing all operations. + * + * Example return value: + * ```js + * { + * find: { + * remoteEnabled: true, + * accepts: [...], + * returns: [...] + * enabled: true + * }, + * save: { + * remoteEnabled: true, + * prototype: true, + * accepts: [...], + * returns: [...], + * enabled: true + * }, + * ... + * } + * ``` */ DataSource.prototype.operations = function () { return this._operations; @@ -1681,7 +1795,7 @@ DataSource.prototype.operations = function () { * Define an operation to the data source * @param {String} name The operation name * @param {Object} options The options - * @param [Function} fn The function + * @param {Function} fn The function */ DataSource.prototype.defineOperation = function (name, options, fn) { options.fn = fn; @@ -1698,10 +1812,10 @@ DataSource.prototype.isRelational = function () { }; /** - * Check if the data source is ready - * @param obj - * @param args - * @returns {boolean} + * Check if the data source is ready. + * Returns a Boolean value. + * @param obj {Object} ? + * @param args {Object} ? */ DataSource.prototype.ready = function (obj, args) { var self = this; diff --git a/lib/geo.js b/lib/geo.js index 84f7656f..30d6931d 100644 --- a/lib/geo.js +++ b/lib/geo.js @@ -1,7 +1,3 @@ -/** - * Dependencies. - */ - var assert = require('assert'); /*! @@ -80,12 +76,41 @@ exports.filter = function (arr, filter) { }); } -/** - * Export the `GeoPoint` class. - */ - exports.GeoPoint = GeoPoint; +/** + * The GeoPoint object represents a physical location. + * + * For example: + * + * ```js + * var here = new GeoPoint({lat: 10.32424, lng: 5.84978}); + * ``` + * + * Embed a latitude / longitude point in a 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, ...] + * }); + * ``` + * @class GeoPoint + * @param {Object} latlong Object with two Number properties: lat and long. + * @prop {Number} lat + * @prop {Number} lng + */ + function GeoPoint(data) { if (!(this instanceof GeoPoint)) { return new GeoPoint(data); @@ -118,7 +143,18 @@ function GeoPoint(data) { } /** - * Determine the spherical distance between two geo points. + * Determine the spherical distance between two GeoPoints. + * Specify units of measurement with the 'type' property in options object. Type can be: + * - `miles` (default) + * - `radians` + * - `kilometers` + * - `meters` + * - `miles` + * - `feet` + * - `degrees` + * @param {GeoPoint} pointA Point A + * @param {GeoPoint} pointB Point B + * @param {Object} options Options object; has one key, 'type', to specify the units of measurment (see above). Default is miles. */ GeoPoint.distanceBetween = function distanceBetween(a, b, options) { @@ -140,6 +176,14 @@ GeoPoint.distanceBetween = function distanceBetween(a, b, options) { /** * Determine the spherical distance to the given point. + * Example: + * ```js + * var here = new GeoPoint({lat: 10, lng: 10}); + * var there = new GeoPoint({lat: 5, lng: 5}); + * GeoPoint.distanceBetween(here, there, {type: 'miles'}) // 438 + * ``` + * @param {Object} point GeoPoint object to which to measure distance. + * @param {Object} options Use type key to specify units of measurment (default is miles). */ GeoPoint.prototype.distanceTo = function (point, options) { @@ -155,16 +199,19 @@ GeoPoint.prototype.toString = function () { } /** - * Si - */ + * @property {Number} PI - Ratio of a circle's circumference to its diameter. + * @property {Number} DEG2RAD - Factor to convert degrees to radians. + * @property {Number} RAD2DEG - Factor to convert radians to degrees. + * @property {Object} EARTH_RADIUS - Radius of the earth. +*/ // ratio of a circle's circumference to its diameter var PI = 3.1415926535897932384626433832795; -// factor to convert decimal degrees to radians +// factor to convert degrees to radians var DEG2RAD = 0.01745329252; -// factor to convert decimal degrees to radians +// factor to convert radians degrees to degrees var RAD2DEG = 57.29577951308; // radius of the earth diff --git a/lib/include.js b/lib/include.js index 4985ff2a..62926ccc 100644 --- a/lib/include.js +++ b/lib/include.js @@ -2,32 +2,42 @@ var utils = require('./utils'); var isPlainObject = utils.isPlainObject; var defineCachedRelations = utils.defineCachedRelations; -/** +/*! * Include mixin for ./model.js */ module.exports = Inclusion; +/** + * Inclusion - Model mixin. + * + * @class + */ + function Inclusion() { } /** - * Allows you to load relations of several objects and optimize numbers of requests. - * - * @param {Array} objects - array of instances - * @param {String}, {Object} or {Array} include - which relations you want to load. - * @param {Function} cb - Callback called when relations are loaded + * Enables you to load relations of several objects and optimize numbers of requests. * * Examples: * - * - User.include(users, 'posts', function() {}); will load all users posts with only one additional request. - * - User.include(users, ['posts'], function() {}); // same - * - User.include(users, ['posts', 'passports'], function() {}); // will load all users posts and passports with two - * additional requests. - * - Passport.include(passports, {owner: 'posts'}, function() {}); // will load all passports owner (users), and all - * posts of each owner loaded - * - Passport.include(passports, {owner: ['posts', 'passports']}); // ... - * - Passport.include(passports, {owner: [{posts: 'images'}, 'passports']}); // ... + * Load all users' posts with only one additional request: + * `User.include(users, 'posts', function() {});` + * Or + * `User.include(users, ['posts'], function() {});` * + * Load all users posts and passports with two additional requests: + * `User.include(users, ['posts', 'passports'], function() {});` + * + * Load all passports owner (users), and all posts of each owner loaded: + *```Passport.include(passports, {owner: 'posts'}, function() {}); + *``` Passport.include(passports, {owner: ['posts', 'passports']}); + *``` Passport.include(passports, {owner: [{posts: 'images'}, 'passports']}); + * + * @param {Array} objects Array of instances + * @param {String}, {Object} or {Array} include Which relations to load. + * @param {Function} cb Callback called when relations are loaded + * */ Inclusion.include = function (objects, include, cb) { var self = this; diff --git a/lib/model-builder.js b/lib/model-builder.js index 8b86e508..786978ae 100644 --- a/lib/model-builder.js +++ b/lib/model-builder.js @@ -16,7 +16,7 @@ require('./types')(ModelBuilder); var introspect = require('./introspection')(ModelBuilder); -/** +/*! * Export public API */ exports.ModelBuilder = exports.Schema = ModelBuilder; @@ -27,9 +27,9 @@ exports.ModelBuilder = exports.Schema = ModelBuilder; var slice = Array.prototype.slice; /** - * ModelBuilder - A builder to define data models + * ModelBuilder - A builder to define data models. * - * @constructor + * @class */ function ModelBuilder() { // create blank models pool @@ -57,11 +57,11 @@ function isModelClass(cls) { } /** - * Get a model by name + * Get a model by name. + * * @param {String} name The model name - * @param {Boolean} forceCreate Indicate if a stub should be created for the - * given name if a model doesn't exist - * @returns {*} The model class + * @param {Boolean} forceCreate Whether the create a stub for the given name if a model doesn't exist. + * Returns {*} The model class */ ModelBuilder.prototype.getModel = function (name, forceCreate) { var model = this.models[name]; @@ -81,17 +81,8 @@ ModelBuilder.prototype.getModelDefinition = function (name) { }; /** - * Define a model class - * - * @param {String} className - * @param {Object} properties - hash of class properties in format - * `{property: Type, property2: Type2, ...}` - * or - * `{property: {type: Type}, property2: {type: Type2}, ...}` - * @param {Object} settings - other configuration of class - * @return newly created class - * - * @example simple case + * Define a model class. + * Simple example: * ``` * var User = modelBuilder.define('User', { * email: String, @@ -100,7 +91,7 @@ ModelBuilder.prototype.getModelDefinition = function (name) { * activated: Boolean * }); * ``` - * @example more advanced case + * More advanced example: * ``` * var User = modelBuilder.define('User', { * email: { type: String, limit: 150, index: true }, @@ -110,6 +101,12 @@ ModelBuilder.prototype.getModelDefinition = function (name) { * activated: { type: Boolean, default: false } * }); * ``` + * + * @param {String} className Name of class + * @param {Object} properties Hash of class properties in format `{property: Type, property2: Type2, ...}` or `{property: {type: Type}, property2: {type: Type2}, ...}` + * @param {Object} settings Other configuration of class + * @return newly created class + * */ ModelBuilder.prototype.define = function defineClass(className, properties, settings, parent) { var modelBuilder = this; @@ -331,7 +328,7 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett /** * Register a property for the model class - * @param propertyName + * @param {String} propertyName Name of the property. */ ModelClass.registerProperty = function (propertyName) { var properties = modelDefinition.build(); @@ -426,9 +423,9 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett /** * Define single property named `propertyName` on `model` * - * @param {String} model - name of model - * @param {String} propertyName - name of property - * @param {Object} propertyDefinition - property settings + * @param {String} model Name of model + * @param {String} propertyName Name of property + * @param {Object} propertyDefinition Property settings */ ModelBuilder.prototype.defineProperty = function (model, propertyName, propertyDefinition) { this.definitions[model].defineProperty(propertyName, propertyDefinition); @@ -438,25 +435,25 @@ ModelBuilder.prototype.defineProperty = function (model, propertyName, propertyD /** * Extend existing model with bunch of properties * - * @param {String} model - name of model - * @param {Object} props - hash of properties - * * Example: - * - * // Instead of doing this: - * - * // amend the content model with competition attributes + * Instead of doing amending the content model with competition attributes like this: + * + * ```js * db.defineProperty('Content', 'competitionType', { type: String }); * db.defineProperty('Content', 'expiryDate', { type: Date, index: true }); * db.defineProperty('Content', 'isExpired', { type: Boolean, index: true }); - * - * // modelBuilder.extend allows to - * // extend the content model with competition attributes + *``` + * The extendModel() method enables you to extend the content model with competition attributes. + * ```js * db.extendModel('Content', { * competitionType: String, * expiryDate: { type: Date, index: true }, * isExpired: { type: Boolean, index: true } * }); + *``` + * + * @param {String} model Name of model + * @param {Object} props Hash of properties */ ModelBuilder.prototype.extendModel = function (model, props) { var t = this; @@ -523,9 +520,9 @@ ModelBuilder.prototype.getSchemaName = function (name) { }; /** - * Resolve the type string to be a function, for example, 'String' to String + * Resolve the type string to be a function, for example, 'String' to String. + * Returns {Function} if the type is resolved * @param {String} type The type string, such as 'number', 'Number', 'boolean', or 'String'. It's case insensitive - * @returns {Function} if the type is resolved */ ModelBuilder.prototype.resolveType = function (type) { if (!type) { @@ -620,10 +617,10 @@ ModelBuilder.prototype.buildModels = function (schemas) { }; /** - * Introspect the json document to build a corresponding model + * Introspect the JSON document to build a corresponding model. * @param {String} name The model name - * @param {Object} json The json object - * @param [Object} options The options + * @param {Object} json The JSON object + * @param {Object} options The options * @returns {} */ ModelBuilder.prototype.buildModelFromInstance = function (name, json, options) { diff --git a/lib/model.js b/lib/model.js index 258d1390..a587c826 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1,9 +1,9 @@ -/** +/*! * Module exports class Model */ module.exports = ModelBaseClass; -/** +/*! * Module dependencies */ @@ -17,15 +17,12 @@ var validations = require('./validations.js'); var BASE_TYPES = ['String', 'Boolean', 'Number', 'Date', 'Text']; /** - * Model class - base class for all persist objects - * provides **common API** to access any database connector. - * This class describes only abstract behavior layer, refer to `lib/connectors/*.js` - * to learn more about specific connector implementations + * Model class: base class for all persistent objects. * * `ModelBaseClass` mixes `Validatable` and `Hookable` classes methods * - * @constructor - * @param {Object} data - initial object data + * @class + * @param {Object} data Initial object data */ function ModelBaseClass(data, options) { options = options || {}; @@ -195,8 +192,9 @@ ModelBaseClass.prototype._initProperties = function (data, options) { }; /** - * @param {String} prop - property name - * @param {Object} params - various property configuration + * Define a property on the model. + * @param {String} prop Property name + * @param {Object} params Various property configuration */ ModelBaseClass.defineProperty = function (prop, params) { this.dataSource.defineProperty(this.modelName, prop, params); @@ -221,20 +219,17 @@ ModelBaseClass.prototype.getPropertyType = function (propName) { /** * Return string representation of class - * - * @override default toString method + * This overrides the default `toString()` method */ ModelBaseClass.toString = function () { return '[Model ' + this.modelName + ']'; }; /** - * Convert model instance to a plain JSON object + * Convert model instance to a plain JSON object. + * Returns a canonical object representation (no getters and setters). * - * @param {Boolean} onlySchema - restrict properties to dataSource only, - * default to false. When onlySchema is true, only properties defined in - * the schema are returned, otherwise all enumerable properties returned - * @returns {Object} - canonical object representation (no getters and setters) + * @param {Boolean} onlySchema Restrict properties to dataSource only. Default is false. If true, the function returns only properties defined in the schema; Otherwise it returns all enumerable properties. */ ModelBaseClass.prototype.toObject = function (onlySchema) { if(onlySchema === undefined) { @@ -303,7 +298,7 @@ ModelBaseClass.prototype.fromObject = function (obj) { /** * Checks is property changed based on current property and initial value * - * @param {String} propertyName - property name + * @param {String} propertyName Property name * @return Boolean */ ModelBaseClass.prototype.propertyChanged = function propertyChanged(propertyName) { @@ -311,10 +306,9 @@ ModelBaseClass.prototype.propertyChanged = function propertyChanged(propertyName }; /** - * Reset dirty attributes - * - * this method does not perform any database operation it just reset object to it's - * initial state + * Reset dirty attributes. + * This method does not perform any database operations; it just resets the object to its + * initial state. */ ModelBaseClass.prototype.reset = function () { var obj = this; diff --git a/lib/relations.js b/lib/relations.js index 5b799556..e8b44ec1 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -1,4 +1,4 @@ -/** +/*! * Dependencies */ var i8n = require('inflection'); @@ -7,6 +7,11 @@ var ModelBaseClass = require('./model.js'); module.exports = Relation; +/** + * Relations class + * + * @class Relation + */ function Relation() { } @@ -31,11 +36,12 @@ function lookupModel(models, modelName) { } /** - * Declare hasMany relation - * - * @param {Relation} anotherClass - class to has many - * @param {Object} params - configuration {as:, foreignKey:} - * @example `User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});` + * Declare "hasMany" relation. + * Example: + * ```User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});``` + * + * @param {Relation} anotherClass Class to has many + * @param {Object} params Configuration {as:, foreignKey:} */ Relation.hasMany = function hasMany(anotherClass, params) { var thisClassName = this.modelName; @@ -172,27 +178,34 @@ Relation.hasMany = function hasMany(anotherClass, params) { }; /** - * Declare belongsTo relation + * Declare "belongsTo" relation. * - * @param {Class} anotherClass - class to belong - * @param {Object} params - configuration {as: 'propertyName', foreignKey: 'keyName'} - * - * **Usage examples** - * Suppose model Post have a *belongsTo* relationship with User (the author of the post). You could declare it this way: + * **Examples** + * + * Suppose the model Post has a *belongsTo* relationship with User (the author of the post). You could declare it this way: + * ```js * Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}); + * ``` * * When a post is loaded, you can load the related author with: + * ```js * post.author(function(err, user) { * // the user variable is your user object * }); + * ``` * * The related object is cached, so if later you try to get again the author, no additional request will be made. * But there is an optional boolean parameter in first position that set whether or not you want to reload the cache: + * ```js * post.author(true, function(err, user) { * // The user is reloaded, even if it was already cached. * }); - * + * ``` * This optional parameter default value is false, so the related object will be loaded from cache if available. + * + * @param {Class} anotherClass Class to belong + * @param {Object} params Configuration {as: 'propertyName', foreignKey: 'keyName'} + * */ Relation.belongsTo = function (anotherClass, params) { params = params || {}; @@ -291,7 +304,13 @@ Relation.belongsTo = function (anotherClass, params) { /** * Many-to-many relation * - * Post.hasAndBelongsToMany('tags'); creates connection model 'PostTag' + * For example, this creates connection model 'PostTag': + * ```js + * Post.hasAndBelongsToMany('tags'); + * ``` + * @param {String} anotherClass + * @param {Object} params + * */ Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params) { params = params || {}; diff --git a/lib/sql.js b/lib/sql.js index 3f32882b..701c8dc0 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -5,7 +5,7 @@ module.exports = BaseSQL; /** * Base class for connectors that are backed by relational databases/SQL - * @constructor + * @class */ function BaseSQL() { Connector.apply(this, [].slice.call(arguments)); @@ -21,7 +21,7 @@ BaseSQL.prototype.relational = true; /** * Get types associated with the connector - * @returns {String[]} The types for the connector + * Returns {String[]} The types for the connector */ BaseSQL.prototype.getTypes = function() { return ['db', 'rdbms', 'sql']; @@ -29,7 +29,7 @@ BaseSQL.prototype.relational = true; /*! * Get the default data type for ID - * @returns {Function} + * Returns {Function} */ BaseSQL.prototype.getDefaultIdType = function() { return Number; @@ -51,9 +51,9 @@ BaseSQL.prototype.queryOne = function (sql, callback) { }; /** - * Get the table name for a given model + * Get the table name for a given model. + * Returns the table name (String). * @param {String} model The model name - * @returns {String} The table name */ BaseSQL.prototype.table = function (model) { var name = this.getDataSource(model).tableName(model); diff --git a/lib/validations.js b/lib/validations.js index c7a43157..9582c2c3 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -1,15 +1,15 @@ var util = require('util'); -/** +/*! * Module exports */ exports.ValidationError = ValidationError; +exports.Validatable = Validatable; /** - * Validation mixins for model.js + * Validation mixins for LoopBack models. * - * Basically validation configurators is just class methods, which adds validations - * configs to AbstractClass._validations. Each of this validations run when - * `obj.isValid()` method called. + * This class provides methods that add validation cababilities to models. + * Each of this validations run when `obj.isValid()` method called. * * Each configurator can accept n params (n-1 field names and one config). Config * is {Object} depends on specific validation, but all of them has one common part: @@ -18,9 +18,8 @@ exports.ValidationError = ValidationError; * * In more complicated cases it can be {Hash} of messages (for each case): * `User.validatesLengthOf('password', { min: 6, max: 20, message: {min: 'too short', max: 'too long'}});` + * @class Validatable */ -exports.Validatable = Validatable; - function Validatable() { } @@ -29,19 +28,16 @@ function Validatable() { * * Default error message "can't be blank" * - * @example presence of title + * For example, validate presence of title * ``` * Post.validatesPresenceOf('title'); * ``` - * @example with custom message + *Example with custom message * ``` - * Post.validatesPresenceOf('title', {message: 'Can not be blank'}); + * Post.validatesPresenceOf('title', {message: 'Cannot be blank'}); * ``` * - * @sync - * - * @nocode - * @see helper/validatePresence + * @param */ Validatable.validatesPresenceOf = getConfigurator('presence'); @@ -54,14 +50,14 @@ Validatable.validatesPresenceOf = getConfigurator('presence'); * - max: too long * - is: length is wrong * - * @example length validations + * Example: length validations * ``` * User.validatesLengthOf('password', {min: 7}); * User.validatesLengthOf('email', {max: 100}); * User.validatesLengthOf('state', {is: 2}); * User.validatesLengthOf('nick', {min: 3, max: 15}); * ``` - * @example length validations with custom error messages + * Example: length validations with custom error messages * ``` * User.validatesLengthOf('password', {min: 7, message: {min: 'too weak'}}); * User.validatesLengthOf('state', {is: 2, message: {is: 'is not valid state name'}}); @@ -76,7 +72,7 @@ Validatable.validatesLengthOf = getConfigurator('length'); /** * Validate numericality. * - * @example + * Example * ``` * User.validatesNumericalityOf('age', { message: { number: '...' }}); * User.validatesNumericalityOf('age', {int: true, message: { int: '...' }}); @@ -96,7 +92,7 @@ Validatable.validatesNumericalityOf = getConfigurator('numericality'); /** * Validate inclusion in set * - * @example + * Example: * ``` * User.validatesInclusionOf('gender', {in: ['male', 'female']}); * User.validatesInclusionOf('role', { @@ -115,7 +111,7 @@ Validatable.validatesInclusionOf = getConfigurator('inclusion'); /** * Validate exclusion * - * @example `Company.validatesExclusionOf('domain', {in: ['www', 'admin']});` + * Example: `Company.validatesExclusionOf('domain', {in: ['www', 'admin']});` * * Default error message: is reserved * @@ -332,7 +328,7 @@ function getConfigurator(name, opts) { * @param {Function} callback called with (valid) * @return {Boolean} true if no async validation configured and all passed * - * @example ExpressJS controller: render user if valid, show flash otherwise + * Example: ExpressJS controller: render user if valid, show flash otherwise * ``` * user.isValid(function (valid) { * if (valid) res.render({user: user}); From 4c8682b1c2ecfe64ff8f3d7dbc4166cad62f5d9c Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Wed, 12 Mar 2014 16:33:07 -0700 Subject: [PATCH 2/8] Updates for JSDoc changes for API doc. --- docs.json | 45 +++------------------------------------------ 1 file changed, 3 insertions(+), 42 deletions(-) diff --git a/docs.json b/docs.json index f1d6e98d..8864fc54 100644 --- a/docs.json +++ b/docs.json @@ -1,51 +1,13 @@ { "content": [ - { - "title": "LoopBack DataSource API", - "depth": 2 - }, - { - "title": "Model builder", - "depth": 3 - }, - - "lib/model-builder.js", - { - "title": "Types", - "depth": 3 - }, - "lib/types.js", - { - "title": "GeoPoint", - "depth": 3 - }, - "lib/geo.js", - { - "title": "Model", - "depth": 3 - }, - "lib/model.js", - - { - "title": "DataSource", - "depth": 3 - }, "lib/datasource.js", - - { - "title": "Data access mixins", - "depth": 3 - }, + "lib/geo.js", "lib/dao.js", - "lib/hooks.js", + "lib/model.js", + "lib/model-builder.js", "lib/include.js", "lib/relations.js", "lib/validations.js", - - { - "title": "Base class for SQL connectors", - "depth": 3 - }, "lib/sql.js" ], "codeSectionDepth": 4, @@ -54,4 +16,3 @@ "/docs": "/docs" } } - From aa11aad2987ec81f89eb4b21ec74aa32a5557661 Mon Sep 17 00:00:00 2001 From: crandmck Date: Thu, 13 Mar 2014 16:26:29 -0700 Subject: [PATCH 3/8] Fix some small errors --- lib/validations.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/validations.js b/lib/validations.js index 9582c2c3..ab2c85ba 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -37,7 +37,6 @@ function Validatable() { * Post.validatesPresenceOf('title', {message: 'Cannot be blank'}); * ``` * - * @param */ Validatable.validatesPresenceOf = getConfigurator('presence'); @@ -62,10 +61,6 @@ Validatable.validatesPresenceOf = getConfigurator('presence'); * User.validatesLengthOf('password', {min: 7, message: {min: 'too weak'}}); * User.validatesLengthOf('state', {is: 2, message: {is: 'is not valid state name'}}); * ``` - * - * @sync - * @nocode - * @see helper/validateLength */ Validatable.validatesLengthOf = getConfigurator('length'); @@ -157,7 +152,7 @@ Validatable.validate = getConfigurator('custom'); * Default error message: is invalid * * Example: - * + *```js * User.validateAsync('name', customValidator, {message: 'Bad name'}); * function customValidator(err, done) { * process.nextTick(function () { @@ -175,7 +170,7 @@ Validatable.validate = getConfigurator('custom'); * user.isValid(function (isValid) { * isValid; // false * }) - * + *``` * @async * @nocode * @see helper/validateCustom @@ -325,8 +320,7 @@ function getConfigurator(name, opts) { * @warning This method can be called as sync only when no async validation * configured. It's strongly recommended to run all validations as asyncronous. * - * @param {Function} callback called with (valid) - * @return {Boolean} true if no async validation configured and all passed + * Returns true if no async validation configured and all passed * * Example: ExpressJS controller: render user if valid, show flash otherwise * ``` @@ -335,6 +329,7 @@ function getConfigurator(name, opts) { * else res.flash('error', 'User is not valid'), console.log(user.errors), res.redirect('/users'); * }); * ``` + * @param {Function} callback called with (valid) */ Validatable.prototype.isValid = function (callback, data) { var valid = true, inst = this, wait = 0, async = false; @@ -531,7 +526,7 @@ function nullCheck(attr, conf, err) { * otherwise returns false * * @param {Mix} v - * @returns {Boolean} whether `v` blank or not + * Returns true if `v` is blank. */ function blank(v) { if (typeof v === 'undefined') return true; From bef90bd529b52149d03b140d06e19ce00d4e68cc Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 11 Mar 2014 15:37:28 -0700 Subject: [PATCH 4/8] Refactor the serialize/deserialize into two functions --- lib/connectors/memory.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index 1184ac44..0db0dc32 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -51,6 +51,24 @@ Memory.prototype.connect = function (callback) { } }; +function serialize(obj) { + if(obj === null || obj === undefined) { + return obj; + } + return JSON.stringify(obj); +} + +function deserialize(dbObj) { + if(dbObj === null || dbObj === undefined) { + return dbObj; + } + if(typeof dbObj === 'string') { + return JSON.parse(dbObj); + } else { + return dbObj; + } +} + Memory.prototype.loadFromFile = function(callback) { var self = this; if (self.settings.file) { @@ -142,7 +160,7 @@ Memory.prototype.create = function create(model, data, callback) { if(!this.cache[model]) { this.cache[model] = {}; } - this.cache[model][id] = JSON.stringify(data); + this.cache[model][id] = serialize(data); this.saveToFile(id, callback); }; @@ -161,7 +179,7 @@ Memory.prototype.updateOrCreate = function (model, data, callback) { }; Memory.prototype.save = function save(model, data, callback) { - this.cache[model][this.getIdValue(model, data)] = JSON.stringify(data); + this.cache[model][this.getIdValue(model, data)] = serialize(data); this.saveToFile(data, callback); }; @@ -185,7 +203,7 @@ Memory.prototype.destroy = function destroy(model, id, callback) { Memory.prototype.fromDb = function (model, data) { if (!data) return null; - data = JSON.parse(data); + data = deserialize(data); var props = this._models[model].properties; for (var key in data) { var val = data[key]; @@ -374,8 +392,8 @@ Memory.prototype.updateAttributes = function updateAttributes(model, id, data, c this.setIdValue(model, data, id); var cachedModels = this.cache[model]; - var modelAsString = cachedModels && this.cache[model][id]; - var modelData = modelAsString && JSON.parse(modelAsString); + var modelData = cachedModels && this.cache[model][id]; + modelData = modelData && deserialize(modelData); if (modelData) { this.save(model, merge(modelData, data), cb); From 1dc0c3425238e50ff326d66b8f3c98552fc39f6a Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 12 Mar 2014 22:11:55 -0700 Subject: [PATCH 5/8] Fix the connector resolver to make sure known connectors are used --- lib/datasource.js | 7 ++++++- test/loopback-dl.test.js | 23 +++++++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/datasource.js b/lib/datasource.js index 32a0ee4b..dc7461ca 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -178,13 +178,18 @@ DataSource.prototype._setupConnector = function () { // List possible connector module names function connectorModuleNames(name) { - var names = [name]; // Check the name as is + var names = []; // Check the name as is if (!name.match(/^\//)) { names.push('./connectors/' + name); // Check built-in connectors if (name.indexOf('loopback-connector-') !== 0) { names.push('loopback-connector-' + name); // Try loopback-connector- } } + // Only try the short name if the connector is not from StrongLoop + if(['mongodb', 'oracle', 'mysql', 'postgresql', 'mssql', 'rest', 'soap'] + .indexOf(name) === -1) { + names.push(name); + } return names; } diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index 4ac1f91b..01cc0114 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -615,9 +615,17 @@ describe('DataSource connector types', function() { describe('DataSource constructor', function () { // Mocked require var loader = function (name) { + if (name.indexOf('./connectors/') !== -1) { + // ./connectors/ doesn't exist + return null; + } + if (name === 'loopback-connector-abc') { + // Assume loopback-connector-abc doesn't exist + return null; + } return { name: name - } + }; }; it('should resolve connector by path', function () { @@ -632,9 +640,20 @@ describe('DataSource constructor', function () { var connector = DataSource._resolveConnector('loopback-connector-xyz', loader); assert(connector.connector); }); - it('should try to resolve connector by short module name', function () { + it('should try to resolve connector by short module name with full name first', function () { var connector = DataSource._resolveConnector('xyz', loader); assert(connector.connector); + assert.equal(connector.connector.name, 'loopback-connector-xyz'); + }); + it('should try to resolve connector by short module name', function () { + var connector = DataSource._resolveConnector('abc', loader); + assert(connector.connector); + assert.equal(connector.connector.name, 'abc'); + }); + it('should try to resolve connector by short module name for known connectors', function () { + var connector = DataSource._resolveConnector('oracle', loader); + assert(connector.connector); + assert.equal(connector.connector.name, 'loopback-connector-oracle'); }); it('should try to resolve connector by full module name', function () { var connector = DataSource._resolveConnector('loopback-xyz', loader); From cadacc44bbfaba9439b50dadc351a45252d84e42 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 11 Mar 2014 15:38:07 -0700 Subject: [PATCH 6/8] Create scoped methods for belongsTo and improve docs --- examples/relations.js | 53 +++++++++++++++++++--- lib/dao.js | 98 ++++++++++++++++++++++------------------ lib/relations.js | 103 +++++++++++++++++++++++++++++++++++------- lib/scope.js | 9 ++++ 4 files changed, 196 insertions(+), 67 deletions(-) diff --git a/examples/relations.js b/examples/relations.js index 04973f90..e83a66f2 100644 --- a/examples/relations.js +++ b/examples/relations.js @@ -3,6 +3,7 @@ var ds = new DataSource('memory'); var Order = ds.createModel('Order', { customerId: Number, + items: [String], orderDate: Date }); @@ -13,7 +14,7 @@ var Customer = ds.createModel('Customer', { Order.belongsTo(Customer); Customer.create({name: 'John'}, function (err, customer) { - Order.create({customerId: customer.id, orderDate: new Date()}, function (err, order) { + Order.create({customerId: customer.id, orderDate: new Date(), items: ['Book']}, function (err, order) { order.customer(console.log); order.customer(true, console.log); @@ -22,6 +23,16 @@ Customer.create({name: 'John'}, function (err, customer) { order.customer(console.log); }); }); + + Order.create({orderDate: new Date(), items: ['Phone']}, function (err, order2) { + + order2.customer.create({name: 'Smith'}, function(err, customer2) { + console.log(order2, customer2); + }); + + var customer3 = order2.customer.build({name: 'Tom'}); + console.log('Customer 3', customer3); + }); }); Customer.hasMany(Order, {as: 'orders', foreignKey: 'customerId'}); @@ -60,13 +71,32 @@ Appointment.belongsTo(Physician); Physician.hasMany(Patient, {through: Appointment}); Patient.hasMany(Physician, {through: Appointment}); -Physician.create({name: 'Smith'}, function (err, physician) { - Patient.create({name: 'Mary'}, function (err, patient) { - Appointment.create({appointmentDate: new Date(), physicianId: physician.id, patientId: patient.id}, - function (err, appt) { - physician.patients(console.log); - patient.physicians(console.log); +Physician.create({name: 'Dr John'}, function (err, physician1) { + Physician.create({name: 'Dr Smith'}, function (err, physician2) { + Patient.create({name: 'Mary'}, function (err, patient1) { + Patient.create({name: 'Ben'}, function (err, patient2) { + Appointment.create({appointmentDate: new Date(), physicianId: physician1.id, patientId: patient1.id}, + function (err, appt1) { + Appointment.create({appointmentDate: new Date(), physicianId: physician1.id, patientId: patient2.id}, + function (err, appt2) { + physician1.patients(console.log); + physician1.patients({where: {name: 'Mary'}}, console.log); + patient1.physicians(console.log); + + // Build an appointment? + var patient3 = patient1.physicians.build({name: 'Dr X'}); + console.log('Physician 3: ', patient3, patient3.constructor.modelName); + + // Create a physician? + patient1.physicians.create({name: 'Dr X'}, function(err, patient4) { + console.log('Physician 4: ', patient4, patient4.constructor.modelName); + }); + + + }); + }); }); + }); }); }); @@ -85,6 +115,15 @@ Assembly.create({name: 'car'}, function (err, assembly) { Part.create({partNumber: 'engine'}, function (err, part) { assembly.parts.add(part, function (err) { assembly.parts(console.log); + + // Build an part? + var part3 = assembly.parts.build({partNumber: 'door'}); + console.log('Part3: ', part3, part3.constructor.modelName); + + // Create a part? + assembly.parts.create({name: 'door'}, function(err, part4) { + console.log('Part4: ', part4, part4.constructor.modelName); + }); }); }); diff --git a/lib/dao.js b/lib/dao.js index 2df44e62..61abe10b 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -458,8 +458,10 @@ DataAccessObject._coerce = function (where) { /** * Find all instances of Model, matched by query - * make sure you have marked as `index: true` fields for filter or sort. - * The params object: + * make sure you have marked as `index: true` fields for filter or sort + * + * @param {Object} [query] the query object + * * - where: Object `{ key: val, key2: {gt: 'val2'}}` * - include: String, Object or Array. See `DataAccessObject.include()`. * - order: String @@ -470,36 +472,36 @@ DataAccessObject._coerce = function (where) { * @param {Function} callback (required) called with two arguments: err (null or Error), array of instances */ -DataAccessObject.find = function find(params, cb) { +DataAccessObject.find = function find(query, cb) { if (stillConnecting(this.getDataSource(), this, arguments)) return; if (arguments.length === 1) { - cb = params; - params = null; + cb = query; + query = null; } var constr = this; - params = params || {}; + query = query || {}; - if (params.where) { - params.where = this._coerce(params.where); + if (query.where) { + query.where = this._coerce(query.where); } - var fields = params.fields; - var near = params && geo.nearFilter(params.where); + var fields = query.fields; + var near = query && geo.nearFilter(query.where); var supportsGeo = !!this.getDataSource().connector.buildNearFilter; // normalize fields as array of included property names if (fields) { - params.fields = fieldsToArray(fields, Object.keys(this.definition.properties)); + query.fields = fieldsToArray(fields, Object.keys(this.definition.properties)); } - params = removeUndefined(params); + query = removeUndefined(query); if (near) { if (supportsGeo) { // convert it - this.getDataSource().connector.buildNearFilter(params, near); - } else if (params.where) { + this.getDataSource().connector.buildNearFilter(query, near); + } else if (query.where) { // do in memory query // using all documents this.getDataSource().connector.all(this.modelName, {}, function (err, data) { @@ -521,7 +523,7 @@ DataAccessObject.find = function find(params, cb) { }); }); - memory.all(modelName, params, cb); + memory.all(modelName, query, cb); } else { cb(null, []); } @@ -532,24 +534,24 @@ DataAccessObject.find = function find(params, cb) { } } - this.getDataSource().connector.all(this.modelName, params, function (err, data) { + this.getDataSource().connector.all(this.modelName, query, function (err, data) { if (data && data.forEach) { data.forEach(function (d, i) { var obj = new constr(); - obj._initProperties(d, {fields: params.fields}); + obj._initProperties(d, {fields: query.fields}); - if (params && params.include) { - if (params.collect) { + if (query && query.include) { + if (query.collect) { // The collect property indicates that the query is to return the // standlone items for a related model, not as child of the parent object // For example, article.tags - obj = obj.__cachedRelations[params.collect]; + obj = obj.__cachedRelations[query.collect]; } else { // This handles the case to return parent items including the related // models. For example, Article.find({include: 'tags'}, ...); // Try to normalize the include - var includes = params.include || []; + var includes = query.include || []; if (typeof includes === 'string') { includes = [includes]; } else if (!Array.isArray(includes) && typeof includes === 'object') { @@ -594,19 +596,19 @@ setRemoting(DataAccessObject.find, { /** * Find one record, same as `all`, limited by 1 and return object, not collection * - * @param {Object} params Search conditions: {where: {test: 'me'}} - * @param {Function} cb Callback called with (err, instance) + * @param {Object} query - search conditions: {where: {test: 'me'}} + * @param {Function} cb - callback called with (err, instance) */ -DataAccessObject.findOne = function findOne(params, cb) { +DataAccessObject.findOne = function findOne(query, cb) { if (stillConnecting(this.getDataSource(), this, arguments)) return; - if (typeof params === 'function') { - cb = params; - params = {}; + if (typeof query === 'function') { + cb = query; + query = {}; } - params = params || {}; - params.limit = 1; - this.find(params, function (err, collection) { + query = query || {}; + query.limit = 1; + this.find(query, function (err, collection) { if (err || !collection || !collection.length > 0) return cb(err, null); cb(err, collection[0]); }); @@ -844,7 +846,7 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb if (stillConnecting(this.getDataSource(), this, arguments)) return; var inst = this; - var Model = this.constructor + var Model = this.constructor; var model = Model.modelName; if (typeof data === 'function') { @@ -911,19 +913,15 @@ setRemoting(DataAccessObject.prototype.updateAttributes, { * @param {Function} callback Called with (err, instance) arguments */ DataAccessObject.prototype.reload = function reload(callback) { - if (stillConnecting(this.getDataSource(), this, arguments)) return; + if (stillConnecting(this.getDataSource(), this, arguments)) { + return; + } this.constructor.findById(getIdValue(this.constructor, this), callback); }; -/* - setRemoting(DataAccessObject.prototype.reload, { - description: 'Reload a model instance from the data source', - returns: {arg: 'data', type: 'object', root: true} - }); - */ -/** +/*! * Define readonly property on object * * @param {Object} obj @@ -941,13 +939,25 @@ function defineReadonlyProp(obj, key, value) { var defineScope = require('./scope.js').defineScope; -/*! - * Define scope. N.B. Not clear if this needs to be exposed in API doc. +/** + * Define a scope for the model class. Scopes enable you to specify commonly-used + * queries that you can reference as method calls on a model. + * + * @param {String} name The scope name + * @param {Object} query The query object for DataAccessObject.find() + * @param {ModelClass} [targetClass] The model class for the query, default to + * the declaring model */ -DataAccessObject.scope = function (name, filter, targetClass) { - defineScope(this, targetClass || this, name, filter); +DataAccessObject.scope = function (name, query, targetClass) { + defineScope(this, targetClass || this, name, query); }; -// jutil.mixin(DataAccessObject, validations.Validatable); +/*! + * Add 'include' + */ jutil.mixin(DataAccessObject, Inclusion); + +/*! + * Add 'relation' + */ jutil.mixin(DataAccessObject, Relation); diff --git a/lib/relations.js b/lib/relations.js index e8b44ec1..fad2d858 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -15,6 +15,11 @@ module.exports = Relation; function Relation() { } +/** + * Find the relation by foreign key + * @param {*} foreignKey The foreign key + * @returns {Object} The relation object + */ Relation.relationNameFor = function relationNameFor(foreignKey) { for (var rel in this.relations) { if (this.relations[rel].type === 'belongsTo' && this.relations[rel].keyFrom === foreignKey) { @@ -23,6 +28,12 @@ Relation.relationNameFor = function relationNameFor(foreignKey) { } }; +/*! + * Look up a model by name from the list of given models + * @param {Object} models Models keyed by name + * @param {String} modelName The model name + * @returns {*} The matching model class + */ function lookupModel(models, modelName) { if(models[modelName]) { return models[modelName]; @@ -71,11 +82,14 @@ Relation.hasMany = function hasMany(anotherClass, params) { // pluralize(anotherClass.modelName) // which is actually just anotherClass.find({where: {thisModelNameId: this[idName]}}, cb); var scopeMethods = { - findById: find, - destroy: destroy + findById: findById, + destroy: destroyById }; if (params.through) { var fk2 = i8n.camelize(anotherClass.modelName + '_id', true); + + // Create an instance of the target model and connect it to the instance of + // the source model by creating an instance of the through model scopeMethods.create = function create(data, done) { if (typeof data !== 'object') { done = data; @@ -86,13 +100,16 @@ Relation.hasMany = function hasMany(anotherClass, params) { }; } var self = this; + // First create the target model anotherClass.create(data, function (err, ac) { if (err) return done(err, ac); var d = {}; d[params.through.relationNameFor(fk)] = self; d[params.through.relationNameFor(fk2)] = ac; + // Then create the through model params.through.create(d, function (e) { if (e) { + // Undo creation of the target model ac.destroy(function () { done(e); }); @@ -102,6 +119,11 @@ Relation.hasMany = function hasMany(anotherClass, params) { }); }); }; + + /** + * Add the target model instance to the 'hasMany' relation + * @param {Object|ID) acInst The actual instance or id value + */ scopeMethods.add = function (acInst, done) { var data = {}; var query = {}; @@ -109,8 +131,14 @@ Relation.hasMany = function hasMany(anotherClass, params) { data[params.through.relationNameFor(fk)] = this; query[fk2] = acInst[idName] || acInst; data[params.through.relationNameFor(fk2)] = acInst; + // Create an instance of the through model params.through.findOrCreate({where: query}, data, done); }; + + /** + * Remove the target model instance from the 'hasMany' relation + * @param {Object|ID) acInst The actual instance or id value + */ scopeMethods.remove = function (acInst, done) { var q = {}; q[fk2] = acInst[idName] || acInst; @@ -124,8 +152,12 @@ Relation.hasMany = function hasMany(anotherClass, params) { d.destroy(done); }); }; + + // No destroy method will be injected delete scopeMethods.destroy; } + + // Mix the property and scoped methods into the prototype class defineScope(this.prototype, params.through || anotherClass, methodName, function () { var filter = {}; filter.where = {}; @@ -142,7 +174,8 @@ Relation.hasMany = function hasMany(anotherClass, params) { anotherClass.dataSource.defineForeignKey(anotherClass.modelName, fk, this.modelName); } - function find(id, cb) { + // Find the target model instance by id + function findById(id, cb) { anotherClass.findById(id, function (err, inst) { if (err) { return cb(err); @@ -150,6 +183,7 @@ Relation.hasMany = function hasMany(anotherClass, params) { if (!inst) { return cb(new Error('Not found')); } + // Check if the foreign key matches the primary key if (inst[fk] && inst[fk].toString() === this[idName].toString()) { cb(null, inst); } else { @@ -158,7 +192,8 @@ Relation.hasMany = function hasMany(anotherClass, params) { }.bind(this)); } - function destroy(id, cb) { + // Destroy the target model instance by id + function destroyById(id, cb) { var self = this; anotherClass.findById(id, function (err, inst) { if (err) { @@ -167,6 +202,7 @@ Relation.hasMany = function hasMany(anotherClass, params) { if (!inst) { return cb(new Error('Not found')); } + // Check if the foreign key matches the primary key if (inst[fk] && inst[fk].toString() === self[idName].toString()) { inst.destroy(cb); } else { @@ -234,6 +270,8 @@ Relation.belongsTo = function (anotherClass, params) { this.dataSource.defineForeignKey(this.modelName, fk, anotherClass.modelName); this.prototype.__finders__ = this.prototype.__finders__ || {}; + // Set up a finder to find by id and make sure the foreign key of the declaring + // model matches the primary key of the target model this.prototype.__finders__[methodName] = function (id, cb) { if (id === null) { cb(null, null); @@ -246,6 +284,7 @@ Relation.belongsTo = function (anotherClass, params) { if (!inst) { return cb(null, null); } + // Check if the foreign key matches the primary key if (inst[idName] === this[fk]) { cb(null, inst); } else { @@ -254,7 +293,12 @@ Relation.belongsTo = function (anotherClass, params) { }.bind(this)); }; - this.prototype[methodName] = function (refresh, p) { + // Define the method for the belongsTo relation itself + // It will support one of the following styles: + // - order.customer(refresh, callback): Load the target model instance asynchronously + // - order.customer(customer): Synchronous setter of the target model instance + // - order.customer(): Synchronous getter of the target model instance + var relationMethod = function (refresh, p) { if (arguments.length === 1) { p = refresh; refresh = false; @@ -290,14 +334,41 @@ Relation.belongsTo = function (anotherClass, params) { } }; - // Set the remoting metadata so that it can be accessed as /api/// - // For example, /api/orders/1/customer - var fn = this.prototype[methodName]; - fn.shared = true; - fn.http = {verb: 'get', path: '/' + methodName}; - fn.accepts = {arg: 'refresh', type: 'boolean', http: {source: 'query'}}; - fn.description = 'Fetches belongsTo relation ' + methodName; - fn.returns = {arg: methodName, type: 'object', root: true}; + // Define a property for the scope so that we have 'this' for the scoped methods + Object.defineProperty(this.prototype, methodName, { + enumerable: false, + configurable: true, + get: function () { + var fn = relationMethod.bind(this); + // Set the remoting metadata so that it can be accessed as /api/// + // For example, /api/orders/1/customer + fn.shared = true; + fn.http = {verb: 'get', path: '/' + methodName}; + fn.accepts = {arg: 'refresh', type: 'boolean', http: {source: 'query'}}; + fn.description = 'Fetches belongsTo relation ' + methodName; + fn.returns = {arg: methodName, type: 'object', root: true}; + + // Create an instance of the target model and set the foreign key of the + // declaring model instance to the id of the target instance + fn.create = function(targetModelData, cb) { + var self = this; + anotherClass.create(targetModelData, function(err, targetModel) { + if(!err) { + self[fk] = targetModel[idName]; + cb && cb(err, targetModel); + } else { + cb && cb(err); + } + }); + }.bind(this); + + // Build an instance of the target model + fn.build = function(targetModelData) { + return new anotherClass(targetModelData); + }.bind(this); + + return fn; + }}); }; @@ -308,9 +379,9 @@ Relation.belongsTo = function (anotherClass, params) { * ```js * Post.hasAndBelongsToMany('tags'); * ``` - * @param {String} anotherClass - * @param {Object} params - * + * @param {String|Function} anotherClass - target class to hasAndBelongsToMany or name of + * the relation + * @param {Object} params - configuration {as: String, foreignKey: *, model: ModelClass} */ Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params) { params = params || {}; diff --git a/lib/scope.js b/lib/scope.js index 5c8d43c5..68462b79 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -5,6 +5,15 @@ var defineCachedRelations = utils.defineCachedRelations; */ exports.defineScope = defineScope; +/** + * Define a scope to the class + * @param {Model} cls The class where the scope method is added + * @param {Model} targetClass The class that a query to run against + * @param {String} name The name of the scope + * @param {Object|Function} params The parameters object for the query or a function + * to return the query object + * @param methods An object of methods keyed by the method name to be bound to the class + */ function defineScope(cls, targetClass, name, params, methods) { // collect meta info about scope From cc5975486d6ea0657495087c591cadcee00c87e3 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 13 Mar 2014 16:43:38 -0700 Subject: [PATCH 7/8] Simplify the inclusion processing --- examples/relations.js | 30 +++++--- lib/dao.js | 1 - lib/include.js | 155 +++++++++++++++++------------------------- lib/model.js | 5 +- lib/relations.js | 4 ++ lib/scope.js | 9 ++- lib/sql.js | 5 +- test/include.test.js | 57 ++++++++++++++-- 8 files changed, 150 insertions(+), 116 deletions(-) diff --git a/examples/relations.js b/examples/relations.js index e83a66f2..ed3474cc 100644 --- a/examples/relations.js +++ b/examples/relations.js @@ -2,7 +2,6 @@ var DataSource = require('../index').DataSource; var ds = new DataSource('memory'); var Order = ds.createModel('Order', { - customerId: Number, items: [String], orderDate: Date }); @@ -13,8 +12,11 @@ var Customer = ds.createModel('Customer', { Order.belongsTo(Customer); +var order1, order2, order3; + Customer.create({name: 'John'}, function (err, customer) { Order.create({customerId: customer.id, orderDate: new Date(), items: ['Book']}, function (err, order) { + order1 = order; order.customer(console.log); order.customer(true, console.log); @@ -24,13 +26,16 @@ Customer.create({name: 'John'}, function (err, customer) { }); }); - Order.create({orderDate: new Date(), items: ['Phone']}, function (err, order2) { + Order.create({orderDate: new Date(), items: ['Phone']}, function (err, order) { - order2.customer.create({name: 'Smith'}, function(err, customer2) { - console.log(order2, customer2); + order.customer.create({name: 'Smith'}, function(err, customer2) { + console.log(order, customer2); + order.save(function(err, order) { + order2 = order; + }); }); - var customer3 = order2.customer.build({name: 'Tom'}); + var customer3 = order.customer.build({name: 'Tom'}); console.log('Customer 3', customer3); }); }); @@ -39,14 +44,15 @@ Customer.hasMany(Order, {as: 'orders', foreignKey: 'customerId'}); Customer.create({name: 'Ray'}, function (err, customer) { Order.create({customerId: customer.id, orderDate: new Date()}, function (err, order) { + order3 = order; customer.orders(console.log); customer.orders.create({orderDate: new Date()}, function (err, order) { console.log(order); Customer.include([customer], 'orders', function (err, results) { console.log('Results: ', results); }); - customer.orders.findById('2', console.log); - customer.orders.destroy('2', console.log); + customer.orders.findById(order3.id, console.log); + customer.orders.destroy(order3.id, console.log); }); }); }); @@ -114,15 +120,21 @@ Part.hasAndBelongsToMany(Assembly); Assembly.create({name: 'car'}, function (err, assembly) { Part.create({partNumber: 'engine'}, function (err, part) { assembly.parts.add(part, function (err) { - assembly.parts(console.log); + assembly.parts(function(err, parts) { + console.log('Parts: ', parts); + }); // Build an part? var part3 = assembly.parts.build({partNumber: 'door'}); console.log('Part3: ', part3, part3.constructor.modelName); // Create a part? - assembly.parts.create({name: 'door'}, function(err, part4) { + assembly.parts.create({partNumber: 'door'}, function(err, part4) { console.log('Part4: ', part4, part4.constructor.modelName); + + Assembly.find({include: 'parts'}, function(err, assemblies) { + console.log('Assemblies: ', assemblies); + }); }); }); diff --git a/lib/dao.js b/lib/dao.js index 61abe10b..0eb7da46 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -732,7 +732,6 @@ DataAccessObject.prototype.save = function (options, callback) { var inst = this; var data = inst.toObject(true); - var Model = this.constructor; var modelName = Model.modelName; if (!getIdValue(Model, this)) { diff --git a/lib/include.js b/lib/include.js index 62926ccc..5a0bb062 100644 --- a/lib/include.js +++ b/lib/include.js @@ -1,3 +1,4 @@ +var async = require('async'); var utils = require('./utils'); var isPlainObject = utils.isPlainObject; var defineCachedRelations = utils.defineCachedRelations; @@ -42,52 +43,46 @@ function Inclusion() { Inclusion.include = function (objects, include, cb) { var self = this; - if ( - !include || (Array.isArray(include) && include.length === 0) || - (isPlainObject(include) && Object.keys(include).length === 0) - ) { - cb(null, objects); - return; + if (!include || (Array.isArray(include) && include.length === 0) || + (isPlainObject(include) && Object.keys(include).length === 0)) { + // The objects are empty + return process.nextTick(function() { + cb && cb(null, objects); + }); } - include = processIncludeJoin(include); + include = normalizeInclude(include); - var keyVals = {}; - var objsByKeys = {}; + async.each(include, function(item, callback) { + processIncludeItem(objects, item, callback); + }, function(err) { + cb && cb(err, objects); + }); - var nbCallbacks = 0; - for (var i = 0; i < include.length; i++) { - var callback = processIncludeItem(objects, include[i], keyVals, objsByKeys); - if (callback !== null) { - nbCallbacks++; - callback(function () { - nbCallbacks--; - if (nbCallbacks === 0) { - cb(null, objects); - } - }); - } else { - cb(null, objects); - } - } - function processIncludeJoin(ij) { - if (typeof ij === 'string') { - ij = [ij]; - } - if (isPlainObject(ij)) { - var newIj = []; - for (var key in ij) { + /*! + * Normalize the include to be an array + * @param include + * @returns {*} + */ + function normalizeInclude(include) { + if (typeof include === 'string') { + return [include]; + } else if (isPlainObject(include)) { + // Build an array of key/value pairs + var newInclude = []; + for (var key in include) { var obj = {}; - obj[key] = ij[key]; - newIj.push(obj); + obj[key] = include[key]; + newInclude.push(obj); } - return newIj; + return newInclude; + } else { + return include; } - return ij; } - function processIncludeItem(objs, include, keyVals, objsByKeys) { + function processIncludeItem(objs, include, cb) { var relations = self.relations; var relationName, subInclude; @@ -96,7 +91,7 @@ Inclusion.include = function (objects, include, cb) { subInclude = include[relationName]; } else { relationName = include; - subInclude = []; + subInclude = null; } var relation = relations[relationName]; @@ -107,68 +102,40 @@ Inclusion.include = function (objects, include, cb) { }; } - var req = {'where': {}}; - - if (!keyVals[relation.keyFrom]) { - objsByKeys[relation.keyFrom] = {}; - objs.filter(Boolean).forEach(function (obj) { - if (!objsByKeys[relation.keyFrom][obj[relation.keyFrom]]) { - objsByKeys[relation.keyFrom][obj[relation.keyFrom]] = []; - } - objsByKeys[relation.keyFrom][obj[relation.keyFrom]].push(obj); - }); - keyVals[relation.keyFrom] = Object.keys(objsByKeys[relation.keyFrom]); - } - - if (keyVals[relation.keyFrom].length > 0) { - // deep clone is necessary since inq seems to change the processed array - var keysToBeProcessed = {}; - var inValues = []; - for (var j = 0; j < keyVals[relation.keyFrom].length; j++) { - keysToBeProcessed[keyVals[relation.keyFrom][j]] = true; - if (keyVals[relation.keyFrom][j] !== 'null' - && keyVals[relation.keyFrom][j] !== 'undefined') { - inValues.push(keyVals[relation.keyFrom][j]); + // Calling the relation method for each object + async.each(objs, function (obj, callback) { + if(relation.type === 'belongsTo') { + // If the belongsTo relation doesn't have an owner + if(obj[relation.keyFrom] === null || obj[relation.keyFrom] === undefined) { + defineCachedRelations(obj); + // Set to null if the owner doesn't exist + obj.__cachedRelations[relationName] = null; + obj[relationName] = null; + return callback(); } } + var inst = (obj instanceof self) ? obj : new self(obj); + var relationMethod = inst[relationName]; + // FIXME: [rfeng] How do we pass in the refresh flag? + relationMethod(function (err, result) { + if (err) { + return callback(err); + } else { + defineCachedRelations(obj); + obj.__cachedRelations[relationName] = result; + obj[relationName] = result; - req.where[relation.keyTo] = {inq: inValues}; - req.include = subInclude; - - return function (cb) { - relation.modelTo.find(req, function (err, objsIncluded) { - var objectsFrom, j; - for (var i = 0; i < objsIncluded.length; i++) { - delete keysToBeProcessed[objsIncluded[i][relation.keyTo]]; - objectsFrom = objsByKeys[relation.keyFrom][objsIncluded[i][relation.keyTo]]; - for (j = 0; j < objectsFrom.length; j++) { - defineCachedRelations(objectsFrom[j]); - if (relation.multiple) { - if (!objectsFrom[j].__cachedRelations[relationName]) { - objectsFrom[j].__cachedRelations[relationName] = []; - } - objectsFrom[j].__cachedRelations[relationName].push(objsIncluded[i]); - } else { - objectsFrom[j].__cachedRelations[relationName] = objsIncluded[i]; - } - } + if (subInclude && result) { + var subItems = relation.multiple ? result : [result]; + // Recursively include the related models + relation.modelTo.include(subItems, subInclude, callback); + } else { + callback(null, result); } + } + }); + }, cb); - // No relation have been found for these keys - for (var key in keysToBeProcessed) { - objectsFrom = objsByKeys[relation.keyFrom][key]; - for (j = 0; j < objectsFrom.length; j++) { - defineCachedRelations(objectsFrom[j]); - objectsFrom[j].__cachedRelations[relationName] = - relation.multiple ? [] : null; - } - } - cb(err, objsIncluded); - }); - }; - } - - return null; } }; diff --git a/lib/model.js b/lib/model.js index a587c826..3e57ee67 100644 --- a/lib/model.js +++ b/lib/model.js @@ -116,7 +116,10 @@ ModelBaseClass.prototype._initProperties = function (data, options) { if (i in properties && typeof data[i] !== 'function') { this.__data[i] = this.__dataWas[i] = clone(data[i]); } else if (i in ctor.relations) { - this.__data[ctor.relations[i].keyFrom] = this.__dataWas[i] = data[i][ctor.relations[i].keyTo]; + if (ctor.relations[i].type === 'belongsTo' && data[i] !== null && data[i] !== undefined) { + // If the related model is populated + this.__data[ctor.relations[i].keyFrom] = this.__dataWas[i] = data[i][ctor.relations[i].keyTo]; + } this.__cachedRelations[i] = data[i]; } else { if (strict === false) { diff --git a/lib/relations.js b/lib/relations.js index fad2d858..3b4a05a7 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -78,6 +78,10 @@ Relation.hasMany = function hasMany(anotherClass, params) { modelTo: anotherClass, multiple: true }; + + if (params.through) { + this.relations[methodName].modelThrough = params.through; + } // each instance of this class should have method named // pluralize(anotherClass.modelName) // which is actually just anotherClass.find({where: {thisModelNameId: this[idName]}}, cb); diff --git a/lib/scope.js b/lib/scope.js index 68462b79..da8aa1ad 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -47,6 +47,7 @@ function defineScope(cls, targetClass, name, params, methods) { * */ get: function () { + var self = this; var f = function caller(condOrRefresh, cb) { var actualCond = {}; var actualRefresh = false; @@ -65,8 +66,9 @@ function defineScope(cls, targetClass, name, params, methods) { throw new Error('Method can be only called with one or two arguments'); } - if (!this.__cachedRelations || (this.__cachedRelations[name] === undefined) || actualRefresh) { - var self = this; + if (!self.__cachedRelations || self.__cachedRelations[name] === undefined + || actualRefresh) { + // It either doesn't hit the cache or reresh is required var params = mergeParams(actualCond, caller._scope); return targetClass.find(params, function (err, data) { if (!err && saveOnCache) { @@ -76,7 +78,8 @@ function defineScope(cls, targetClass, name, params, methods) { cb(err, data); }); } else { - cb(null, this.__cachedRelations[name]); + // Return from cache + cb(null, self.__cachedRelations[name]); } }; f._scope = typeof params === 'function' ? params.call(this) : params; diff --git a/lib/sql.js b/lib/sql.js index 701c8dc0..e2006c84 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -194,8 +194,11 @@ BaseSQL.prototype.exists = function (model, id, callback) { * @param {Function} callback The callback function */ BaseSQL.prototype.find = function find(model, id, callback) { + var idQuery = (id === null || id === undefined) + ? this.idColumnEscaped(model) + ' IS NULL' + : this.idColumnEscaped(model) + ' = ' + id; var sql = 'SELECT * FROM ' + - this.tableEscaped(model) + ' WHERE ' + this.idColumnEscaped(model) + ' = ' + id + ' LIMIT 1'; + this.tableEscaped(model) + ' WHERE ' + idQuery + ' LIMIT 1'; this.query(sql, function (err, data) { if (data && data.length === 1) { diff --git a/test/include.test.js b/test/include.test.js index d8ca49fd..bd918d5d 100644 --- a/test/include.test.js +++ b/test/include.test.js @@ -1,15 +1,14 @@ // This test written in mocha+should.js var should = require('./init.js'); -var db, User, Post, Passport, City, Street, Building; -var nbSchemaRequests = 0; +var db, User, Post, Passport, City, Street, Building, Assembly, Part; describe('include', function () { before(setup); it('should fetch belongsTo relation', function (done) { - Passport.all({include: 'owner'}, function (err, passports) { + Passport.find({include: 'owner'}, function (err, passports) { passports.length.should.be.ok; passports.forEach(function (p) { p.__cachedRelations.should.have.property('owner'); @@ -32,7 +31,7 @@ describe('include', function () { }); it('should fetch hasMany relation', function (done) { - User.all({include: 'posts'}, function (err, users) { + User.find({include: 'posts'}, function (err, users) { should.not.exist(err); should.exist(users); users.length.should.be.ok; @@ -52,7 +51,7 @@ describe('include', function () { }); it('should fetch Passport - Owner - Posts', function (done) { - Passport.all({include: {owner: 'posts'}}, function (err, passports) { + Passport.find({include: {owner: 'posts'}}, function (err, passports) { should.not.exist(err); should.exist(passports); passports.length.should.be.ok; @@ -81,7 +80,7 @@ describe('include', function () { }); it('should fetch Passports - User - Posts - User', function (done) { - Passport.all({ + Passport.find({ include: {owner: {posts: 'author'}} }, function (err, passports) { should.not.exist(err); @@ -109,7 +108,7 @@ describe('include', function () { }); it('should fetch User - Posts AND Passports', function (done) { - User.all({include: ['posts', 'passports']}, function (err, users) { + User.find({include: ['posts', 'passports']}, function (err, users) { should.not.exist(err); should.exist(users); users.length.should.be.ok; @@ -140,6 +139,39 @@ describe('include', function () { }); }); + it('should support hasAndBelongsToMany', function (done) { + + Assembly.destroyAll(function(err) { + Part.destroyAll(function(err) { + Assembly.relations.parts.modelThrough.destroyAll(function(err) { + Assembly.create({name: 'car'}, function (err, assembly) { + Part.create({partNumber: 'engine'}, function (err, part) { + assembly.parts.add(part, function (err, data) { + assembly.parts(function (err, parts) { + should.not.exist(err); + should.exists(parts); + parts.length.should.equal(1); + parts[0].partNumber.should.equal('engine'); + + // Create a part + assembly.parts.create({partNumber: 'door'}, function (err, part4) { + + Assembly.find({include: 'parts'}, function (err, assemblies) { + assemblies.length.should.equal(1); + assemblies[0].parts.length.should.equal(2); + done(); + }); + + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); function setup(done) { @@ -163,6 +195,17 @@ function setup(done) { User.hasMany('posts', {foreignKey: 'userId'}); Post.belongsTo('author', {model: User, foreignKey: 'userId'}); + Assembly = db.define('Assembly', { + name: String + }); + + Part = db.define('Part', { + partNumber: String + }); + + Assembly.hasAndBelongsToMany(Part); + Part.hasAndBelongsToMany(Assembly); + db.automigrate(function () { var createdUsers = []; var createdPassports = []; From c3f5487914da8df4ac6a491285603b7a60d90b4a Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 19 Mar 2014 17:14:09 -0700 Subject: [PATCH 8/8] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 01816627..b80632f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "1.3.6", + "version": "1.3.7", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop",