From 65fa7a1c00a4787305a1505af5221634cd83388a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 20 May 2014 10:47:44 +0200 Subject: [PATCH 01/57] validations: include more details in `err.message` Modify ValidationError constructor to include the model name and a human-readable representation of the validation errors (messages) in the error message. Before this change, the message was pointing the reader to `err.details`. Most frameworks (e.g. express, mocha) log only `err.message` but not other error properties, thus the logs were rather unhelpful. Example of the new error message: The `User` instance is not valid. Details: `name` can't be blank. --- lib/validations.js | 28 +++++++++++++++++++++++++--- test/validations.test.js | 19 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/validations.js b/lib/validations.js index 200bf6ba..a12a8127 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -687,12 +687,18 @@ function ValidationError(obj) { if (!(this instanceof ValidationError)) return new ValidationError(obj); this.name = 'ValidationError'; - this.message = 'The Model instance is not valid. ' + - 'See `details` property of the error object for more info.'; + + var context = obj && obj.constructor && obj.constructor.modelName; + this.message = util.format( + 'The %s instance is not valid. Details: %s.', + context ? '`' + context + '`' : 'model', + formatErrors(obj.errors) || '(unknown)' + ); + this.statusCode = 422; this.details = { - context: obj && obj.constructor && obj.constructor.modelName, + context: context, codes: obj.errors && obj.errors.codes, messages: obj.errors }; @@ -701,3 +707,19 @@ function ValidationError(obj) { } util.inherits(ValidationError, Error); + +function formatErrors(errors) { + var DELIM = '; '; + errors = errors || {}; + return Object.getOwnPropertyNames(errors) + .filter(function(propertyName) { + return Array.isArray(errors[propertyName]); + }) + .map(function(propertyName) { + var messages = errors[propertyName]; + return messages.map(function(msg) { + return '`' + propertyName + '` ' + msg; + }).join(DELIM); + }) + .join(DELIM); +} diff --git a/test/validations.test.js b/test/validations.test.js index f01414aa..c3b5c8d4 100644 --- a/test/validations.test.js +++ b/test/validations.test.js @@ -111,6 +111,25 @@ describe('validations', function () { done(); }); + it('should include validation messages in err.message', function(done) { + delete User._validations; + User.validatesPresenceOf('name'); + User.create(function (e, u) { + should.exist(e); + e.message.should.match(/`name` can't be blank/); + done(); + }); + }); + + it('should include model name in err.message', function(done) { + delete User._validations; + User.validatesPresenceOf('name'); + User.create(function (e, u) { + should.exist(e); + e.message.should.match(/`User` instance/i); + done(); + }); + }); }); }); From 8e609150610a4d7cdf40991c43a3e3cd6d2fade6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 20 May 2014 17:59:05 +0200 Subject: [PATCH 02/57] 1.5.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 09efe955..fb56d97e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "1.5.1", + "version": "1.5.2", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", From 835708d25de5405dd0e9cba98809f6736360b0cd Mon Sep 17 00:00:00 2001 From: crandmck Date: Wed, 21 May 2014 17:50:44 -0700 Subject: [PATCH 03/57] Copy info from api-model.md to JSDoc --- lib/dao.js | 138 ++++++++++++++++++++++++++++++++++++----------- lib/relations.js | 44 ++++++++++++--- 2 files changed, 143 insertions(+), 39 deletions(-) diff --git a/lib/dao.js b/lib/dao.js index aad2cc6c..99c2fc4a 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -22,12 +22,10 @@ var removeUndefined = utils.removeUndefined; /** * 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. + * This class describes only abstract behavior. Refer to the specific connector for additional details. * * `DataAccessObject` mixes `Inclusion` classes methods. - * * @class DataAccessObject - * @param {Object} data Initial object data */ function DataAccessObject() { if (DataAccessObject._mixins) { @@ -39,6 +37,8 @@ function DataAccessObject() { } } + + function idName(m) { return m.getDataSource().idName ? m.getDataSource().idName(m.modelName) : 'id'; @@ -71,15 +71,20 @@ DataAccessObject._forDB = function (data) { }; /** - * Create new instance of Model class, saved in database. - * The callback function is called with arguments: + * Create an instance of Model with given data and save to the attached data source. Callback is optional. + * Example: + *```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. * + * @param {Object} data Optional data object + * @param {Function} callback Callback function called with these 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; @@ -210,7 +215,10 @@ function stillConnecting(dataSource, obj, args) { } /** - * Update or insert a model instance. + * Update or insert a model instance: update exiting record if one is found, such that parameter `data.id` matches `id` of model instance; + * otherwise, insert a new record. + * + * NOTE: No setters, validations, or hooks are applied when using upsert. * `updateOrCreate` is an alias * @param {Object} data The model instance data * @param {Function} callback The callback function (optional). @@ -269,10 +277,12 @@ 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 + * Find one record that matches specified query criteria. Same as `find`, but limited to one record, and this function returns an + * object, not a collection. + * If the specified instance is not found, then create it using data provided as second argument. * - * @param {Object} query Search conditions: {where: {test: 'me'}}. + * @param {Object} query Search conditions. See [find](#dataaccessobjectfindquery-callback) for query format. + * For example: `{where: {test: 'me'}}`. * @param {Object} data Object to create. * @param {Function} cb Callback called with (err, instance) */ @@ -323,7 +333,14 @@ setRemoting(DataAccessObject.exists, { }); /** - * Find object by id + * Find model instance by ID. + * + * Example: + * ```js + * User.findById(23, function(err, user) { + * console.info(user.id); // 23 + * }); + * ``` * * @param {*} id Primary key value * @param {Function} cb Callback called with (err, instance) @@ -477,18 +494,59 @@ 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} [query] the query object - * - * - where: Object `{ key: val, key2: {gt: 'val2'}}` - * - include: String, Object or Array. See `DataAccessObject.include()`. - * - order: String - * - limit: Number - * - skip: Number + * Find all instances of Model that match the specified query. + * Fields used for filter and sort should be declared with `{index: true}` in model definition. + * See [Querying models](http://docs.strongloop.com/display/DOC/Querying+models) for more information. * - * @param {Function} callback (required) called with two arguments: err (null or Error), array of instances + * For example, find the second page of ten 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 + * ); + * ``` + * + * @options {Object} [query] Optional JSON object that specifies query criteria and parameters. + * @property {Object} where Search criteria in JSON format `{ key: val, key2: {gt: 'val2'}}`. + * Operations: + * - gt: > + * - gte: >= + * - lt: < + * - lte: <= + * - between + * - inq: IN + * - nin: NOT IN + * - neq: != + * - like: LIKE + * - nlike: NOT LIKE + * + * You can also use `and` and `or` operations. See [Querying models](http://docs.strongloop.com/display/DOC/Querying+models) for more information. + * @property {String|Object|Array} include Allows you to load relations of several objects and optimize numbers of requests. + * Format examples; + * - `'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 + * See `DataAccessObject.include()`. + * @property {String} order Sort order. Format: `'key1 ASC, key2 DESC'` + * @property {Number} limit Maximum number of instances to return. + * @property {Number} skip Number of instances to skip. + * @property {Number} offset Alias for `skip`. + * @property {Object|Array|String} fields Included/excluded fields. + * - `['foo']` or `'foo'` - include only the foo property + * - `['foo', 'bar']` - include the foo and bar properties. Format: + * - `{foo: true}` - include only foo + * - `{bat: false}` - include all properties, exclude bat + * + * @param {Function} callback Required callback function. Call this function with two arguments: `err` (null or Error) and an array of instances. */ DataAccessObject.find = function find(query, cb) { @@ -613,10 +671,11 @@ setRemoting(DataAccessObject.find, { }); /** - * Find one record, same as `all`, limited by 1 and return object, not collection + * Find one record, same as `find`, but limited to one result. This function returns an object, not a collection. * - * @param {Object} query - search conditions: {where: {test: 'me'}} - * @param {Function} cb - callback called with (err, instance) + * @param {Object} query Sarch conditions. See [find](#dataaccessobjectfindquery-callback) for query format. + * For example: `{where: {test: 'me'}}`. + * @param {Function} cb Callback function called with (err, instance) */ DataAccessObject.findOne = function findOne(query, cb) { if (stillConnecting(this.getDataSource(), this, arguments)) return; @@ -641,8 +700,16 @@ setRemoting(DataAccessObject.findOne, { }); /** - * Destroy all matching records - * @param {Object} [where] An object that defines the criteria + * Destroy all matching records. + * Delete all model instances from data source. Note: destroyAll method does not destroy hooks. + * Example: + *````js + * Product.destroyAll({price: {gt: 99}}, function(err) { + // removed matching products + * }); + * ```` + * + * @param {Object} [where] Optional object that defines the criteria. This is a "where" object. Do NOT pass a filter object. * @param {Function} [cb] Callback called with (err) */ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyAll = function destroyAll(where, cb) { @@ -697,9 +764,16 @@ setRemoting(DataAccessObject.deleteById, { }); /** - * Return count of matched records + * Return count of matched records. Optional query parameter allows you to count filtered set of model instances. + * Example: + * + *```js + * User.count({approved: true}, function(err, count) { + * console.log(count); // 2081 + * }); + * ``` * - * @param {Object} where Search conditions (optional) + * @param {Object} [where] Search conditions (optional) * @param {Function} cb Callback, called with (err, count) */ DataAccessObject.count = function (where, cb) { diff --git a/lib/relations.js b/lib/relations.js index a18da4e9..81495170 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -61,8 +61,37 @@ function lookupModel(models, modelName) { * ``` * Book.hasMany('chapters', {model: Chapter}); * ``` - * @param {Relation} anotherClass Class to has many - * @options {Object} parameters Configuration parameters + * + * Query and create 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 + * 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); + * }); + * }); + *``` + * @param {Object|String} anotherClass Model object (or String name of model) to which you are creating the relationship. + * @options {Object} parameters Configuration parameters; see below. * @property {String} as * @property {String} foreignKey Property name of foreign key field. * @property {Object} model Model object @@ -275,8 +304,8 @@ Relation.hasMany = function hasMany(anotherClass, params) { * ``` * 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} Parameters Configuration parameters + * @param {Class|String} anotherClass Model object (or String name of model) to which you are creating the relationship. + * @options {Object} params Configuration parameters; see below. * @property {String} as Can be 'propertyName' * @property {String} foreignKey Name of foreign key property. * @@ -446,11 +475,12 @@ Relation.belongsTo = function (anotherClass, params) { * user.groups.remove(group, callback); * ``` * - * @param {String|Function} anotherClass - target class to hasAndBelongsToMany or name of + * @param {String|Object} anotherClass Model object (or String name of model) to which you are creating the relationship. * the relation - * @options {Object} params - configuration {as: String, foreignKey: *, model: ModelClass} - * @property {Object} model Model name + * @options {Object} params Configuration parameters; see below. + * @property {String} as * @property {String} foreignKey Property name of foreign key field. + * @property {Object} model Model object */ Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params) { params = params || {}; From 83979cad7ac5d75838e323c6214a051c927f8aa0 Mon Sep 17 00:00:00 2001 From: crandmck Date: Thu, 22 May 2014 15:02:57 -0700 Subject: [PATCH 04/57] Remove JSDocs for scopeMethods.add(acInst) and scopeMethods.remove(acInst) --- lib/relations.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/relations.js b/lib/relations.js index 81495170..c07b87d2 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -92,7 +92,7 @@ function lookupModel(models, modelName) { *``` * @param {Object|String} anotherClass Model object (or String name of model) to which you are creating the relationship. * @options {Object} parameters Configuration parameters; see below. - * @property {String} as + * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. * @property {String} foreignKey Property name of foreign key field. * @property {Object} model Model object */ @@ -166,9 +166,9 @@ 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 + * @param {Object|ID} acInst The actual instance or id value */ scopeMethods.add = function (acInst, done) { var data = {}; @@ -181,7 +181,7 @@ Relation.hasMany = function hasMany(anotherClass, params) { 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 */ @@ -306,7 +306,7 @@ Relation.hasMany = function hasMany(anotherClass, params) { * * @param {Class|String} anotherClass Model object (or String name of model) to which you are creating the relationship. * @options {Object} params Configuration parameters; see below. - * @property {String} as Can be 'propertyName' + * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. * @property {String} foreignKey Name of foreign key property. * */ @@ -478,7 +478,7 @@ Relation.belongsTo = function (anotherClass, params) { * @param {String|Object} anotherClass Model object (or String name of model) to which you are creating the relationship. * the relation * @options {Object} params Configuration parameters; see below. - * @property {String} as + * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. * @property {String} foreignKey Property name of foreign key field. * @property {Object} model Model object */ From 97b1501c1fa05d31ad8a8cd633e3dda3ba0e4d4b Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 26 May 2014 08:21:23 -0700 Subject: [PATCH 05/57] Keep undefined/null values for the array type This allows connectors to distinguish between empty array and undefined/null. For example, mongodb will not override existing array properties if the value is undefined. --- lib/model.js | 11 +++++++---- test/loopback-dl.test.js | 9 ++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/model.js b/lib/model.js index b38b7120..508c5b6d 100644 --- a/lib/model.js +++ b/lib/model.js @@ -134,8 +134,9 @@ ModelBaseClass.prototype._initProperties = function (data, options) { } } + var propertyName; if (applySetters === true) { - for (var propertyName in data) { + for (propertyName in data) { if (typeof data[propertyName] !== 'function' && ((propertyName in properties) || (propertyName in ctor.relations))) { self[propertyName] = self.__data[propertyName] || data[propertyName]; } @@ -144,7 +145,7 @@ ModelBaseClass.prototype._initProperties = function (data, options) { // Set the unknown properties as properties to the object if (strict === false) { - for (var propertyName in data) { + for (propertyName in data) { if (typeof data[propertyName] !== 'function' && !(propertyName in properties)) { self[propertyName] = self.__data[propertyName] || data[propertyName]; } @@ -174,8 +175,10 @@ ModelBaseClass.prototype._initProperties = function (data, options) { } } if (type.name === 'Array' || Array.isArray(type)) { - if (!(self.__data[propertyName] instanceof List)) { - self.__data[propertyName] = new List(self.__data[propertyName], type, self); + if (!(self.__data[propertyName] instanceof List) + && self.__data[propertyName] !== undefined + && self.__data[propertyName] !== null ) { + self.__data[propertyName] = List(self.__data[propertyName], type, self); } } } diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index 6a1e013d..7f247007 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -582,7 +582,8 @@ describe('Models attached to a dataSource', function() { var ds = new DataSource('memory');// define models Post = ds.define('Post', { title: { type: String, length: 255, index: true }, - content: { type: String } + content: { type: String }, + comments: [String] }); }); @@ -613,9 +614,10 @@ describe('Models attached to a dataSource', function() { }); it('updateOrCreate should update the instance without removing existing properties', function (done) { - Post.create({title: 'a', content: 'AAA'}, function (err, post) { + Post.create({title: 'a', content: 'AAA', comments: ['Comment1']}, function (err, post) { post = post.toObject(); delete post.title; + delete post.comments; Post.updateOrCreate(post, function (err, p) { should.not.exist(err); p.id.should.be.equal(post.id); @@ -627,7 +629,8 @@ describe('Models attached to a dataSource', function() { should.not.exist(p._id); p.content.should.be.equal(post.content); p.title.should.be.equal('a'); - + p.comments.length.should.be.equal(1); + p.comments[0].should.be.equal('Comment1'); done(); }); }); From fc6c2245997bd05cf5703a702432ecc65077bdf0 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 27 May 2014 16:11:57 -0700 Subject: [PATCH 06/57] Bump version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fb56d97e..70607672 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "1.5.2", + "version": "1.5.3", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", @@ -27,7 +27,7 @@ "mocha": "~1.18.2" }, "dependencies": { - "async": "~0.8.0", + "async": "~0.9.0", "inflection": "~1.3.5", "traverse": "~0.6.6", "qs": "~0.6.6", From fb4dfb6f628009b7200d613807adb1dd60860afc Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 27 May 2014 22:03:41 -0700 Subject: [PATCH 07/57] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 70607672..24c3fc7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "1.5.3", + "version": "1.5.4", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", From f52e287c82e6b04a48c4dc6696b78fe9e14f1e1f Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 15 May 2014 08:56:00 -0700 Subject: [PATCH 08/57] Add support for logical operator (AND/OR) --- lib/connectors/memory.js | 34 ++++++++++++++++++++++------ test/basic-querying.test.js | 44 +++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index da4bc2c4..ce466221 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -332,14 +332,31 @@ Memory.prototype.all = function all(model, filter, callback) { }; function applyFilter(filter) { - if (typeof filter.where === 'function') { - return filter.where; + var where = filter.where; + if (typeof where === 'function') { + return where; } - var keys = Object.keys(filter.where); + var keys = Object.keys(where); return function (obj) { var pass = true; keys.forEach(function (key) { - if (!test(filter.where[key], obj && obj[key])) { + if(key === 'and' || key === 'or') { + if(Array.isArray(where[key])) { + if(key === 'and') { + pass = where[key].every(function(cond) { + return applyFilter({where: cond})(obj); + }); + return pass; + } + if(key === 'or') { + pass = where[key].some(function(cond) { + return applyFilter({where: cond})(obj); + }); + return pass; + } + } + } + if (!test(where[key], obj && obj[key])) { pass = false; } }); @@ -350,11 +367,14 @@ function applyFilter(filter) { if (typeof value === 'string' && example && example.constructor.name === 'RegExp') { return value.match(example); } - if (typeof example === 'undefined') return undefined; - if (typeof value === 'undefined') return undefined; + if (example === undefined || value === undefined) { + return undefined; + } if (typeof example === 'object') { // ignore geo near filter - if (example.near) return true; + if (example.near) { + return true; + } if (example.inq) { if (!value) return false; diff --git a/test/basic-querying.test.js b/test/basic-querying.test.js index 9b90d66d..ae72ca45 100644 --- a/test/basic-querying.test.js +++ b/test/basic-querying.test.js @@ -131,6 +131,50 @@ describe('basic-querying', function () { }); }); + it('should support "and" operator that is satisfied', function (done) { + User.find({where: {and: [ + {name: 'John Lennon'}, + {role: 'lead'} + ]}}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 1); + done(); + }); + }); + + it('should support "and" operator that is not satisfied', function (done) { + User.find({where: {and: [ + {name: 'John Lennon'}, + {role: 'member'} + ]}}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 0); + done(); + }); + }); + + it('should support "or" that is satisfied', function (done) { + User.find({where: {or: [ + {name: 'John Lennon'}, + {role: 'lead'} + ]}}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 2); + done(); + }); + }); + + it('should support "or" operator that is not satisfied', function (done) { + User.find({where: {or: [ + {name: 'XYZ'}, + {role: 'Hello1'} + ]}}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 0); + done(); + }); + }); + it('should only include fields as specified', function (done) { var remaining = 0; From b43cae06679fb0b72c250c86db08c636dd91e5d1 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 16 May 2014 08:50:58 -0700 Subject: [PATCH 09/57] Add a path to show customer.orders(query, cb) --- examples/relations.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/relations.js b/examples/relations.js index ed3474cc..702a7071 100644 --- a/examples/relations.js +++ b/examples/relations.js @@ -3,7 +3,8 @@ var ds = new DataSource('memory'); var Order = ds.createModel('Order', { items: [String], - orderDate: Date + orderDate: Date, + qty: Number }); var Customer = ds.createModel('Customer', { @@ -43,14 +44,17 @@ Customer.create({name: 'John'}, function (err, customer) { 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) { + Order.create({customerId: customer.id, qty: 3, orderDate: new Date()}, function (err, order) { order3 = order; customer.orders(console.log); - customer.orders.create({orderDate: new Date()}, function (err, order) { + customer.orders.create({orderDate: new Date(), qty: 4}, function (err, order) { console.log(order); Customer.include([customer], 'orders', function (err, results) { console.log('Results: ', results); }); + customer.orders({where: {qty: 4}}, function(err, results) { + console.log('customer.orders', results); + }); customer.orders.findById(order3.id, console.log); customer.orders.destroy(order3.id, console.log); }); From 4abb4d2fdf8f5b74910769ecdb220064b0f2c3ec Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 16 May 2014 08:52:25 -0700 Subject: [PATCH 10/57] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a569f21f..09efe955 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "1.5.0", + "version": "1.5.1", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", From 438df25d8e32f1c424213ec9db895914c0211af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 20 May 2014 10:47:44 +0200 Subject: [PATCH 11/57] validations: include more details in `err.message` Modify ValidationError constructor to include the model name and a human-readable representation of the validation errors (messages) in the error message. Before this change, the message was pointing the reader to `err.details`. Most frameworks (e.g. express, mocha) log only `err.message` but not other error properties, thus the logs were rather unhelpful. Example of the new error message: The `User` instance is not valid. Details: `name` can't be blank. --- lib/validations.js | 28 +++++++++++++++++++++++++--- test/validations.test.js | 19 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/validations.js b/lib/validations.js index 200bf6ba..a12a8127 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -687,12 +687,18 @@ function ValidationError(obj) { if (!(this instanceof ValidationError)) return new ValidationError(obj); this.name = 'ValidationError'; - this.message = 'The Model instance is not valid. ' + - 'See `details` property of the error object for more info.'; + + var context = obj && obj.constructor && obj.constructor.modelName; + this.message = util.format( + 'The %s instance is not valid. Details: %s.', + context ? '`' + context + '`' : 'model', + formatErrors(obj.errors) || '(unknown)' + ); + this.statusCode = 422; this.details = { - context: obj && obj.constructor && obj.constructor.modelName, + context: context, codes: obj.errors && obj.errors.codes, messages: obj.errors }; @@ -701,3 +707,19 @@ function ValidationError(obj) { } util.inherits(ValidationError, Error); + +function formatErrors(errors) { + var DELIM = '; '; + errors = errors || {}; + return Object.getOwnPropertyNames(errors) + .filter(function(propertyName) { + return Array.isArray(errors[propertyName]); + }) + .map(function(propertyName) { + var messages = errors[propertyName]; + return messages.map(function(msg) { + return '`' + propertyName + '` ' + msg; + }).join(DELIM); + }) + .join(DELIM); +} diff --git a/test/validations.test.js b/test/validations.test.js index f01414aa..c3b5c8d4 100644 --- a/test/validations.test.js +++ b/test/validations.test.js @@ -111,6 +111,25 @@ describe('validations', function () { done(); }); + it('should include validation messages in err.message', function(done) { + delete User._validations; + User.validatesPresenceOf('name'); + User.create(function (e, u) { + should.exist(e); + e.message.should.match(/`name` can't be blank/); + done(); + }); + }); + + it('should include model name in err.message', function(done) { + delete User._validations; + User.validatesPresenceOf('name'); + User.create(function (e, u) { + should.exist(e); + e.message.should.match(/`User` instance/i); + done(); + }); + }); }); }); From dc3d2233e735fc23428c7db54a28bfe84253c223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 20 May 2014 17:59:05 +0200 Subject: [PATCH 12/57] 1.5.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 09efe955..fb56d97e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "1.5.1", + "version": "1.5.2", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", From ea263f86eee531fde1bd93d808f59aa7f1347a1f Mon Sep 17 00:00:00 2001 From: crandmck Date: Wed, 21 May 2014 17:50:44 -0700 Subject: [PATCH 13/57] Copy info from api-model.md to JSDoc --- lib/dao.js | 138 ++++++++++++++++++++++++++++++++++++----------- lib/relations.js | 44 ++++++++++++--- 2 files changed, 143 insertions(+), 39 deletions(-) diff --git a/lib/dao.js b/lib/dao.js index aad2cc6c..99c2fc4a 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -22,12 +22,10 @@ var removeUndefined = utils.removeUndefined; /** * 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. + * This class describes only abstract behavior. Refer to the specific connector for additional details. * * `DataAccessObject` mixes `Inclusion` classes methods. - * * @class DataAccessObject - * @param {Object} data Initial object data */ function DataAccessObject() { if (DataAccessObject._mixins) { @@ -39,6 +37,8 @@ function DataAccessObject() { } } + + function idName(m) { return m.getDataSource().idName ? m.getDataSource().idName(m.modelName) : 'id'; @@ -71,15 +71,20 @@ DataAccessObject._forDB = function (data) { }; /** - * Create new instance of Model class, saved in database. - * The callback function is called with arguments: + * Create an instance of Model with given data and save to the attached data source. Callback is optional. + * Example: + *```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. * + * @param {Object} data Optional data object + * @param {Function} callback Callback function called with these 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; @@ -210,7 +215,10 @@ function stillConnecting(dataSource, obj, args) { } /** - * Update or insert a model instance. + * Update or insert a model instance: update exiting record if one is found, such that parameter `data.id` matches `id` of model instance; + * otherwise, insert a new record. + * + * NOTE: No setters, validations, or hooks are applied when using upsert. * `updateOrCreate` is an alias * @param {Object} data The model instance data * @param {Function} callback The callback function (optional). @@ -269,10 +277,12 @@ 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 + * Find one record that matches specified query criteria. Same as `find`, but limited to one record, and this function returns an + * object, not a collection. + * If the specified instance is not found, then create it using data provided as second argument. * - * @param {Object} query Search conditions: {where: {test: 'me'}}. + * @param {Object} query Search conditions. See [find](#dataaccessobjectfindquery-callback) for query format. + * For example: `{where: {test: 'me'}}`. * @param {Object} data Object to create. * @param {Function} cb Callback called with (err, instance) */ @@ -323,7 +333,14 @@ setRemoting(DataAccessObject.exists, { }); /** - * Find object by id + * Find model instance by ID. + * + * Example: + * ```js + * User.findById(23, function(err, user) { + * console.info(user.id); // 23 + * }); + * ``` * * @param {*} id Primary key value * @param {Function} cb Callback called with (err, instance) @@ -477,18 +494,59 @@ 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} [query] the query object - * - * - where: Object `{ key: val, key2: {gt: 'val2'}}` - * - include: String, Object or Array. See `DataAccessObject.include()`. - * - order: String - * - limit: Number - * - skip: Number + * Find all instances of Model that match the specified query. + * Fields used for filter and sort should be declared with `{index: true}` in model definition. + * See [Querying models](http://docs.strongloop.com/display/DOC/Querying+models) for more information. * - * @param {Function} callback (required) called with two arguments: err (null or Error), array of instances + * For example, find the second page of ten 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 + * ); + * ``` + * + * @options {Object} [query] Optional JSON object that specifies query criteria and parameters. + * @property {Object} where Search criteria in JSON format `{ key: val, key2: {gt: 'val2'}}`. + * Operations: + * - gt: > + * - gte: >= + * - lt: < + * - lte: <= + * - between + * - inq: IN + * - nin: NOT IN + * - neq: != + * - like: LIKE + * - nlike: NOT LIKE + * + * You can also use `and` and `or` operations. See [Querying models](http://docs.strongloop.com/display/DOC/Querying+models) for more information. + * @property {String|Object|Array} include Allows you to load relations of several objects and optimize numbers of requests. + * Format examples; + * - `'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 + * See `DataAccessObject.include()`. + * @property {String} order Sort order. Format: `'key1 ASC, key2 DESC'` + * @property {Number} limit Maximum number of instances to return. + * @property {Number} skip Number of instances to skip. + * @property {Number} offset Alias for `skip`. + * @property {Object|Array|String} fields Included/excluded fields. + * - `['foo']` or `'foo'` - include only the foo property + * - `['foo', 'bar']` - include the foo and bar properties. Format: + * - `{foo: true}` - include only foo + * - `{bat: false}` - include all properties, exclude bat + * + * @param {Function} callback Required callback function. Call this function with two arguments: `err` (null or Error) and an array of instances. */ DataAccessObject.find = function find(query, cb) { @@ -613,10 +671,11 @@ setRemoting(DataAccessObject.find, { }); /** - * Find one record, same as `all`, limited by 1 and return object, not collection + * Find one record, same as `find`, but limited to one result. This function returns an object, not a collection. * - * @param {Object} query - search conditions: {where: {test: 'me'}} - * @param {Function} cb - callback called with (err, instance) + * @param {Object} query Sarch conditions. See [find](#dataaccessobjectfindquery-callback) for query format. + * For example: `{where: {test: 'me'}}`. + * @param {Function} cb Callback function called with (err, instance) */ DataAccessObject.findOne = function findOne(query, cb) { if (stillConnecting(this.getDataSource(), this, arguments)) return; @@ -641,8 +700,16 @@ setRemoting(DataAccessObject.findOne, { }); /** - * Destroy all matching records - * @param {Object} [where] An object that defines the criteria + * Destroy all matching records. + * Delete all model instances from data source. Note: destroyAll method does not destroy hooks. + * Example: + *````js + * Product.destroyAll({price: {gt: 99}}, function(err) { + // removed matching products + * }); + * ```` + * + * @param {Object} [where] Optional object that defines the criteria. This is a "where" object. Do NOT pass a filter object. * @param {Function} [cb] Callback called with (err) */ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyAll = function destroyAll(where, cb) { @@ -697,9 +764,16 @@ setRemoting(DataAccessObject.deleteById, { }); /** - * Return count of matched records + * Return count of matched records. Optional query parameter allows you to count filtered set of model instances. + * Example: + * + *```js + * User.count({approved: true}, function(err, count) { + * console.log(count); // 2081 + * }); + * ``` * - * @param {Object} where Search conditions (optional) + * @param {Object} [where] Search conditions (optional) * @param {Function} cb Callback, called with (err, count) */ DataAccessObject.count = function (where, cb) { diff --git a/lib/relations.js b/lib/relations.js index a18da4e9..81495170 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -61,8 +61,37 @@ function lookupModel(models, modelName) { * ``` * Book.hasMany('chapters', {model: Chapter}); * ``` - * @param {Relation} anotherClass Class to has many - * @options {Object} parameters Configuration parameters + * + * Query and create 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 + * 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); + * }); + * }); + *``` + * @param {Object|String} anotherClass Model object (or String name of model) to which you are creating the relationship. + * @options {Object} parameters Configuration parameters; see below. * @property {String} as * @property {String} foreignKey Property name of foreign key field. * @property {Object} model Model object @@ -275,8 +304,8 @@ Relation.hasMany = function hasMany(anotherClass, params) { * ``` * 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} Parameters Configuration parameters + * @param {Class|String} anotherClass Model object (or String name of model) to which you are creating the relationship. + * @options {Object} params Configuration parameters; see below. * @property {String} as Can be 'propertyName' * @property {String} foreignKey Name of foreign key property. * @@ -446,11 +475,12 @@ Relation.belongsTo = function (anotherClass, params) { * user.groups.remove(group, callback); * ``` * - * @param {String|Function} anotherClass - target class to hasAndBelongsToMany or name of + * @param {String|Object} anotherClass Model object (or String name of model) to which you are creating the relationship. * the relation - * @options {Object} params - configuration {as: String, foreignKey: *, model: ModelClass} - * @property {Object} model Model name + * @options {Object} params Configuration parameters; see below. + * @property {String} as * @property {String} foreignKey Property name of foreign key field. + * @property {Object} model Model object */ Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params) { params = params || {}; From e2789134b031a7692c1b1cfd68c39631b60d00fd Mon Sep 17 00:00:00 2001 From: crandmck Date: Thu, 22 May 2014 15:02:57 -0700 Subject: [PATCH 14/57] Remove JSDocs for scopeMethods.add(acInst) and scopeMethods.remove(acInst) --- lib/relations.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/relations.js b/lib/relations.js index 81495170..c07b87d2 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -92,7 +92,7 @@ function lookupModel(models, modelName) { *``` * @param {Object|String} anotherClass Model object (or String name of model) to which you are creating the relationship. * @options {Object} parameters Configuration parameters; see below. - * @property {String} as + * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. * @property {String} foreignKey Property name of foreign key field. * @property {Object} model Model object */ @@ -166,9 +166,9 @@ 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 + * @param {Object|ID} acInst The actual instance or id value */ scopeMethods.add = function (acInst, done) { var data = {}; @@ -181,7 +181,7 @@ Relation.hasMany = function hasMany(anotherClass, params) { 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 */ @@ -306,7 +306,7 @@ Relation.hasMany = function hasMany(anotherClass, params) { * * @param {Class|String} anotherClass Model object (or String name of model) to which you are creating the relationship. * @options {Object} params Configuration parameters; see below. - * @property {String} as Can be 'propertyName' + * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. * @property {String} foreignKey Name of foreign key property. * */ @@ -478,7 +478,7 @@ Relation.belongsTo = function (anotherClass, params) { * @param {String|Object} anotherClass Model object (or String name of model) to which you are creating the relationship. * the relation * @options {Object} params Configuration parameters; see below. - * @property {String} as + * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. * @property {String} foreignKey Property name of foreign key field. * @property {Object} model Model object */ From 7a087f358374c5c1ea739b45db08ebaf0cdd4367 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 26 May 2014 08:21:23 -0700 Subject: [PATCH 15/57] Keep undefined/null values for the array type This allows connectors to distinguish between empty array and undefined/null. For example, mongodb will not override existing array properties if the value is undefined. --- lib/model.js | 11 +++++++---- test/loopback-dl.test.js | 9 ++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/model.js b/lib/model.js index b38b7120..508c5b6d 100644 --- a/lib/model.js +++ b/lib/model.js @@ -134,8 +134,9 @@ ModelBaseClass.prototype._initProperties = function (data, options) { } } + var propertyName; if (applySetters === true) { - for (var propertyName in data) { + for (propertyName in data) { if (typeof data[propertyName] !== 'function' && ((propertyName in properties) || (propertyName in ctor.relations))) { self[propertyName] = self.__data[propertyName] || data[propertyName]; } @@ -144,7 +145,7 @@ ModelBaseClass.prototype._initProperties = function (data, options) { // Set the unknown properties as properties to the object if (strict === false) { - for (var propertyName in data) { + for (propertyName in data) { if (typeof data[propertyName] !== 'function' && !(propertyName in properties)) { self[propertyName] = self.__data[propertyName] || data[propertyName]; } @@ -174,8 +175,10 @@ ModelBaseClass.prototype._initProperties = function (data, options) { } } if (type.name === 'Array' || Array.isArray(type)) { - if (!(self.__data[propertyName] instanceof List)) { - self.__data[propertyName] = new List(self.__data[propertyName], type, self); + if (!(self.__data[propertyName] instanceof List) + && self.__data[propertyName] !== undefined + && self.__data[propertyName] !== null ) { + self.__data[propertyName] = List(self.__data[propertyName], type, self); } } } diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index 6a1e013d..7f247007 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -582,7 +582,8 @@ describe('Models attached to a dataSource', function() { var ds = new DataSource('memory');// define models Post = ds.define('Post', { title: { type: String, length: 255, index: true }, - content: { type: String } + content: { type: String }, + comments: [String] }); }); @@ -613,9 +614,10 @@ describe('Models attached to a dataSource', function() { }); it('updateOrCreate should update the instance without removing existing properties', function (done) { - Post.create({title: 'a', content: 'AAA'}, function (err, post) { + Post.create({title: 'a', content: 'AAA', comments: ['Comment1']}, function (err, post) { post = post.toObject(); delete post.title; + delete post.comments; Post.updateOrCreate(post, function (err, p) { should.not.exist(err); p.id.should.be.equal(post.id); @@ -627,7 +629,8 @@ describe('Models attached to a dataSource', function() { should.not.exist(p._id); p.content.should.be.equal(post.content); p.title.should.be.equal('a'); - + p.comments.length.should.be.equal(1); + p.comments[0].should.be.equal('Comment1'); done(); }); }); From 6662bde84d0ef0361989ec12e2ee1357b84b98b9 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 27 May 2014 16:11:57 -0700 Subject: [PATCH 16/57] Bump version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fb56d97e..70607672 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "1.5.2", + "version": "1.5.3", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", @@ -27,7 +27,7 @@ "mocha": "~1.18.2" }, "dependencies": { - "async": "~0.8.0", + "async": "~0.9.0", "inflection": "~1.3.5", "traverse": "~0.6.6", "qs": "~0.6.6", From 01410c3495715e84103abc36ff1628f85a35c636 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 27 May 2014 22:05:42 -0700 Subject: [PATCH 17/57] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 70607672..24c3fc7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "1.5.3", + "version": "1.5.4", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", From 653aab856ce2491e166a8b79c1e4eec2abce5edb Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Thu, 29 May 2014 15:33:01 -0700 Subject: [PATCH 18/57] JSDoc improvements Improve createModel doc, Fix some other minor issues. --- lib/datasource.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/datasource.js b/lib/datasource.js index e20d481e..44f9957e 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -494,7 +494,11 @@ DataSource.prototype.setupDataAccess = function (modelClass, settings) { }; /** - * Define a model class. + * Define a model class. Returns newly created model object. + * The first (String) argument specifying the model name is required. + * You can provide one or two JSON object arguments, to provide configuration options. + * See [Model definition reference](http://docs.strongloop.com/display/DOC/Model+definition+reference) for details. + * * Simple example: * ``` * var User = dataSource.createModel('User', { @@ -533,9 +537,8 @@ DataSource.prototype.setupDataAccess = function (modelClass, settings) { * ``` * * @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 + * @param {Object} properties Hash of model properties in format `{property: Type, property2: Type2, ...}` or `{property: {type: Type}, property2: {type: Type2}, ...}` + * @options {Object} properties Other configuration options. This corresponds to the options key in the config object. * */ @@ -766,10 +769,10 @@ DataSource.prototype.autoupdate = function (models, cb) { * * Keys in options object: * - * @property all {Boolean} If true, discover all models; if false, discover only models owned by the current user. - * @property views {Boolean} If true, nclude views; if false, only tables. - * @property limit {Number} Page size - * @property offset {Number} Starting index + * @property {Boolean} all If true, discover all models; if false, discover only models owned by the current user. + * @property {Boolean} views If true, nclude views; if false, only tables. + * @property {Number} limit Page size + * @property {Number} offset Starting index * */ DataSource.prototype.discoverModelDefinitions = function (options, cb) { From 3e8284d1ee70998567f5819f8f0f0a462d4d833f Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sun, 1 Jun 2014 09:14:42 -0700 Subject: [PATCH 19/57] Use connector's buildWhere to implement count --- lib/sql.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/sql.js b/lib/sql.js index 1777602e..23e4c741 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -292,8 +292,14 @@ BaseSQL.prototype.count = function count(model, callback, where) { var self = this; var props = this._models[model].properties; + var whereClause = ''; + if (typeof this.buildWhere === 'function') { + whereClause = this.buildWhere(model, where); + } else { + whereClause = buildWhere(where); + } this.queryOne('SELECT count(*) as cnt FROM ' + - this.tableEscaped(model) + ' ' + buildWhere(where), function (err, res) { + this.tableEscaped(model) + ' ' + whereClause, function (err, res) { if (err) { return callback(err); } From 42c1ad3dcaebe385d2afe4c15409386cc3bf0aca Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sun, 1 Jun 2014 23:31:51 -0700 Subject: [PATCH 20/57] Normalize/validate the query filter object --- lib/dao.js | 124 +++++++++++++++++++++++++++------- test/loopback-dl.test.js | 139 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 239 insertions(+), 24 deletions(-) diff --git a/lib/dao.js b/lib/dao.js index 99c2fc4a..d089c3f1 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -18,6 +18,7 @@ var Memory = require('./connectors/memory').Memory; var utils = require('./utils'); var fieldsToArray = utils.fieldsToArray; var removeUndefined = utils.removeUndefined; +var util = require('util'); /** * Base class for all persistent objects. @@ -37,8 +38,6 @@ function DataAccessObject() { } } - - function idName(m) { return m.getDataSource().idName ? m.getDataSource().idName(m.modelName) : 'id'; @@ -400,13 +399,83 @@ var operators = { nlike: 'NOT LIKE' }; +/*! + * Normalize the filter object and throw errors if invalid values are detected + * @param {Object} filter The query filter object + * @returns {Object} The normalized filter object + * @private + */ +DataAccessObject._normalize = function (filter) { + if (!filter) { + return undefined; + } + var err = null; + if ((typeof filter !== 'object') || Array.isArray(filter)) { + err = new Error(util.format('The query filter %j is not an object', filter)); + err.statusCode = 400; + throw err; + } + if (filter.limit || filter.skip || filter.offset) { + var limit = Number(filter.limit || 100); + var offset = Number(filter.skip || filter.offset || 0); + if (isNaN(limit) || limit <= 0 || Math.ceil(limit) !== limit) { + err = new Error(util.format('The limit parameter %j is not valid', + filter.limit)); + err.statusCode = 400; + throw err; + } + if (isNaN(offset) || offset < 0 || Math.ceil(offset) !== offset) { + err = new Error(util.format('The offset/skip parameter %j is not valid', + filter.skip || filter.offset)); + err.statusCode = 400; + throw err; + } + filter.limit = limit; + filter.offset = offset; + delete filter.skip; + } + + // normalize fields as array of included property names + if (filter.fields) { + filter.fields = fieldsToArray(filter.fields, + Object.keys(this.definition.properties)); + } + + filter = removeUndefined(filter); + this._coerce(filter.where); + return filter; +}; + +/*! + * Coerce values based the property types + * @param {Object} where The where clause + * @returns {Object} The coerced where clause + * @private + */ DataAccessObject._coerce = function (where) { + var self = this; if (!where) { return where; } - var props = this.getDataSource().getModelDefinition(this.modelName).properties; + if (typeof where !== 'object' || Array.isArray(where)) { + var err = new Error(util.format('The where clause %j is not an object', where)); + err.statusCode = 400; + throw err; + } + + var props = self.definition.properties; for (var p in where) { + // Handle logical operators + if (p === 'and' || p === 'or' || p === 'nor') { + var clauses = where[p]; + if (Array.isArray(clauses)) { + for (var i = 0; i < clauses.length; i++) { + self._coerce(clauses[i]); + } + } + return where; + } var DataType = props[p] && props[p].type; if (!DataType) { continue; @@ -556,24 +625,21 @@ DataAccessObject.find = function find(query, cb) { cb = query; query = null; } - var constr = this; + var self = this; query = query || {}; - if (query.where) { - query.where = this._coerce(query.where); + try { + this._normalize(query); + } catch (err) { + return process.nextTick(function () { + cb && cb(err); + }); } - 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) { - query.fields = fieldsToArray(fields, Object.keys(this.definition.properties)); - } - - query = removeUndefined(query); if (near) { if (supportsGeo) { // convert it @@ -583,15 +649,15 @@ DataAccessObject.find = function find(query, cb) { // using all documents this.getDataSource().connector.all(this.modelName, {}, function (err, data) { var memory = new Memory(); - var modelName = constr.modelName; + var modelName = self.modelName; if (err) { cb(err); } else if (Array.isArray(data)) { memory.define({ - properties: constr.dataSource.definitions[constr.modelName].properties, - settings: constr.dataSource.definitions[constr.modelName].settings, - model: constr + properties: self.dataSource.definitions[self.modelName].properties, + settings: self.dataSource.definitions[self.modelName].settings, + model: self }); data.forEach(function (obj) { @@ -614,7 +680,7 @@ DataAccessObject.find = function find(query, cb) { this.getDataSource().connector.all(this.modelName, query, function (err, data) { if (data && data.forEach) { data.forEach(function (d, i) { - var obj = new constr(); + var obj = new self(); obj._initProperties(d, {fields: query.fields}); @@ -726,9 +792,15 @@ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyA if(!err) Model.emit('deletedAll'); }.bind(this)); } else { - // Support an optional where object - where = removeUndefined(where); - where = this._coerce(where); + try { + // Support an optional where object + where = removeUndefined(where); + where = this._coerce(where); + } catch (err) { + return process.nextTick(function() { + cb && cb(err); + }); + } this.getDataSource().connector.destroyAll(this.modelName, where, function (err, data) { cb && cb(err, data); if(!err) Model.emit('deletedAll', where); @@ -783,8 +855,14 @@ DataAccessObject.count = function (where, cb) { cb = where; where = null; } - where = removeUndefined(where); - where = this._coerce(where); + try { + where = removeUndefined(where); + where = this._coerce(where); + } catch (err) { + return process.nextTick(function () { + cb && cb(err); + }); + } this.getDataSource().connector.count(this.modelName, cb, where); }; diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index 7f247007..6b1d4049 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -993,7 +993,7 @@ describe('Model with scopes', function () { }); describe('DataAccessObject', function () { - var ds, model, where; + var ds, model, where, error; before(function () { ds = new DataSource('memory'); @@ -1007,6 +1007,10 @@ describe('DataAccessObject', function () { }); }); + beforeEach(function () { + error = null; + }); + it('should be able to coerce where clause for string types', function () { where = model._coerce({id: 1}); assert.deepEqual(where, {id: '1'}); @@ -1069,6 +1073,139 @@ describe('DataAccessObject', function () { }); + it('should be able to coerce where clause with and operators', function () { + where = model._coerce({and: [{age: '10'}, {vip: 'true'}]}); + assert.deepEqual(where, {and: [{age: 10}, {vip: true}]}); + }); + + it('should be able to coerce where clause with or operators', function () { + where = model._coerce({or: [{age: '10'}, {vip: 'true'}]}); + assert.deepEqual(where, {or: [{age: 10}, {vip: true}]}); + }); + + it('should throw if the where property is not an object', function () { + try { + // The where clause has to be an object + error = err;model._coerce('abc'); + } catch (err) { + error = err; + } + assert(error, 'An error should have been thrown'); + }); + + it('should throw if the where property is an array', function () { + try { + // The where clause cannot be an array + error = err;model._coerce([ + {vip: true} + ]); + } catch (err) { + error = err; + } + assert(error, 'An error should have been thrown'); + }); + + it('should throw if the and operator does not take an array', function () { + try { + // The and operator only takes an array of objects + error = err;model._coerce({and: {x: 1}}); + } catch (err) { + error = err; + } + assert(error, 'An error should have been thrown'); + }); + + it('should throw if the or operator does not take an array', function () { + try { + // The or operator only takes an array of objects + error = err;model._coerce({or: {x: 1}}); + } catch (err) { + error = err; + } + assert(error, 'An error should have been thrown'); + }); + + it('should throw if the or operator does not take an array of objects', function () { + try { + // The or operator only takes an array of objects + error = err;model._coerce({or: ['x']}); + } catch(err) { + error = err; + } + assert(error, 'An error should have been thrown'); + }); + + it('should throw if filter property is not an object', function () { + var filter = null; + try { + // The filter clause has to be an object + filter = model._normalize('abc'); + } catch (err) { + error = err; + } + assert(error, 'An error should have been thrown'); + }); + + it('should throw if filter.limit property is not a number', function () { + try { + // The limit param must be a valid number + filter = model._normalize({limit: 'x'}); + } catch (err) { + error = err; + } + assert(error, 'An error should have been thrown'); + }); + + it('should throw if filter.limit property is nagative', function () { + try { + // The limit param must be a valid number + filter = model._normalize({limit: -1}); + } catch (err) { + error = err; + } + assert(error, 'An error should have been thrown'); + }); + + it('should throw if filter.limit property is not an integer', function () { + try { + // The limit param must be a valid number + filter = model._normalize({limit: 5.8}); + } catch (err) { + error = err; + } + assert(error, 'An error should have been thrown'); + }); + + it('should throw if filter.offset property is not a number', function () { + try { + // The limit param must be a valid number + filter = model._normalize({offset: 'x'}); + } catch (err) { + error = err; + } + assert(error, 'An error should have been thrown'); + }); + + it('should throw if filter.skip property is not a number', function () { + try { + // The limit param must be a valid number + filter = model._normalize({skip: '_'}); + } catch (err) { + error = err; + } + assert(error, 'An error should have been thrown'); + }); + + it('should normalize limit/offset/skip', function () { + filter = model._normalize({limit: '10', skip: 5}); + assert.deepEqual(filter, {limit: 10, offset: 5}); + }); + + it('should set the default value for limit', function () { + filter = model._normalize({skip: 5}); + assert.deepEqual(filter, {limit: 100, offset: 5}); + }); + it('should skip GeoPoint', function () { where = model._coerce({location: {near: {lng: 10, lat: 20}, maxDistance: 20}}); assert.deepEqual(where, {location: {near: {lng: 10, lat: 20}, maxDistance: 20}}); From cdf99568673b019aaeb5cb2b9437cdb155770de6 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 4 Jun 2014 14:02:55 -0700 Subject: [PATCH 21/57] Fix JS doc for private methods --- lib/dao.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/dao.js b/lib/dao.js index d089c3f1..128aa921 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -399,7 +399,7 @@ var operators = { nlike: 'NOT LIKE' }; -/*! +/* * Normalize the filter object and throw errors if invalid values are detected * @param {Object} filter The query filter object * @returns {Object} The normalized filter object @@ -446,7 +446,7 @@ DataAccessObject._normalize = function (filter) { return filter; }; -/*! +/* * Coerce values based the property types * @param {Object} where The where clause * @returns {Object} The coerced where clause @@ -1077,10 +1077,11 @@ setRemoting(DataAccessObject.prototype.updateAttributes, { http: {verb: 'put', path: '/'} }); -/** +/* * Reload object from persistence * Requires `id` member of `object` to be able to call `find` * @param {Function} callback Called with (err, instance) arguments + * @private */ DataAccessObject.prototype.reload = function reload(callback) { if (stillConnecting(this.getDataSource(), this, arguments)) { @@ -1091,12 +1092,13 @@ DataAccessObject.prototype.reload = function reload(callback) { }; -/*! +/* * Define readonly property on object * * @param {Object} obj * @param {String} key * @param {Mixed} value + * @private */ function defineReadonlyProp(obj, key, value) { Object.defineProperty(obj, key, { @@ -1122,12 +1124,12 @@ DataAccessObject.scope = function (name, query, targetClass) { defineScope(this, targetClass || this, name, query); }; -/*! +/* * Add 'include' */ jutil.mixin(DataAccessObject, Inclusion); -/*! +/* * Add 'relation' */ jutil.mixin(DataAccessObject, Relation); From 5f3c856d2ea79e6e8ab822273be000266155b972 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 4 Jun 2014 14:23:53 -0700 Subject: [PATCH 22/57] Fix the logical operator check --- lib/dao.js | 7 ++++++- test/loopback-dl.test.js | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/dao.js b/lib/dao.js index 128aa921..f5a6886f 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -458,8 +458,9 @@ DataAccessObject._coerce = function (where) { return where; } + var err; if (typeof where !== 'object' || Array.isArray(where)) { - var err = new Error(util.format('The where clause %j is not an object', where)); + err = new Error(util.format('The where clause %j is not an object', where)); err.statusCode = 400; throw err; } @@ -473,6 +474,10 @@ DataAccessObject._coerce = function (where) { for (var i = 0; i < clauses.length; i++) { self._coerce(clauses[i]); } + } else { + err = new Error(util.format('The %p operator has invalid clauses %j', p, clauses)); + err.statusCode = 400; + throw err; } return where; } diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index 6b1d4049..d1d2b0a8 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -1086,7 +1086,7 @@ describe('DataAccessObject', function () { it('should throw if the where property is not an object', function () { try { // The where clause has to be an object - error = err;model._coerce('abc'); + model._coerce('abc'); } catch (err) { error = err; } @@ -1096,7 +1096,7 @@ describe('DataAccessObject', function () { it('should throw if the where property is an array', function () { try { // The where clause cannot be an array - error = err;model._coerce([ + model._coerce([ {vip: true} ]); } catch (err) { @@ -1108,7 +1108,7 @@ describe('DataAccessObject', function () { it('should throw if the and operator does not take an array', function () { try { // The and operator only takes an array of objects - error = err;model._coerce({and: {x: 1}}); + model._coerce({and: {x: 1}}); } catch (err) { error = err; } @@ -1118,7 +1118,7 @@ describe('DataAccessObject', function () { it('should throw if the or operator does not take an array', function () { try { // The or operator only takes an array of objects - error = err;model._coerce({or: {x: 1}}); + model._coerce({or: {x: 1}}); } catch (err) { error = err; } @@ -1128,7 +1128,7 @@ describe('DataAccessObject', function () { it('should throw if the or operator does not take an array of objects', function () { try { // The or operator only takes an array of objects - error = err;model._coerce({or: ['x']}); + model._coerce({or: ['x']}); } catch(err) { error = err; } From 8087e4b1e7ffa3a399fe0d88771138892a2409c5 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 4 Jun 2014 15:26:04 -0700 Subject: [PATCH 23/57] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 24c3fc7d..246c21a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "1.5.4", + "version": "1.5.5", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", From f651edca03ea231e68ca6a63c8f9415cf1c183db Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Wed, 4 Jun 2014 16:05:43 -0700 Subject: [PATCH 24/57] Update docs.json Remove model-builder.js and sql.js --- docs.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs.json b/docs.json index 8864fc54..ee78dd1f 100644 --- a/docs.json +++ b/docs.json @@ -4,11 +4,9 @@ "lib/geo.js", "lib/dao.js", "lib/model.js", - "lib/model-builder.js", "lib/include.js", "lib/relations.js", - "lib/validations.js", - "lib/sql.js" + "lib/validations.js" ], "codeSectionDepth": 4, "assets": { From 29e3eb991fe6d5b85aeb325dfa2ae4721619554e Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Wed, 4 Jun 2014 16:16:39 -0700 Subject: [PATCH 25/57] Update docs.json Remove lib/model.js --- docs.json | 1 - 1 file changed, 1 deletion(-) diff --git a/docs.json b/docs.json index ee78dd1f..a7da7f44 100644 --- a/docs.json +++ b/docs.json @@ -3,7 +3,6 @@ "lib/datasource.js", "lib/geo.js", "lib/dao.js", - "lib/model.js", "lib/include.js", "lib/relations.js", "lib/validations.js" From 04e1256b8b0668b95d4a5000794be52bb43751d9 Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Wed, 4 Jun 2014 17:44:57 -0700 Subject: [PATCH 26/57] Update datasource.js Fix small typo --- lib/datasource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/datasource.js b/lib/datasource.js index 44f9957e..19aa1d21 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -1625,7 +1625,7 @@ DataSource.prototype.defineForeignKey = function defineForeignKey(className, key /** * Close database connection - * @param {Fucntion} cb The callback function. Optional. + * @param {Function} cb The callback function. Optional. */ DataSource.prototype.disconnect = function disconnect(cb) { var self = this; From a0a9fae9c6856d4142516c4099de5aaa7d9b7919 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 6 Jun 2014 08:19:41 -0700 Subject: [PATCH 27/57] Enhance comparators for memory connector --- lib/connectors/memory.js | 55 +++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index ce466221..670ece80 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -377,24 +377,61 @@ function applyFilter(filter) { } if (example.inq) { - if (!value) return false; - for (var i = 0; i < example.inq.length; i += 1) { - if (example.inq[i] == value) return true; + // if (!value) return false; + for (var i = 0; i < example.inq.length; i++) { + if (example.inq[i] == value) { + return true; + } } return false; } - if (isNum(example.gt) && example.gt < value) return true; - if (isNum(example.gte) && example.gte <= value) return true; - if (isNum(example.lt) && example.lt > value) return true; - if (isNum(example.lte) && example.lte >= value) return true; + if (testInEquality(example, value)) { + return true; + } } // not strict equality return (example !== null ? example.toString() : example) == (value !== null ? value.toString() : value); } - function isNum(n) { - return typeof n === 'number'; + /** + * Compare two values + * @param {*} val1 The 1st value + * @param {*} val2 The 2nd value + * @returns {number} 0: =, postive: >, negative < + * @private + */ + function compare(val1, val2) { + if (typeof val1 === 'number') { + return val1 - val2; + } + if (typeof val1 === 'string') { + return (val1 > val2) ? 1 : ((val1 < val2) ? -1 : 0); + } + if (typeof val1 === 'boolean') { + return val1 - val2; + } + if (val1 instanceof Date) { + return val1.getTime() - ((val2 && val2.getTime()) || 0); + } + // Return NaN if we don't know how to compare + return (val1 === val2) ? 0 : NaN; + } + + function testInEquality(example, val) { + if ('gt' in example) { + return compare(example.gt, val) > 0; + } + if ('gte' in example) { + return compare(example.gte, val) >= 0; + } + if ('lt' in example) { + return compare(example.lt, val) < 0; + } + if ('lte' in example) { + return compare(example.lte, val) <= 0; + } + return false; } } From 0191e3c2dbfe4d398c3fa82564d07a6b014f63db Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 6 Jun 2014 08:48:05 -0700 Subject: [PATCH 28/57] Add more tests --- lib/connectors/memory.js | 11 ++-- test/basic-querying.test.js | 122 +++++++++++++++++++++++++++++++++++- 2 files changed, 127 insertions(+), 6 deletions(-) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index 670ece80..e136e81f 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -412,7 +412,8 @@ function applyFilter(filter) { return val1 - val2; } if (val1 instanceof Date) { - return val1.getTime() - ((val2 && val2.getTime()) || 0); + var result = val1.getTime() - ((val2 && val2.getTime()) || 0); + return result; } // Return NaN if we don't know how to compare return (val1 === val2) ? 0 : NaN; @@ -420,16 +421,16 @@ function applyFilter(filter) { function testInEquality(example, val) { if ('gt' in example) { - return compare(example.gt, val) > 0; + return compare(val, example.gt) > 0; } if ('gte' in example) { - return compare(example.gte, val) >= 0; + return compare(val, example.gte) >= 0; } if ('lt' in example) { - return compare(example.lt, val) < 0; + return compare(val, example.lt) < 0; } if ('lte' in example) { - return compare(example.lte, val) <= 0; + return compare(val, example.lte) <= 0; } return false; } diff --git a/test/basic-querying.test.js b/test/basic-querying.test.js index ae72ca45..3b95dd68 100644 --- a/test/basic-querying.test.js +++ b/test/basic-querying.test.js @@ -10,6 +10,7 @@ describe('basic-querying', function () { User = db.define('User', { name: {type: String, index: true, sort: true}, email: {type: String, index: true}, + birthday: {type: Date, index: true}, role: {type: String, index: true}, order: {type: Number, index: true, sort: true} }); @@ -175,6 +176,123 @@ describe('basic-querying', function () { }); }); + it('should support date "gte" that is satisfied', function (done) { + User.find({where: { birthday: { "gte": new Date('1980-12-08') } + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 1); + users[0].name.should.equal('John Lennon'); + done(); + }); + }); + + it('should support date "gt" that is not satisfied', function (done) { + User.find({where: { birthday: { "gt": new Date('1980-12-08') } + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 0); + done(); + }); + }); + + it('should support date "gt" that is satisfied', function (done) { + User.find({where: { birthday: { "gt": new Date('1980-12-07') } + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 1); + users[0].name.should.equal('John Lennon'); + done(); + }); + }); + + it('should support date "lt" that is satisfied', function (done) { + User.find({where: { birthday: { "lt": new Date('1980-12-07') } + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 1); + users[0].name.should.equal('Paul McCartney'); + done(); + }); + }); + + it('should support number "gte" that is satisfied', function (done) { + User.find({where: { order: { "gte": 3} + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 4); + users[0].name.should.equal('George Harrison'); + done(); + }); + }); + + it('should support number "gt" that is not satisfied', function (done) { + User.find({where: { order: { "gt": 6 } + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 0); + done(); + }); + }); + + it('should support number "gt" that is satisfied', function (done) { + User.find({where: { order: { "gt": 5 } + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 1); + users[0].name.should.equal('Ringo Starr'); + done(); + }); + }); + + it('should support number "lt" that is satisfied', function (done) { + User.find({where: { order: { "lt": 2 } + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 1); + users[0].name.should.equal('Paul McCartney'); + done(); + }); + }); + + it('should support string "gte" that is satisfied', function (done) { + User.find({where: { name: { "gte": 'Paul McCartney'} + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 4); + users[0].name.should.equal('Paul McCartney'); + done(); + }); + }); + + it('should support string "gt" that is not satisfied', function (done) { + User.find({where: { name: { "gt": 'xyz' } + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 0); + done(); + }); + }); + + it('should support string "gt" that is satisfied', function (done) { + User.find({where: { name: { "gt": 'Paul McCartney' } + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 3); + users[0].name.should.equal('Ringo Starr'); + done(); + }); + }); + + it('should support string "lt" that is satisfied', function (done) { + User.find({where: { name: { "lt": 'Paul McCartney' } + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 2); + users[0].name.should.equal('John Lennon'); + done(); + }); + }); + it('should only include fields as specified', function (done) { var remaining = 0; @@ -214,7 +332,7 @@ describe('basic-querying', function () { } sample({name: true}).expect(['name']); - sample({name: false}).expect(['id', 'email', 'role', 'order']); + sample({name: false}).expect(['id', 'email', 'role', 'order', 'birthday']); sample({name: false, id: true}).expect(['id']); sample({id: true}).expect(['id']); sample('id').expect(['id']); @@ -363,12 +481,14 @@ function seed(done) { name: 'John Lennon', email: 'john@b3atl3s.co.uk', role: 'lead', + birthday: new Date('1980-12-08'), order: 2 }, { name: 'Paul McCartney', email: 'paul@b3atl3s.co.uk', role: 'lead', + birthday: new Date('1942-06-18'), order: 1 }, {name: 'George Harrison', order: 5}, From 171642ff4889b288008769a299f57024e5d34283 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 6 Jun 2014 09:09:56 -0700 Subject: [PATCH 29/57] Make sure the records are sorted by seq --- test/basic-querying.test.js | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/test/basic-querying.test.js b/test/basic-querying.test.js index 3b95dd68..764e6ad6 100644 --- a/test/basic-querying.test.js +++ b/test/basic-querying.test.js @@ -8,6 +8,7 @@ describe('basic-querying', function () { db = getSchema(); User = db.define('User', { + seq: {type: Number, index: true}, name: {type: String, index: true, sort: true}, email: {type: String, index: true}, birthday: {type: Date, index: true}, @@ -177,7 +178,7 @@ describe('basic-querying', function () { }); it('should support date "gte" that is satisfied', function (done) { - User.find({where: { birthday: { "gte": new Date('1980-12-08') } + User.find({order: 'seq', where: { birthday: { "gte": new Date('1980-12-08') } }}, function (err, users) { should.not.exist(err); users.should.have.property('length', 1); @@ -187,7 +188,7 @@ describe('basic-querying', function () { }); it('should support date "gt" that is not satisfied', function (done) { - User.find({where: { birthday: { "gt": new Date('1980-12-08') } + User.find({order: 'seq', where: { birthday: { "gt": new Date('1980-12-08') } }}, function (err, users) { should.not.exist(err); users.should.have.property('length', 0); @@ -196,7 +197,7 @@ describe('basic-querying', function () { }); it('should support date "gt" that is satisfied', function (done) { - User.find({where: { birthday: { "gt": new Date('1980-12-07') } + User.find({order: 'seq', where: { birthday: { "gt": new Date('1980-12-07') } }}, function (err, users) { should.not.exist(err); users.should.have.property('length', 1); @@ -206,7 +207,7 @@ describe('basic-querying', function () { }); it('should support date "lt" that is satisfied', function (done) { - User.find({where: { birthday: { "lt": new Date('1980-12-07') } + User.find({order: 'seq', where: { birthday: { "lt": new Date('1980-12-07') } }}, function (err, users) { should.not.exist(err); users.should.have.property('length', 1); @@ -216,7 +217,7 @@ describe('basic-querying', function () { }); it('should support number "gte" that is satisfied', function (done) { - User.find({where: { order: { "gte": 3} + User.find({order: 'seq', where: { order: { "gte": 3} }}, function (err, users) { should.not.exist(err); users.should.have.property('length', 4); @@ -226,7 +227,7 @@ describe('basic-querying', function () { }); it('should support number "gt" that is not satisfied', function (done) { - User.find({where: { order: { "gt": 6 } + User.find({order: 'seq', where: { order: { "gt": 6 } }}, function (err, users) { should.not.exist(err); users.should.have.property('length', 0); @@ -235,7 +236,7 @@ describe('basic-querying', function () { }); it('should support number "gt" that is satisfied', function (done) { - User.find({where: { order: { "gt": 5 } + User.find({order: 'seq', where: { order: { "gt": 5 } }}, function (err, users) { should.not.exist(err); users.should.have.property('length', 1); @@ -245,7 +246,7 @@ describe('basic-querying', function () { }); it('should support number "lt" that is satisfied', function (done) { - User.find({where: { order: { "lt": 2 } + User.find({order: 'seq', where: { order: { "lt": 2 } }}, function (err, users) { should.not.exist(err); users.should.have.property('length', 1); @@ -255,7 +256,7 @@ describe('basic-querying', function () { }); it('should support string "gte" that is satisfied', function (done) { - User.find({where: { name: { "gte": 'Paul McCartney'} + User.find({order: 'seq', where: { name: { "gte": 'Paul McCartney'} }}, function (err, users) { should.not.exist(err); users.should.have.property('length', 4); @@ -265,7 +266,7 @@ describe('basic-querying', function () { }); it('should support string "gt" that is not satisfied', function (done) { - User.find({where: { name: { "gt": 'xyz' } + User.find({order: 'seq', where: { name: { "gt": 'xyz' } }}, function (err, users) { should.not.exist(err); users.should.have.property('length', 0); @@ -274,7 +275,7 @@ describe('basic-querying', function () { }); it('should support string "gt" that is satisfied', function (done) { - User.find({where: { name: { "gt": 'Paul McCartney' } + User.find({order: 'seq', where: { name: { "gt": 'Paul McCartney' } }}, function (err, users) { should.not.exist(err); users.should.have.property('length', 3); @@ -284,7 +285,7 @@ describe('basic-querying', function () { }); it('should support string "lt" that is satisfied', function (done) { - User.find({where: { name: { "lt": 'Paul McCartney' } + User.find({order: 'seq', where: { name: { "lt": 'Paul McCartney' } }}, function (err, users) { should.not.exist(err); users.should.have.property('length', 2); @@ -332,7 +333,7 @@ describe('basic-querying', function () { } sample({name: true}).expect(['name']); - sample({name: false}).expect(['id', 'email', 'role', 'order', 'birthday']); + sample({name: false}).expect(['id', 'seq', 'email', 'role', 'order', 'birthday']); sample({name: false, id: true}).expect(['id']); sample({id: true}).expect(['id']); sample('id').expect(['id']); @@ -478,6 +479,7 @@ function seed(done) { var count = 0; var beatles = [ { + seq: 0, name: 'John Lennon', email: 'john@b3atl3s.co.uk', role: 'lead', @@ -485,16 +487,17 @@ function seed(done) { order: 2 }, { + seq: 1, name: 'Paul McCartney', email: 'paul@b3atl3s.co.uk', role: 'lead', birthday: new Date('1942-06-18'), order: 1 }, - {name: 'George Harrison', order: 5}, - {name: 'Ringo Starr', order: 6}, - {name: 'Pete Best', order: 4}, - {name: 'Stuart Sutcliffe', order: 3} + {seq: 2, name: 'George Harrison', order: 5}, + {seq: 3, name: 'Ringo Starr', order: 6}, + {seq: 4, name: 'Pete Best', order: 4}, + {seq: 5, name: 'Stuart Sutcliffe', order: 3} ]; User.destroyAll(function () { beatles.forEach(function (beatle) { From e0d3fec7436faa997a4910014e0a801871ba4b00 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 6 Jun 2014 09:10:47 -0700 Subject: [PATCH 30/57] Fix the typo --- lib/connectors/memory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index e136e81f..0b7a36cd 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -398,7 +398,7 @@ function applyFilter(filter) { * Compare two values * @param {*} val1 The 1st value * @param {*} val2 The 2nd value - * @returns {number} 0: =, postive: >, negative < + * @returns {number} 0: =, positive: >, negative < * @private */ function compare(val1, val2) { From b5816506e07e7c07c79621d9817efb4caa43cb3c Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 6 Jun 2014 09:28:08 -0700 Subject: [PATCH 31/57] Add boolean tests --- test/basic-querying.test.js | 57 ++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/test/basic-querying.test.js b/test/basic-querying.test.js index 764e6ad6..ef0bfb7f 100644 --- a/test/basic-querying.test.js +++ b/test/basic-querying.test.js @@ -13,7 +13,8 @@ describe('basic-querying', function () { email: {type: String, index: true}, birthday: {type: Date, index: true}, role: {type: String, index: true}, - order: {type: Number, index: true, sort: true} + order: {type: Number, index: true, sort: true}, + vip: {type: Boolean} }); db.automigrate(done); @@ -294,6 +295,46 @@ describe('basic-querying', function () { }); }); + it('should support boolean "gte" that is satisfied', function (done) { + User.find({order: 'seq', where: { vip: { "gte": true} + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 3); + users[0].name.should.equal('John Lennon'); + done(); + }); + }); + + it('should support boolean "gt" that is not satisfied', function (done) { + User.find({order: 'seq', where: { vip: { "gt": true } + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 0); + done(); + }); + }); + + it('should support boolean "gt" that is satisfied', function (done) { + User.find({order: 'seq', where: { vip: { "gt": false } + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 3); + users[0].name.should.equal('John Lennon'); + done(); + }); + }); + + it('should support boolean "lt" that is satisfied', function (done) { + User.find({order: 'seq', where: { vip: { "lt": true } + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 2); + users[0].name.should.equal('George Harrison'); + done(); + }); + }); + + it('should only include fields as specified', function (done) { var remaining = 0; @@ -333,7 +374,7 @@ describe('basic-querying', function () { } sample({name: true}).expect(['name']); - sample({name: false}).expect(['id', 'seq', 'email', 'role', 'order', 'birthday']); + sample({name: false}).expect(['id', 'seq', 'email', 'role', 'order', 'birthday', 'vip']); sample({name: false, id: true}).expect(['id']); sample({id: true}).expect(['id']); sample('id').expect(['id']); @@ -484,7 +525,8 @@ function seed(done) { email: 'john@b3atl3s.co.uk', role: 'lead', birthday: new Date('1980-12-08'), - order: 2 + order: 2, + vip: true }, { seq: 1, @@ -492,12 +534,13 @@ function seed(done) { email: 'paul@b3atl3s.co.uk', role: 'lead', birthday: new Date('1942-06-18'), - order: 1 + order: 1, + vip: true }, - {seq: 2, name: 'George Harrison', order: 5}, - {seq: 3, name: 'Ringo Starr', order: 6}, + {seq: 2, name: 'George Harrison', order: 5, vip: false}, + {seq: 3, name: 'Ringo Starr', order: 6, vip: false}, {seq: 4, name: 'Pete Best', order: 4}, - {seq: 5, name: 'Stuart Sutcliffe', order: 3} + {seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true} ]; User.destroyAll(function () { beatles.forEach(function (beatle) { From c355c99cbde07b946e31972b62d8d3fdaeea71e4 Mon Sep 17 00:00:00 2001 From: crandmck Date: Tue, 10 Jun 2014 14:27:58 -0700 Subject: [PATCH 32/57] More JSDoc cleanup --- lib/datasource.js | 230 ++++++++++++++++++++++------------------------ 1 file changed, 109 insertions(+), 121 deletions(-) diff --git a/lib/datasource.js b/lib/datasource.js index 19aa1d21..7c8b4190 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -587,6 +587,7 @@ DataSource.prototype.createModel = DataSource.prototype.define = function define * Mixin DataAccessObject methods. * * @param {Function} ModelCtor The model constructor + * @private */ DataSource.prototype.mixin = function (ModelCtor) { @@ -719,12 +720,14 @@ DataSource.prototype.defineProperty = function (model, prop, params) { /** * Drop each model table and re-create. - * This method applies only to SQL connectors. - * + * This method applies only to database connectors. For MongoDB, it drops and creates indexes. + * + * **WARNING**: Calling this function deletes all data! Use `autoupdate()` to preserve data. + * * @param {String} model Model to migrate. If not present, apply to all models. Can also be an array of Strings. - * @param {Function} cb Callback function. Optional. + * @param {Function} [callback] Callback function. Optional. * - * 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(); @@ -741,7 +744,7 @@ DataSource.prototype.automigrate = function (models, cb) { /** * Update existing database tables. - * This method make sense only for sql connectors. + * This method applies only to database connectors. * * @param {String} model Model to migrate. If not present, apply to all models. Can also be an array of Strings. * @param {Function} [cb] The callback function @@ -765,10 +768,7 @@ DataSource.prototype.autoupdate = function (models, cb) { * * @param {Object} options The options * @param {Function} Callback function. Optional. - * @options {Object} options Discovery options. - * - * Keys in options object: - * + * @options {Object} options Discovery options. See below. * @property {Boolean} all If true, discover all models; if false, discover only models owned by the current user. * @property {Boolean} views If true, nclude views; if false, only tables. * @property {Number} limit Page size @@ -785,8 +785,12 @@ DataSource.prototype.discoverModelDefinitions = function (options, cb) { }; /** - * The synchronous version of discoverModelDefinitions - * @param {Object} options The options + * The synchronous version of discoverModelDefinitions. + * @options {Object} options The options + * @property {Boolean} all If true, discover all models; if false, discover only models owned by the current user. + * @property {Boolean} views If true, nclude views; if false, only tables. + * @property {Number} limit Page size + * @property {Number} offset Starting index * @returns {*} */ DataSource.prototype.discoverModelDefinitionsSync = function (options) { @@ -800,24 +804,22 @@ DataSource.prototype.discoverModelDefinitionsSync = function (options) { /** * Discover properties for a given model. * - * property description: + * Callback function return value is an object that can have the following properties: * -*| 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 + *| 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| * * @param {String} modelName The table/view name - * @param {Object} options The options + * @options {Object} options The options + * @property {String} owner|schema The database owner or schema * @param {Function} cb Callback function. Optional * */ @@ -846,22 +848,19 @@ DataSource.prototype.discoverModelPropertiesSync = function (modelName, options) /** * Discover primary keys for a given owner/modelName. + * Callback function return value is an object that can have the following properties: * - * 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) - * - * The owner defaults to current user. - * - * Options: - * - owner/schema The database owner/schema + *| Key | Type | Description | + *|-----|------|-------------| + *| owner |String | Table schema or owner (may be null). Owner defaults to current user. + *| tableName |String| Table name + *| columnName |String| Column name + *| keySeq |Number| Sequence number within primary key (1 indicates the first column in the primary key; 2 indicates the second column in the primary key). + *| pkName |String| Primary key name (may be null) * * @param {String} modelName The model name - * @param {Object} options The options + * @options {Object} options The options + * @property {String} owner|schema The database owner or schema * @param {Function} [cb] The callback function */ DataSource.prototype.discoverPrimaryKeys = function (modelName, options, cb) { @@ -876,7 +875,8 @@ DataSource.prototype.discoverPrimaryKeys = function (modelName, options, cb) { /** * The synchronous version of discoverPrimaryKeys * @param {String} modelName The model name - * @param {Object} options The options + * @options {Object} options The options + * @property {String} owner|schema The database owner orschema * @returns {*} */ DataSource.prototype.discoverPrimaryKeysSync = function (modelName, options) { @@ -890,24 +890,23 @@ DataSource.prototype.discoverPrimaryKeysSync = function (modelName, options) { /** * Discover foreign keys for a given owner/modelName * - * `foreign key description` + * Callback function return value is an object that can have the following properties: * - * fkOwner String => foreign key table schema (may be null) - * fkName String => foreign key name (may be null) - * fkTableName String => foreign key table name - * fkColumnName String => foreign key column name - * keySeq Number => sequence number within a foreign key( a value of 1 represents the first column of the foreign key, a value of 2 would represent the second column within the foreign key). - * pkOwner String => primary key table schema being imported (may be null) - * pkName String => primary key name (may be null) - * pkTableName String => primary key table name being imported - * pkColumnName String => primary key column name being imported - * - * `options` - * - * owner/schema The database owner/schema + *| Key | Type | Description | + *|-----|------|-------------| + *|fkOwner |String | Foreign key table schema (may be null) + *|fkName |String | Foreign key name (may be null) + *|fkTableName |String | Foreign key table name + *|fkColumnName |String | Foreign key column name + *|keySeq |Number | Sequence number within a foreign key( a value of 1 represents the first column of the foreign key, a value of 2 would represent the second column within the foreign key). + *|pkOwner |String | Primary key table schema being imported (may be null) + *|pkName |String | Primary key name (may be null) + *|pkTableName |String | Primary key table name being imported + *|pkColumnName |String | Primary key column name being imported * * @param {String} modelName The model name - * @param {Object} options The options + * @options {Object} options The options + * @property {String} owner|schema The database owner or schema * @param {Function} [cb] The callback function * */ @@ -936,27 +935,26 @@ DataSource.prototype.discoverForeignKeysSync = function (modelName, options) { }; /** - * Retrieves a description of the foreign key columns that reference the given table's primary key columns (the foreign keys exported by a table). - * They are ordered by fkTableOwner, fkTableName, and keySeq. + * Retrieves a description of the foreign key columns that reference the given table's primary key columns + * (the foreign keys exported by a table), ordered by fkTableOwner, fkTableName, and keySeq. + * + * Callback function return value is an object that can have the following properties: * - * `foreign key description` - * - * fkOwner {String} => foreign key table schema (may be null) - * fkName {String} => foreign key name (may be null) - * fkTableName {String} => foreign key table name - * fkColumnName {String} => foreign key column name - * keySeq {Number} => sequence number within a foreign key( a value of 1 represents the first column of the foreign key, a value of 2 would represent the second column within the foreign key). - * pkOwner {String} => primary key table schema being imported (may be null) - * pkName {String} => primary key name (may be null) - * pkTableName {String} => primary key table name being imported - * pkColumnName {String} => primary key column name being imported - * - * `options` - * - * owner/schema The database owner/schema + *| Key | Type | Description | + *|-----|------|-------------| + *|fkOwner |String | Foreign key table schema (may be null) + *|fkName |String | Foreign key name (may be null) + *|fkTableName |String | Foreign key table name + *|fkColumnName |String | Foreign key column name + *|keySeq |Number | Sequence number within a foreign key( a value of 1 represents the first column of the foreign key, a value of 2 would represent the second column within the foreign key). + *|pkOwner |String | Primary key table schema being imported (may be null) + *|pkName |String | Primary key name (may be null) + *|pkTableName |String | Primary key table name being imported + *|pkColumnName |String | Primary key column name being imported * * @param {String} modelName The model name - * @param {Object} options The options + * @options {Object} options The options + * @property {String} owner|schema The database owner or schema * @param {Function} [cb] The callback function */ DataSource.prototype.discoverExportedForeignKeys = function (modelName, options, cb) { @@ -1083,17 +1081,14 @@ DataSource.prototype.discoverSchema = function (modelName, options, cb) { }; /** - * Discover schema from a given modelName/view + * Discover schema from a given modelName/view. * - * `options` - * - * {String} owner/schema - The database owner/schema name - * {Boolean} relations - If relations (primary key/foreign key) are navigated - * {Boolean} all - If all owners are included - * {Boolean} views - If views are included - * - * @param {String} modelName The model name - * @param {Object} [options] The options + * @param {String} modelName The model name. + * @options {Object} [options] Options; see below. + * @property {String} owner|schema Database owner or schema name. + * @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise. + * @property {Boolean} all True if all owners are included; false otherwise. + * @property {Boolean} views True if views are included; false otherwise. * @param {Function} [cb] The callback function */ DataSource.prototype.discoverSchemas = function (modelName, options, cb) { @@ -1255,15 +1250,12 @@ DataSource.prototype.discoverSchemas = function (modelName, options, cb) { /** * Discover schema from a given table/view synchronously * - * `options` - * - * {String} owner/schema - The database owner/schema name - * {Boolean} relations - If relations (primary key/foreign key) are navigated - * {Boolean} all - If all owners are included - * {Boolean} views - If views are included - * * @param {String} modelName The model name - * @param {Object} [options] The options + * @options {Object} [options] Options; see below. + * @property {String} owner|schema Database owner or schema name. + * @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise. + * @property {Boolean} all True if all owners are included; false otherwise. + * @property {Boolean} views True if views are included; false otherwise. */ DataSource.prototype.discoverSchemasSync = function (modelName, options) { var self = this; @@ -1395,17 +1387,14 @@ DataSource.prototype.discoverSchemasSync = function (modelName, options) { }; /** - * Discover and build models from the given owner/modelName + * Discover and build models from the specified owner/modelName. * - * `options` - * - * {String} owner/schema - The database owner/schema name - * {Boolean} relations - If relations (primary key/foreign key) are navigated - * {Boolean} all - If all owners are included - * {Boolean} views - If views are included - * - * @param {String} modelName The model name - * @param {Object} [options] The options + * @param {String} modelName The model name. + * @options {Object} [options] Options; see below. + * @property {String} owner|schema Database owner or schema name. + * @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise. + * @property {Boolean} all True if all owners are included; false otherwise. + * @property {Boolean} views True if views are included; false otherwise. * @param {Function} [cb] The callback function */ DataSource.prototype.discoverAndBuildModels = function (modelName, options, cb) { @@ -1432,15 +1421,15 @@ DataSource.prototype.discoverAndBuildModels = function (modelName, options, cb) }; /** - * Discover and build models from the given owner/modelName synchronously - * - * `options` - * - * {String} owner/schema - The database owner/schema name - * {Boolean} relations - If relations (primary key/foreign key) are navigated - * {Boolean} all - If all owners are included - * {Boolean} views - If views are included + * Discover and build models from the given owner/modelName synchronously. * + * @param {String} modelName The model name. + * @options {Object} [options] Options; see below. + * @property {String} owner|schema Database owner or schema name. + * @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise. + * @property {Boolean} all True if all owners are included; false otherwise. + * @property {Boolean} views True if views are included; false otherwise. + * @param {String} modelName The model name * @param {Object} [options] The options */ @@ -1459,8 +1448,8 @@ DataSource.prototype.discoverAndBuildModelsSync = function (modelName, options) /** * Check whether migrations needed - * This method make sense only for sql connectors. - * @param {String[]} [models] A model name or an array of model names. If not present, apply to all models + * This method applies only to SQL connectors. + * @param {String|String[]} [models] A model name or an array of model names. If not present, apply to all models. */ DataSource.prototype.isActual = function (models, cb) { this.freeze(); @@ -1516,8 +1505,8 @@ DataSource.prototype.tableName = function (modelName) { /** * Return column name for specified modelName and propertyName * @param {String} modelName The model name - * @param propertyName The property name - * @returns {String} columnName + * @param {String} propertyName The property name + * @returns {String} columnName The column name. */ DataSource.prototype.columnName = function (modelName, propertyName) { return this.getModelDefinition(modelName).columnName(this.connector.name, propertyName); @@ -1526,7 +1515,7 @@ DataSource.prototype.columnName = function (modelName, propertyName) { /** * Return column metadata for specified modelName and propertyName * @param {String} modelName The model name - * @param propertyName The property name + * @param {String} propertyName The property name * @returns {Object} column metadata */ DataSource.prototype.columnMetadata = function (modelName, propertyName) { @@ -1625,7 +1614,7 @@ DataSource.prototype.defineForeignKey = function defineForeignKey(className, key /** * Close database connection - * @param {Function} cb The callback function. Optional. + * @param {Function} [cb] The callback function. Optional. */ DataSource.prototype.disconnect = function disconnect(cb) { var self = this; @@ -1741,7 +1730,6 @@ DataSource.prototype.enableRemote = function (operation) { * and disabled operations. To list the operations, call `dataSource.operations()`. * *```js - * * var oracle = loopback.createDataSource({ * connector: require('loopback-connector-oracle'), * host: '...', @@ -1752,8 +1740,8 @@ DataSource.prototype.enableRemote = function (operation) { * **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. + * - 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 */ @@ -1831,11 +1819,11 @@ DataSource.prototype.isRelational = function () { return this.connector && this.connector.relational; }; -/** +/*! * Check if the data source is ready. * Returns a Boolean value. - * @param obj {Object} ? - * @param args {Object} ? + * @param {Object} obj ? + * @param {Object} args ? */ DataSource.prototype.ready = function (obj, args) { var self = this; From bb57fcbe110a8ced8603f690ea92dd1d0203d8d7 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 10 Jun 2014 16:11:50 -0700 Subject: [PATCH 33/57] Fix the comparison for null/boolean values --- lib/connectors/memory.js | 10 +++++++--- lib/dao.js | 8 ++++++-- test/basic-querying.test.js | 27 +++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index 0b7a36cd..ab54f959 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -402,21 +402,25 @@ function applyFilter(filter) { * @private */ function compare(val1, val2) { + if(val1 == null || val2 == null) { + // Either val1 or val2 is null or undefined + return val1 == val2 ? 0 : NaN; + } if (typeof val1 === 'number') { return val1 - val2; } if (typeof val1 === 'string') { - return (val1 > val2) ? 1 : ((val1 < val2) ? -1 : 0); + return (val1 > val2) ? 1 : ((val1 < val2) ? -1 : (val1 == val2) ? 0 : NaN); } if (typeof val1 === 'boolean') { return val1 - val2; } if (val1 instanceof Date) { - var result = val1.getTime() - ((val2 && val2.getTime()) || 0); + var result = val1 - val2; return result; } // Return NaN if we don't know how to compare - return (val1 === val2) ? 0 : NaN; + return (val1 == val2) ? 0 : NaN; } function testInEquality(example, val) { diff --git a/lib/dao.js b/lib/dao.js index f5a6886f..d4bd40de 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -551,10 +551,14 @@ DataAccessObject._coerce = function (where) { // Coerce the array items if (Array.isArray(val)) { for (var i = 0; i < val.length; i++) { - val[i] = DataType(val[i]); + if (val[i] !== null && val[i] !== undefined) { + val[i] = DataType(val[i]); + } } } else { - val = DataType(val); + if (val !== null && val !== undefined) { + val = DataType(val); + } } // Rebuild {property: {operator: value}} if (operator) { diff --git a/test/basic-querying.test.js b/test/basic-querying.test.js index ef0bfb7f..eaccb348 100644 --- a/test/basic-querying.test.js +++ b/test/basic-querying.test.js @@ -256,6 +256,33 @@ describe('basic-querying', function () { }); }); + it('should support number "gt" that is satisfied by null value', function (done) { + User.find({order: 'seq', where: { order: { "gt": null } + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 0); + done(); + }); + }); + + it('should support number "lt" that is not satisfied by null value', function (done) { + User.find({order: 'seq', where: { order: { "lt": null } + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 0); + done(); + }); + }); + + it('should support string "gte" that is satisfied by null value', function (done) { + User.find({order: 'seq', where: { name: { "gte": null} + }}, function (err, users) { + should.not.exist(err); + users.should.have.property('length', 0); + done(); + }); + }); + it('should support string "gte" that is satisfied', function (done) { User.find({order: 'seq', where: { name: { "gte": 'Paul McCartney'} }}, function (err, users) { From 4fd3c969f92f235cbb47618221cbf968a12f43c1 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Wed, 11 Jun 2014 16:59:21 -0300 Subject: [PATCH 34/57] Convert null to NotFoundError for remoting call to DataAccessObject.findOne. --- lib/dao.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/dao.js b/lib/dao.js index f5a6886f..0f54304f 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -78,7 +78,7 @@ DataAccessObject._forDB = function (data) { * }); * ``` * 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. + * saved or having an ID. * * @param {Object} data Optional data object * @param {Function} callback Callback function called with these arguments: @@ -216,7 +216,7 @@ function stillConnecting(dataSource, obj, args) { /** * Update or insert a model instance: update exiting record if one is found, such that parameter `data.id` matches `id` of model instance; * otherwise, insert a new record. - * + * * NOTE: No setters, validations, or hooks are applied when using upsert. * `updateOrCreate` is an alias * @param {Object} data The model instance data @@ -333,7 +333,7 @@ setRemoting(DataAccessObject.exists, { /** * Find model instance by ID. - * + * * Example: * ```js * User.findById(23, function(err, user) { @@ -571,7 +571,7 @@ DataAccessObject._coerce = function (where) { * Find all instances of Model that match the specified query. * Fields used for filter and sort should be declared with `{index: true}` in model definition. * See [Querying models](http://docs.strongloop.com/display/DOC/Querying+models) for more information. - * + * * For example, find the second page of ten users over age 21 in descending order exluding the password property. * * ```js @@ -588,7 +588,7 @@ DataAccessObject._coerce = function (where) { * ``` * * @options {Object} [query] Optional JSON object that specifies query criteria and parameters. - * @property {Object} where Search criteria in JSON format `{ key: val, key2: {gt: 'val2'}}`. + * @property {Object} where Search criteria in JSON format `{ key: val, key2: {gt: 'val2'}}`. * Operations: * - gt: > * - gte: >= @@ -600,7 +600,7 @@ DataAccessObject._coerce = function (where) { * - neq: != * - like: LIKE * - nlike: NOT LIKE - * + * * You can also use `and` and `or` operations. See [Querying models](http://docs.strongloop.com/display/DOC/Querying+models) for more information. * @property {String|Object|Array} include Allows you to load relations of several objects and optimize numbers of requests. * Format examples; @@ -619,7 +619,7 @@ DataAccessObject._coerce = function (where) { * - `['foo', 'bar']` - include the foo and bar properties. Format: * - `{foo: true}` - include only foo * - `{bat: false}` - include all properties, exclude bat - * + * * @param {Function} callback Required callback function. Call this function with two arguments: `err` (null or Error) and an array of instances. */ @@ -767,7 +767,8 @@ setRemoting(DataAccessObject.findOne, { description: 'Find first instance of the model matched by filter from the data source', accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, orderBy, offset, and limit'}, returns: {arg: 'data', type: 'object', root: true}, - http: {verb: 'get', path: '/findOne'} + http: {verb: 'get', path: '/findOne'}, + rest: {after: convertNullToNotFoundError} }); /** @@ -779,7 +780,7 @@ setRemoting(DataAccessObject.findOne, { // removed matching products * }); * ```` - * + * * @param {Object} [where] Optional object that defines the criteria. This is a "where" object. Do NOT pass a filter object. * @param {Function} [cb] Callback called with (err) */ @@ -814,7 +815,7 @@ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyA }; /** - * Delete the record with the specified ID. + * Delete the record with the specified ID. * Aliases are `destroyById` and `deleteById`. * @param {*} id The id value * @param {Function} cb Callback called with (err) @@ -843,7 +844,7 @@ setRemoting(DataAccessObject.deleteById, { /** * Return count of matched records. Optional query parameter allows you to count filtered set of model instances. * Example: - * + * *```js * User.count({approved: true}, function(err, count) { * console.log(count); // 2081 From 1787f5ec4f5aef4892da016b48ece699065c5378 Mon Sep 17 00:00:00 2001 From: crandmck Date: Wed, 11 Jun 2014 15:47:44 -0700 Subject: [PATCH 35/57] Add properties and other doc cleanup --- lib/geo.js | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/lib/geo.js b/lib/geo.js index 1dac1abd..b5b8cd3b 100644 --- a/lib/geo.js +++ b/lib/geo.js @@ -106,9 +106,12 @@ exports.GeoPoint = GeoPoint; * }); * ``` * @class GeoPoint - * @param {Object} latlong Object with two Number properties: lat and long. - * @prop {Number} lat - * @prop {Number} lng + * @property {Number} lat The latitude in degrees. + * @property {Number} lng The longitude in degrees. + * + * @options {Object} Options Object with two Number properties: lat and long. + * @property {Number} lat The latitude point in degrees. Range: -90 to 90. + * @property {Number} lng The longitude point in degrees. Range: -90 to 90. */ function GeoPoint(data) { @@ -138,20 +141,18 @@ function GeoPoint(data) { assert(data.lat <= 90, 'lat must be <= 90'); assert(data.lat >= -90, 'lat must be >= -90'); -/** - * @property {Number} lat The latitude point in degrees. Range: -90 to 90. -*/ - this.lat = data.lat; - -/** - * @property {Number} lng The longitude point in degrees. Range: -90 to 90. -*/ + this.lat = data.lat; this.lng = data.lng; } /** * Determine the spherical distance between two GeoPoints. - * Specify units of measurement with the 'type' property in options object. Type can be: + * + * @param {GeoPoint} pointA Point A + * @param {GeoPoint} pointB Point B + * @options {Object} options Options object with one key, 'type'. See below. + * @property {String} type Unit of measurement, one of: + * * - `miles` (default) * - `radians` * - `kilometers` @@ -159,9 +160,6 @@ function GeoPoint(data) { * - `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) { @@ -190,7 +188,16 @@ GeoPoint.distanceBetween = function distanceBetween(a, b, options) { * 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). + * @options {Object} options Options object with one key, 'type'. See below. + * @property {String} type Unit of measurement, one of: + * + * - `miles` (default) + * - `radians` + * - `kilometers` + * - `meters` + * - `miles` + * - `feet` + * - `degrees` */ GeoPoint.prototype.distanceTo = function (point, options) { From 1406c22a646a2e10255b039d3fbc6a042476b8a1 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sun, 15 Jun 2014 15:53:58 -0700 Subject: [PATCH 36/57] Refactor relation into classes --- lib/relation-definition.js | 641 +++++++++++++++++++++++++++++++++++++ lib/relations.js | 384 +--------------------- test/loopback-dl.test.js | 18 +- 3 files changed, 668 insertions(+), 375 deletions(-) create mode 100644 lib/relation-definition.js diff --git a/lib/relation-definition.js b/lib/relation-definition.js new file mode 100644 index 00000000..b3dfe835 --- /dev/null +++ b/lib/relation-definition.js @@ -0,0 +1,641 @@ +/*! + * Dependencies + */ +var assert = require('assert'); +var util = require('util'); +var i8n = require('inflection'); +var defineScope = require('./scope.js').defineScope; +var ModelBaseClass = require('./model.js'); + +exports.Relation = Relation; +exports.RelationDefinition = RelationDefinition; + +var RelationTypes = { + belongsTo: 'belongsTo', + hasMany: 'hasMany', + hasOne: 'hasOne', + hasAndBelongsToMany: 'hasAndBelongsToMany' +}; + +exports.RelationTypes = RelationTypes; +exports.HasMany = HasMany; +exports.HasManyThrough = HasManyThrough; +exports.HasOne = HasOne; +exports.HasAndBelongsToMany = HasAndBelongsToMany; +exports.BelongsTo = BelongsTo; + +var RelationClasses = { + belongsTo: BelongsTo, + hasMany: HasMany, + hasManyThrough: HasManyThrough, + hasOne: HasOne, + hasAndBelongsToMany: HasAndBelongsToMany +}; + +function normalizeType(type) { + if (!type) { + return type; + } + var t1 = type.toLowerCase(); + for (var t2 in RelationTypes) { + if (t2.toLowerCase() === t1) { + return t2; + } + } + return null; +} + +/** + * Relation definition class. Use to define relationships between models. + * @param {Object} definition + * @class RelationDefinition + */ +function RelationDefinition(definition) { + if (!(this instanceof RelationDefinition)) { + return new RelationDefinition(definition); + } + definition = definition || {}; + this.name = definition.name; + assert(this.name, 'Relation name is missing'); + this.type = normalizeType(definition.type); + assert(this.type, 'Invalid relation type: ' + definition.type); + this.modelFrom = definition.modelFrom; + assert(this.modelFrom); + this.keyFrom = definition.keyFrom; + this.modelTo = definition.modelTo; + assert(this.modelTo); + this.keyTo = definition.keyTo; + this.modelThrough = definition.modelThrough; + this.keyThrough = definition.keyThrough; + this.multiple = (this.type !== 'belongsTo' && this.type !== 'hasOne'); +} + +RelationDefinition.prototype.toJSON = function () { + var json = { + name: this.name, + type: this.type, + modelFrom: this.modelFrom.modelName, + keyFrom: this.keyFrom, + modelTo: this.modelTo.modelName, + keyTo: this.keyTo, + multiple: this.multiple + }; + if (this.modelThrough) { + json.modelThrough = this.modelThrough.modelName; + json.keyThrough = this.keyThrough; + } + return json; +}; + +/** + * A relation attaching to a given model instance + * @param {RelationDefinition|Object} definition + * @param {Object} modelInstance + * @returns {Relation} + * @constructor + * @class Relation + */ +function Relation(definition, modelInstance) { + if (!(this instanceof Relation)) { + return new Relation(definition, modelInstance); + } + if (!(definition instanceof RelationDefinition)) { + definition = new RelationDefinition(definition); + } + this.definition = definition; + this.modelInstance = modelInstance; +} + +/** + * HasMany subclass + * @param {RelationDefinition|Object} definition + * @param {Object} modelInstance + * @returns {HasMany} + * @constructor + * @class HasMany + */ +function HasMany(definition, modelInstance) { + if (!(this instanceof HasMany)) { + return new HasMany(definition, modelInstance); + } + assert(definition.type === RelationTypes.hasMany); + Relation.apply(this, arguments); +} + +util.inherits(HasMany, Relation); + +/** + * HasManyThrough subclass + * @param {RelationDefinition|Object} definition + * @param {Object} modelInstance + * @returns {HasManyThrough} + * @constructor + * @class HasManyThrough + */ +function HasManyThrough(definition, modelInstance) { + if (!(this instanceof HasManyThrough)) { + return new HasManyThrough(definition, modelInstance); + } + assert(definition.type === RelationTypes.hasMany); + assert(definition.modelThrough); + HasMany.apply(this, arguments); +} + +util.inherits(HasManyThrough, HasMany); + +/** + * BelongsTo subclass + * @param {RelationDefinition|Object} definition + * @param {Object} modelInstance + * @returns {BelongsTo} + * @constructor + * @class BelongsTo + */ +function BelongsTo(definition, modelInstance) { + if (!(this instanceof BelongsTo)) { + return new BelongsTo(definition, modelInstance); + } + assert(definition.type === RelationTypes.belongsTo); + Relation.apply(this, arguments); +} + +util.inherits(BelongsTo, Relation); + +/** + * HasAndBelongsToMany subclass + * @param {RelationDefinition|Object} definition + * @param {Object} modelInstance + * @returns {HasAndBelongsToMany} + * @constructor + * @class HasAndBelongsToMany + */ +function HasAndBelongsToMany(definition, modelInstance) { + if (!(this instanceof HasAndBelongsToMany)) { + return new HasAndBelongsToMany(definition, modelInstance); + } + assert(definition.type === RelationTypes.hasAndBelongsToMany); + Relation.apply(this, arguments); +} + +util.inherits(HasAndBelongsToMany, Relation); + +/** + * HasOne subclass + * @param {RelationDefinition|Object} definition + * @param {Object} modelInstance + * @returns {HasOne} + * @constructor + * @class HasOne + */ +function HasOne(definition, modelInstance) { + if (!(this instanceof HasOne)) { + return new HasOne(definition, modelInstance); + } + assert(definition.type === RelationTypes.hasOne); + Relation.apply(this, arguments); +} + +util.inherits(HasOne, Relation); + + +/*! + * Find the relation by foreign key + * @param {*} foreignKey The foreign key + * @returns {Object} The relation object + */ +function findBelongsTo(model, foreignKey) { + var relations = model.relations; + var keys = Object.keys(relations); + for (var k = 0; k < keys.length; k++) { + var rel = relations[keys[k]]; + if (rel.type === RelationTypes.belongsTo && + rel.keyFrom === foreignKey) { + return keys[k]; + } + } + return null; +} + +/*! + * 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]; + } + var lookupClassName = modelName.toLowerCase(); + for (var name in models) { + if (name.toLowerCase() === lookupClassName) { + return models[name]; + } + } +} + +/** + * Define a "one to many" relationship by specifying the model name + * + * Examples: + * ``` + * User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'}); + * ``` + * + * ``` + * Book.hasMany(Chapter); + * ``` + * Or, equivalently: + * ``` + * Book.hasMany('chapters', {model: Chapter}); + * ``` + * @param {Model} modelFrom Source model class + * @param {Object|String} modelTo Model object (or String name of model) to which you are creating the relationship. + * @options {Object} params Configuration parameters; see below. + * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. + * @property {String} foreignKey Property name of foreign key field. + * @property {Object} model Model object + */ +RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { + var thisClassName = modelFrom.modelName; + params = params || {}; + if (typeof modelTo === 'string') { + params.as = modelTo; + if (params.model) { + modelTo = params.model; + } else { + var modelToName = i8n.singularize(modelTo).toLowerCase(); + modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName); + } + } + var relationName = params.as || i8n.camelize(modelTo.pluralModelName, true); + var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true); + + var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id'; + + var definition = new RelationDefinition({ + name: relationName, + type: RelationTypes.hasMany, + modelFrom: modelFrom, + keyFrom: idName, + keyTo: fk, + modelTo: modelTo, + multiple: true + }); + + if (params.through) { + definition.modelThrough = params.through; + var keyThrough = definition.throughKey || i8n.camelize(modelTo.modelName + '_id', true); + definition.keyThrough = keyThrough; + } + + modelFrom.relations[relationName] = definition; + + if (!params.through) { + // obviously, modelTo should have attribute called `fk` + modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, this.modelName); + } + + var scopeMethods = { + findById: scopeMethod(definition, 'findById'), + destroy: scopeMethod(definition, 'destroyById') + } + + if(definition.modelThrough) { + scopeMethods.create = scopeMethod(definition, 'create'); + scopeMethods.add = scopeMethod(definition, 'add'); + scopeMethods.remove = scopeMethod(definition, 'remove'); + } + + // Mix the property and scoped methods into the prototype class + defineScope(modelFrom.prototype, params.through || modelTo, relationName, function () { + var filter = {}; + filter.where = {}; + filter.where[fk] = this[idName]; + if (params.through) { + filter.collect = i8n.camelize(modelTo.modelName, true); + filter.include = filter.collect; + } + return filter; + }, scopeMethods); + +}; + +function scopeMethod(definition, methodName) { + var method = function () { + var relationClass = RelationClasses[definition.type]; + if (definition.type === RelationTypes.hasMany && definition.modelThrough) { + relationClass = RelationClasses.hasManyThrough; + } + var relation = new relationClass(definition, this); + return relation[methodName].apply(relation, arguments); + }; + return method; +} + +HasMany.prototype.findById = function (id, cb) { + var modelTo = this.definition.modelTo; + var fk = this.definition.keyTo; + var pk = this.definition.keyFrom; + var modelInstance = this.modelInstance; + modelTo.findById(id, function (err, inst) { + if (err) { + return cb(err); + } + if (!inst) { + return cb(new Error('Not found')); + } + // Check if the foreign key matches the primary key + if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) { + cb(null, inst); + } else { + cb(new Error('Permission denied: foreign key does not match the primary key')); + } + }); +}; + +HasMany.prototype.destroyById = function (id, cb) { + var modelTo = this.definition.modelTo; + var fk = this.definition.keyTo; + var pk = this.definition.keyFrom; + var modelInstance = this.modelInstance; + modelTo.findById(id, function (err, inst) { + if (err) { + return cb(err); + } + if (!inst) { + return cb(new Error('Not found')); + } + // Check if the foreign key matches the primary key + if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) { + inst.destroy(cb); + } else { + cb(new Error('Permission denied: foreign key does not match the primary key')); + } + }); +}; + +// 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 +HasManyThrough.prototype.create = function create(data, done) { + var modelTo = this.definition.modelTo; + var modelThrough = this.definition.modelThrough; + var fk = this.definition.keyTo; + var fk2 = this.definition.keyThrough; + + if (typeof data === 'function' && !done) { + done = data; + data = {}; + } + + var self = this.modelInstance; + // First create the target model + modelTo.create(data, function (err, ac) { + if (err) { + return done && done(err, ac); + } + var d = {}; + d[findBelongsTo(modelThrough, fk)] = self; + d[findBelongsTo(modelThrough, fk2)] = ac; + // Then create the through model + modelThrough.create(d, function (e) { + if (e) { + // Undo creation of the target model + ac.destroy(function () { + done && done(e); + }); + } else { + done && done(err, ac); + } + }); + }); +}; + +/** + * Add the target model instance to the 'hasMany' relation + * @param {Object|ID} acInst The actual instance or id value + */ +HasManyThrough.prototype.add = function (acInst, done) { + var modelThrough = this.definition.modelThrough; + var fk = this.definition.keyTo; + var fk2 = this.definition.keyThrough; + var pk = this.definition.keyFrom; + + var data = {}; + var query = {}; + query[fk] = this[pk]; + data[findBelongsTo(modelThrough, fk)] = this.modelInstance; + query[fk2] = acInst[pk] || acInst; + data[findBelongsTo(modelThrough, fk2)] = acInst; + // Create an instance of the through model + modelThrough.findOrCreate({where: query}, data, done); +}; + +/** + * Remove the target model instance from the 'hasMany' relation + * @param {Object|ID) acInst The actual instance or id value + */ +HasManyThrough.prototype.remove = function (acInst, done) { + var modelThrough = this.definition.modelThrough; + var fk2 = this.definition.keyThrough; + var pk = this.definition.keyFrom; + + var q = {}; + q[fk2] = acInst[pk] || acInst; + modelThrough.deleteAll(q, done ); +}; + + +/** + * Declare "belongsTo" relation that 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 an application includes users and posts, and each post can + * be written by exactly one user. The following code specifies that `Post` has + * a reference called `author` to the `User` model via the `userId` property of + * `Post` as the foreign key. + * ``` + * Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}); + * ``` + * + * This optional parameter default value is false, so the related object will + * be loaded from cache if available. + * + * @param {Class|String} modelTo Model object (or String name of model) to + * which you are creating the relationship. + * @options {Object} params Configuration parameters; see below. + * @property {String} as Name of the property in the referring model that + * corresponds to the foreign key field in the related model. + * @property {String} foreignKey Name of foreign key property. + * + */ +RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { + params = params || {}; + if ('string' === typeof modelTo) { + params.as = modelTo; + if (params.model) { + modelTo = params.model; + } else { + var modelToName = modelTo.toLowerCase(); + modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName); + } + } + + var idName = modelFrom.dataSource.idName(modelTo.modelName) || 'id'; + var relationName = params.as || i8n.camelize(modelTo.modelName, true); + var fk = params.foreignKey || relationName + 'Id'; + + var relationDef = modelFrom.relations[relationName] = new RelationDefinition({ + name: relationName, + type: RelationTypes.belongsTo, + modelFrom: modelFrom, + keyFrom: fk, + keyTo: idName, + modelTo: modelTo + }); + + modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelTo.modelName); + + // Define a property for the scope so that we have 'this' for the scoped methods + Object.defineProperty(modelFrom.prototype, relationName, { + enumerable: true, + configurable: true, + get: function() { + var relation = new BelongsTo(relationDef, this); + var relationMethod = relation.related.bind(relation) + relationMethod.create = relation.create.bind(relation); + relationMethod.build = relation.build.bind(relation); + return relationMethod; + } + }); +}; + +BelongsTo.prototype.create = function(targetModelData, cb) { + var modelTo = this.definition.modelTo; + var fk = this.definition.keyTo; + var pk = this.definition.keyFrom; + var modelInstance = this.modelInstance; + + modelTo.create(targetModelData, function(err, targetModel) { + if(!err) { + modelInstance[fk] = targetModel[pk]; + cb && cb(err, targetModel); + } else { + cb && cb(err); + } + }); +}; + +BelongsTo.prototype.build = function(targetModelData) { + var modelTo = this.definition.modelTo; + return new modelTo(targetModelData); +}; + +/** + * 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 + * + * @param refresh + * @param params + * @returns {*} + */ +BelongsTo.prototype.related = function (refresh, params) { + var modelTo = this.definition.modelTo; + var pk = this.definition.keyTo; + var fk = this.definition.keyFrom; + var modelInstance = this.modelInstance; + var relationName = this.definition.name; + + if (arguments.length === 1) { + params = refresh; + refresh = false; + } else if (arguments.length > 2) { + throw new Error('Method can\'t be called with more than two arguments'); + } + + var cachedValue; + if (!refresh && modelInstance.__cachedRelations + && (modelInstance.__cachedRelations[relationName] !== undefined)) { + cachedValue = modelInstance.__cachedRelations[relationName]; + } + if (params instanceof ModelBaseClass) { // acts as setter + modelInstance[fk] = params[pk]; + modelInstance.__cachedRelations[relationName] = params; + } else if (typeof params === 'function') { // acts as async getter + var cb = params; + if (cachedValue === undefined) { + modelTo.findById(modelInstance[fk], function (err, inst) { + if (err) { + return cb(err); + } + if (!inst) { + return cb(null, null); + } + // Check if the foreign key matches the primary key + if (inst[pk] === modelInstance[fk]) { + cb(null, inst); + } else { + cb(new Error('Permission denied')); + } + }); + return modelInstance[fk]; + } else { + cb(null, cachedValue); + return cachedValue; + } + } else if (params === undefined) { // acts as sync getter + return modelInstance[fk]; + } else { // setter + modelInstance[fk] = params; + delete modelInstance.__cachedRelations[relationName]; + } +}; + +/** + * 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: + * ``` + * User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'}); + * ``` + * + * @param {String|Object} modelTo Model object (or String name of model) to which you are creating the relationship. + * the relation + * @options {Object} params Configuration parameters; see below. + * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. + * @property {String} foreignKey Property name of foreign key field. + * @property {Object} model Model object + */ +RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, modelTo, params) { + params = params || {}; + var models = modelFrom.dataSource.modelBuilder.models; + + if ('string' === typeof modelTo) { + params.as = modelTo; + if (params.model) { + modelTo = params.model; + } else { + modelTo = lookupModel(models, i8n.singularize(modelTo)) || + modelTo; + } + if (typeof modelTo === 'string') { + throw new Error('Could not find "' + modelTo + '" relation for ' + modelFrom.modelName); + } + } + + if (!params.through) { + var name1 = modelFrom.modelName + modelTo.modelName; + var name2 = modelTo.modelName + modelFrom.modelName; + params.through = lookupModel(models, name1) || lookupModel(models, name2) || + modelFrom.dataSource.define(name1); + } + params.through.belongsTo(modelFrom); + params.through.belongsTo(modelTo); + + this.hasMany(modelFrom, modelTo, {as: params.as, through: params.through}); + +}; diff --git a/lib/relations.js b/lib/relations.js index c07b87d2..8b740359 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -1,49 +1,17 @@ /*! * Dependencies */ -var i8n = require('inflection'); -var defineScope = require('./scope.js').defineScope; -var ModelBaseClass = require('./model.js'); +var relation = require('./relation-definition'); +var RelationDefinition = relation.RelationDefinition; -module.exports = Relation; +module.exports = RelationMixin; /** - * Relations class. Use to define relationships between models. + * RelationMixin class. Use to define relationships between models. * - * @class Relation + * @class RelationMixin */ -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) { - return rel; - } - } -}; - -/*! - * 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]; - } - var lookupClassName = modelName.toLowerCase(); - for (var name in models) { - if (name.toLowerCase() === lookupClassName) { - return models[name]; - } - } +function RelationMixin() { } /** @@ -90,173 +58,14 @@ function lookupModel(models, modelName) { * }); * }); *``` - * @param {Object|String} anotherClass Model object (or String name of model) to which you are creating the relationship. + * @param {Object|String} modelTo Model object (or String name of model) to which you are creating the relationship. * @options {Object} parameters Configuration parameters; see below. * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. * @property {String} foreignKey Property name of foreign key field. * @property {Object} model Model object */ -Relation.hasMany = function hasMany(anotherClass, params) { - var thisClassName = this.modelName; - params = params || {}; - if (typeof anotherClass === 'string') { - params.as = anotherClass; - if (params.model) { - anotherClass = params.model; - } else { - var anotherClassName = i8n.singularize(anotherClass).toLowerCase(); - anotherClass = lookupModel(this.dataSource.modelBuilder.models, anotherClassName); - } - } - var methodName = params.as || i8n.camelize(anotherClass.pluralModelName, true); - var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true); - - var idName = this.dataSource.idName(this.modelName) || 'id'; - - this.relations[methodName] = { - type: 'hasMany', - keyFrom: idName, - keyTo: fk, - 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); - var scopeMethods = { - 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; - data = {}; - } - if ('function' !== typeof done) { - done = function () { - }; - } - 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); - }); - } else { - done(err, ac); - } - }); - }); - }; - - /*! - * 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 = {}; - query[fk] = this[idName]; - 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; - params.through.findOne({where: q}, function (err, d) { - if (err) { - return done(err); - } - if (!d) { - return done(); - } - 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 = {}; - filter.where[fk] = this[idName]; - if (params.through) { - filter.collect = i8n.camelize(anotherClass.modelName, true); - filter.include = filter.collect; - } - return filter; - }, scopeMethods); - - if (!params.through) { - // obviously, anotherClass should have attribute called `fk` - anotherClass.dataSource.defineForeignKey(anotherClass.modelName, fk, this.modelName); - } - - // Find the target model instance by id - function findById(id, cb) { - anotherClass.findById(id, function (err, inst) { - if (err) { - return cb(err); - } - 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 { - cb(new Error('Permission denied')); - } - }.bind(this)); - } - - // Destroy the target model instance by id - function destroyById(id, cb) { - var self = this; - anotherClass.findById(id, function (err, inst) { - if (err) { - return cb(err); - } - 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 { - cb(new Error('Permission denied')); - } - }); - } - +RelationMixin.hasMany = function hasMany(modelTo, params) { + RelationDefinition.hasMany(this, modelTo, params); }; /** @@ -304,151 +113,14 @@ Relation.hasMany = function hasMany(anotherClass, params) { * ``` * This optional parameter default value is false, so the related object will be loaded from cache if available. * - * @param {Class|String} anotherClass Model object (or String name of model) to which you are creating the relationship. + * @param {Class|String} modelTo Model object (or String name of model) to which you are creating the relationship. * @options {Object} params Configuration parameters; see below. * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. * @property {String} foreignKey Name of foreign key property. * */ -Relation.belongsTo = function (anotherClass, params) { - params = params || {}; - if ('string' === typeof anotherClass) { - params.as = anotherClass; - if (params.model) { - anotherClass = params.model; - } else { - var anotherClassName = anotherClass.toLowerCase(); - anotherClass = lookupModel(this.dataSource.modelBuilder.models, anotherClassName); - } - } - - var idName = this.dataSource.idName(anotherClass.modelName) || 'id'; - var methodName = params.as || i8n.camelize(anotherClass.modelName, true); - var fk = params.foreignKey || methodName + 'Id'; - - this.relations[methodName] = { - type: 'belongsTo', - keyFrom: fk, - keyTo: idName, - modelTo: anotherClass, - multiple: false - }; - - 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); - return; - } - anotherClass.findById(id, function (err, inst) { - if (err) { - return cb(err); - } - if (!inst) { - return cb(null, null); - } - // Check if the foreign key matches the primary key - if (inst[idName] === this[fk]) { - cb(null, inst); - } else { - cb(new Error('Permission denied')); - } - }.bind(this)); - }; - - // 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; - } else if (arguments.length > 2) { - throw new Error('Method can\'t be called with more than two arguments'); - } - var self = this; - var cachedValue; - if (!refresh && this.__cachedRelations && (this.__cachedRelations[methodName] !== undefined)) { - cachedValue = this.__cachedRelations[methodName]; - } - if (p instanceof ModelBaseClass) { // acts as setter - this[fk] = p[idName]; - this.__cachedRelations[methodName] = p; - } else if (typeof p === 'function') { // acts as async getter - if (typeof cachedValue === 'undefined') { - this.__finders__[methodName].apply(self, [this[fk], function (err, inst) { - if (!err) { - self.__cachedRelations[methodName] = inst; - } - p(err, inst); - }]); - return this[fk]; - } else { - p(null, cachedValue); - return cachedValue; - } - } else if (typeof p === 'undefined') { // acts as sync getter - return this[fk]; - } else { // setter - this[fk] = p; - delete this.__cachedRelations[methodName]; - } - }; - - // Define a property for the scope so that we have 'this' for the scoped methods - Object.defineProperty(this.prototype, methodName, { - enumerable: true, - configurable: true, - get: function () { - var fn = function() { - // Call the relation method on the declaring model instance - return relationMethod.apply(this, arguments); - } - // 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); - - fn._targetClass = anotherClass.modelName; - - return fn; - }}); - - // Wrap the property into a function for remoting - // so that it can be accessed as /api/// - // For example, /api/orders/1/customer - var fn = function() { - var f = this[methodName]; - f.apply(this, arguments); - }; - - 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}; - - this.prototype['__get__' + methodName] = fn; +RelationMixin.belongsTo = function (modelTo, params) { + RelationDefinition.belongsTo(this, modelTo, params); }; /** @@ -475,39 +147,13 @@ Relation.belongsTo = function (anotherClass, params) { * user.groups.remove(group, callback); * ``` * - * @param {String|Object} anotherClass Model object (or String name of model) to which you are creating the relationship. + * @param {String|Object} modelTo Model object (or String name of model) to which you are creating the relationship. * the relation * @options {Object} params Configuration parameters; see below. * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. * @property {String} foreignKey Property name of foreign key field. * @property {Object} model Model object */ -Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params) { - params = params || {}; - var models = this.dataSource.modelBuilder.models; - - if ('string' === typeof anotherClass) { - params.as = anotherClass; - if (params.model) { - anotherClass = params.model; - } else { - anotherClass = lookupModel(models, i8n.singularize(anotherClass).toLowerCase()) || - anotherClass; - } - if (typeof anotherClass === 'string') { - throw new Error('Could not find "' + anotherClass + '" relation for ' + this.modelName); - } - } - - if (!params.through) { - var name1 = this.modelName + anotherClass.modelName; - var name2 = anotherClass.modelName + this.modelName; - params.through = lookupModel(models, name1) || lookupModel(models, name2) || - this.dataSource.define(name1); - } - params.through.belongsTo(this); - params.through.belongsTo(anotherClass); - - this.hasMany(anotherClass, {as: params.as, through: params.through}); - +RelationMixin.hasAndBelongsToMany = function hasAndBelongsToMany(modelTo, params) { + RelationDefinition.hasAndBelongsToMany(this, modelTo, params); }; diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index d1d2b0a8..2d840fca 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -872,27 +872,33 @@ describe('Load models with relations', function () { var Account = ds.define('Account', {userId: Number, type: String}, {relations: {user: {type: 'belongsTo', model: 'User'}}}); assert(Post.relations['user']); - assert.deepEqual(Post.relations['user'], { + assert.deepEqual(Post.relations['user'].toJSON(), { + name: 'user', type: 'belongsTo', + modelFrom: 'Post', keyFrom: 'userId', + modelTo: 'User', keyTo: 'id', - modelTo: User, multiple: false }); assert(User.relations['posts']); - assert.deepEqual(User.relations['posts'], { + assert.deepEqual(User.relations['posts'].toJSON(), { + name: 'posts', type: 'hasMany', + modelFrom: 'User', keyFrom: 'id', + modelTo: 'Post', keyTo: 'userId', - modelTo: Post, multiple: true }); assert(User.relations['accounts']); - assert.deepEqual(User.relations['accounts'], { + assert.deepEqual(User.relations['accounts'].toJSON(), { + name: 'accounts', type: 'hasMany', + modelFrom: 'User', keyFrom: 'id', + modelTo: 'Account', keyTo: 'userId', - modelTo: Account, multiple: true }); From 34c1998f0406ec488edfded89b9796e77d56670e Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 16 Jun 2014 00:36:12 -0700 Subject: [PATCH 37/57] Fix the hasMany through connection --- lib/relation-definition.js | 55 +++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index b3dfe835..09849d12 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -203,14 +203,15 @@ util.inherits(HasOne, Relation); * @param {*} foreignKey The foreign key * @returns {Object} The relation object */ -function findBelongsTo(model, foreignKey) { - var relations = model.relations; +function findBelongsTo(modelFrom, modelTo, keyTo) { + var relations = modelFrom.relations; var keys = Object.keys(relations); for (var k = 0; k < keys.length; k++) { var rel = relations[keys[k]]; if (rel.type === RelationTypes.belongsTo && - rel.keyFrom === foreignKey) { - return keys[k]; + rel.modelTo === modelTo && + rel.keyTo === keyTo) { + return rel.keyFrom; } } return null; @@ -378,25 +379,30 @@ HasMany.prototype.destroyById = function (id, cb) { // 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 HasManyThrough.prototype.create = function create(data, done) { - var modelTo = this.definition.modelTo; - var modelThrough = this.definition.modelThrough; - var fk = this.definition.keyTo; - var fk2 = this.definition.keyThrough; + var definition = this.definition; + var modelTo = definition.modelTo; + var modelThrough = definition.modelThrough; if (typeof data === 'function' && !done) { done = data; data = {}; } - var self = this.modelInstance; + var modelInstance = this.modelInstance; + // First create the target model modelTo.create(data, function (err, ac) { if (err) { return done && done(err, ac); } + // The primary key for the target model + var pk2 = modelTo.dataSource.idName(modelTo.modelName) || 'id'; + var fk1 = findBelongsTo(modelThrough, definition.modelFrom, + definition.keyFrom); + var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2); var d = {}; - d[findBelongsTo(modelThrough, fk)] = self; - d[findBelongsTo(modelThrough, fk2)] = ac; + d[fk1] = modelInstance[definition.keyFrom]; + d[fk2] = ac[pk2]; // Then create the through model modelThrough.create(d, function (e) { if (e) { @@ -416,17 +422,28 @@ HasManyThrough.prototype.create = function create(data, done) { * @param {Object|ID} acInst The actual instance or id value */ HasManyThrough.prototype.add = function (acInst, done) { - var modelThrough = this.definition.modelThrough; - var fk = this.definition.keyTo; - var fk2 = this.definition.keyThrough; - var pk = this.definition.keyFrom; + var definition = this.definition; + var modelThrough = definition.modelThrough; + var modelTo = definition.modelTo; + var pk1 = definition.keyFrom; var data = {}; var query = {}; - query[fk] = this[pk]; - data[findBelongsTo(modelThrough, fk)] = this.modelInstance; - query[fk2] = acInst[pk] || acInst; - data[findBelongsTo(modelThrough, fk2)] = acInst; + + var fk1 = findBelongsTo(modelThrough, definition.modelFrom, + definition.keyFrom); + + // The primary key for the target model + var pk2 = modelTo.dataSource.idName(modelTo.modelName) || 'id'; + + var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2); + + query[fk1] = this.modelInstance[pk1]; + query[fk2] = acInst[pk2] || acInst; + + data[fk1] = this.modelInstance[pk1]; + data[fk2] = acInst[pk2] || acInst; + // Create an instance of the through model modelThrough.findOrCreate({where: query}, data, done); }; From 2db43c58e5b0cb4cc3a70b9a13eacebb90139987 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 16 Jun 2014 01:17:37 -0700 Subject: [PATCH 38/57] Add support for hasOne --- lib/relation-definition.js | 139 +++++++++++++++++++++++++++++++++++++ lib/relations.js | 4 ++ test/relations.test.js | 39 ++++++++++- 3 files changed, 181 insertions(+), 1 deletion(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 09849d12..ae030143 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -656,3 +656,142 @@ RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, this.hasMany(modelFrom, modelTo, {as: params.as, through: params.through}); }; + +/** + * HasOne + * @param modelFrom + * @param modelTo + * @param params + */ +RelationDefinition.hasOne = function (modelFrom, modelTo, params) { + params = params || {}; + if ('string' === typeof modelTo) { + params.as = modelTo; + if (params.model) { + modelTo = params.model; + } else { + var modelToName = modelTo.toLowerCase(); + modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName); + } + } + + var pk = modelFrom.dataSource.idName(modelTo.modelName) || 'id'; + var relationName = params.as || i8n.camelize(modelTo.modelName, true); + + var fk = params.foreignKey || i8n.camelize(modelFrom.modelName + '_id', true); + + var relationDef = modelFrom.relations[relationName] = new RelationDefinition({ + name: relationName, + type: RelationTypes.hasOne, + modelFrom: modelFrom, + keyFrom: pk, + keyTo: fk, + modelTo: modelTo + }); + + modelFrom.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName); + + // Define a property for the scope so that we have 'this' for the scoped methods + Object.defineProperty(modelFrom.prototype, relationName, { + enumerable: true, + configurable: true, + get: function() { + var relation = new HasOne(relationDef, this); + var relationMethod = relation.related.bind(relation) + relationMethod.create = relation.create.bind(relation); + relationMethod.build = relation.build.bind(relation); + return relationMethod; + } + }); +}; + +HasOne.prototype.create = function(targetModelData, cb) { + var modelTo = this.definition.modelTo; + var fk = this.definition.keyTo; + var pk = this.definition.keyFrom; + var modelInstance = this.modelInstance; + + targetModelData = targetModelData || {}; + targetModelData[fk] = modelInstance[pk]; + modelTo.create(targetModelData, function(err, targetModel) { + if(!err) { + cb && cb(err, targetModel); + } else { + cb && cb(err); + } + }); +}; + +HasOne.prototype.build = function(targetModelData) { + var modelTo = this.definition.modelTo; + var pk = this.definition.keyFrom; + var fk = this.definition.keyTo; + targetModelData = targetModelData || {}; + targetModelData[fk] = this.modelInstance[pk]; + return new modelTo(targetModelData); +}; + +/** + * Define the method for the hasOne 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 + * + * @param refresh + * @param params + * @returns {*} + */ +HasOne.prototype.related = function (refresh, params) { + var modelTo = this.definition.modelTo; + var fk = this.definition.keyTo; + var pk = this.definition.keyFrom; + var modelInstance = this.modelInstance; + var relationName = this.definition.name; + + if (arguments.length === 1) { + params = refresh; + refresh = false; + } else if (arguments.length > 2) { + throw new Error('Method can\'t be called with more than two arguments'); + } + + var cachedValue; + if (!refresh && modelInstance.__cachedRelations + && (modelInstance.__cachedRelations[relationName] !== undefined)) { + cachedValue = modelInstance.__cachedRelations[relationName]; + } + if (params instanceof ModelBaseClass) { // acts as setter + params[fk] = modelInstance[pk]; + modelInstance.__cachedRelations[relationName] = params; + } else if (typeof params === 'function') { // acts as async getter + var cb = params; + if (cachedValue === undefined) { + var query = {where: {}}; + query.where[fk] = modelInstance[pk]; + modelTo.findOne(query, function (err, inst) { + if (err) { + return cb(err); + } + if (!inst) { + return cb(null, null); + } + // Check if the foreign key matches the primary key + if (inst[fk] === modelInstance[pk]) { + cb(null, inst); + } else { + cb(new Error('Permission denied')); + } + }); + return modelInstance[pk]; + } else { + cb(null, cachedValue); + return cachedValue; + } + } else if (params === undefined) { // acts as sync getter + return modelInstance[pk]; + } else { // setter + params[fk] = modelInstance[pk]; + delete modelInstance.__cachedRelations[relationName]; + } +}; diff --git a/lib/relations.js b/lib/relations.js index 8b740359..068bce7f 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -157,3 +157,7 @@ RelationMixin.belongsTo = function (modelTo, params) { RelationMixin.hasAndBelongsToMany = function hasAndBelongsToMany(modelTo, params) { RelationDefinition.hasAndBelongsToMany(this, modelTo, params); }; + +RelationMixin.hasOne = function hasMany(modelTo, params) { + RelationDefinition.hasOne(this, modelTo, params); +}; diff --git a/test/relations.test.js b/test/relations.test.js index 7bc3a1b3..d0cd0ea9 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -1,7 +1,7 @@ // This test written in mocha+should.js var should = require('./init.js'); -var db, Book, Chapter, Author, Reader; +var db, Book, Chapter, Author, Reader, Publisher; describe('relations', function () { before(function (done) { @@ -180,6 +180,43 @@ describe('relations', function () { }); + describe('hasOne', function () { + var Supplier, Account; + + before(function () { + db = getSchema(); + Supplier = db.define('Supplier', {name: String}); + Account = db.define('Account', {accountNo: String}); + }); + + it('can be declared using hasOne method', function () { + Supplier.hasOne(Account); + Object.keys((new Account()).toObject()).should.include('supplierId'); + (new Supplier()).account.should.be.an.instanceOf(Function); + + }); + + it('can be used to query data', function (done) { + // Supplier.hasOne(Account); + db.automigrate(function () { + Supplier.create({name: 'Supplier 1'}, function (e, supplier) { + should.not.exist(e); + should.exist(supplier); + supplier.account.create({accountNo: 'a01'}, function (err, account) { + supplier.account(function (e, act) { + should.not.exist(e); + should.exist(act); + act.should.be.an.instanceOf(Account); + supplier.account().should.equal(act.id); + done(); + }); + }); + }); + }); + }); + + }); + describe('hasAndBelongsToMany', function () { var Article, Tag, ArticleTag; it('can be declared', function (done) { From 046816191df587e686ebd5e6c0c7e330159c643e Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 16 Jun 2014 10:50:42 -0700 Subject: [PATCH 39/57] Clean up scope implementation --- lib/scope.js | 208 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 132 insertions(+), 76 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index ac14efc7..185ad0d3 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -6,6 +6,52 @@ var defineCachedRelations = utils.defineCachedRelations; */ exports.defineScope = defineScope; +function ScopeDefinition(definition) { + this.sourceModel = definition.sourceModel; + this.targetModel = definition.targetModel || definition.sourceModel; + this.name = definition.name; + this.params = definition.params; + this.methods = definition.methods; +} + +ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) { + var name = this.name; + var self = receiver; + + var actualCond = {}; + var actualRefresh = false; + var saveOnCache = true; + if (arguments.length === 3) { + cb = condOrRefresh; + } else if (arguments.length === 4) { + if (typeof condOrRefresh === 'boolean') { + actualRefresh = condOrRefresh; + } else { + actualCond = condOrRefresh; + actualRefresh = true; + saveOnCache = false; + } + } else { + throw new Error('Method can be only called with one or two arguments'); + } + + if (!self.__cachedRelations || self.__cachedRelations[name] === undefined + || actualRefresh) { + // It either doesn't hit the cache or refresh is required + var params = mergeParams(actualCond, scopeParams); + return this.targetModel.find(params, function (err, data) { + if (!err && saveOnCache) { + defineCachedRelations(self); + self.__cachedRelations[name] = data; + } + cb(err, data); + }); + } else { + // Return from cache + cb(null, self.__cachedRelations[name]); + } +} + /** * Define a scope to the class * @param {Model} cls The class where the scope method is added @@ -22,7 +68,7 @@ function defineScope(cls, targetClass, name, params, methods) { cls._scopeMeta = {}; } - // only makes sence to add scope in meta if base and target classes + // only makes sense to add scope in meta if base and target classes // are same if (cls === targetClass) { cls._scopeMeta[name] = params; @@ -32,6 +78,14 @@ function defineScope(cls, targetClass, name, params, methods) { } } + var definition = new ScopeDefinition({ + sourceModel: cls, + targetModel: targetClass, + name: name, + params: params, + methods: methods + }); + // Define a property for the scope Object.defineProperty(cls, name, { enumerable: false, @@ -49,42 +103,18 @@ function defineScope(cls, targetClass, name, params, methods) { */ get: function () { var self = this; - var f = function caller(condOrRefresh, cb) { - var actualCond = {}; - var actualRefresh = false; - var saveOnCache = true; - if (arguments.length === 1) { - cb = condOrRefresh; - } else if (arguments.length === 2) { - if (typeof condOrRefresh === 'boolean') { - actualRefresh = condOrRefresh; - } else { - actualCond = condOrRefresh; - actualRefresh = true; - saveOnCache = false; - } + var f = function(condOrRefresh, cb) { + if(arguments.length === 1) { + definition.related(self, f._scope, condOrRefresh); } else { - throw new Error('Method can be only called with one or two arguments'); - } - - 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) { - defineCachedRelations(self); - self.__cachedRelations[name] = data; - } - cb(err, data); - }); - } else { - // Return from cache - cb(null, self.__cachedRelations[name]); + definition.related(self, f._scope, condOrRefresh, cb); } }; - f._scope = typeof params === 'function' ? params.call(this) : params; - f._targetClass = targetClass.modelName; + + f._scope = typeof definition.params === 'function' ? + definition.params.call(self) : definition.params; + + f._targetClass = definition.targetModel.modelName; if (f._scope.collect) { f._targetClass = i8n.capitalize(f._scope.collect); } @@ -92,11 +122,14 @@ function defineScope(cls, targetClass, name, params, methods) { f.build = build; f.create = create; f.destroyAll = destroyAll; - for (var i in methods) { - f[i] = methods[i].bind(this); + for (var i in definition.methods) { + f[i] = definition.methods[i].bind(self); } - // define sub-scopes + // Define scope-chaining, such as + // Station.scope('active', {where: {isActive: true}}); + // Station.scope('subway', {where: {isUndeground: true}}); + // Station.active.subway(cb); Object.keys(targetClass._scopeMeta).forEach(function (name) { Object.defineProperty(f, name, { enumerable: false, @@ -105,7 +138,7 @@ function defineScope(cls, targetClass, name, params, methods) { return f; } }); - }.bind(this)); + }.bind(self)); return f; } }); @@ -152,7 +185,13 @@ function defineScope(cls, targetClass, name, params, methods) { // and it should have create/build methods with binded thisModelNameId param function build(data) { - return new targetClass(mergeParams(this._scope, {where: data || {}}).where); + data = data || {}; + var params = mergeParams(this._scope, {where: data}).where; + delete params['and']; + delete params['or']; + delete params['nor']; + + return new targetClass(params); } function create(data, cb) { @@ -187,53 +226,70 @@ function defineScope(cls, targetClass, name, params, methods) { } }); } - - function mergeParams(base, update) { - base = base || {}; - if (update.where) { - base.where = merge(base.where, update.where); - } - if (update.include) { - base.include = update.include; - } - if (update.collect) { - base.collect = update.collect; - } - - // overwrite order - if (update.order) { - base.order = update.order; - } - - if(update.limit !== undefined) { - base.limit = update.limit; - } - if(update.skip !== undefined) { - base.skip = update.skip; - } - if(update.offset !== undefined) { - base.offset = update.offset; - } - if(update.fields !== undefined) { - base.fields = update.fields; - } - return base; - - } } -/** +/*! * Merge `base` and `update` params * @param {Object} base - base object (updating this object) * @param {Object} update - object with new data to update base * @returns {Object} `base` + * @private */ -function merge(base, update) { +function mergeWhere(base, update) { base = base || {}; if (update) { - Object.keys(update).forEach(function (key) { + var keys = Object.keys(update); + for (var k = 0; k < keys.length; k++) { + var key = keys[k]; base[key] = update[key]; - }); + } + } + return base; +} + +/*! + * Merge query parameters + * @param base + * @param update + * @returns {*|{}} + * @private + */ +function mergeParams(base, update) { + if (!update) { + return; + } + base = base || {}; + if (update.where) { + base.where = mergeWhere(base.where, update.where); + } + + // Overwrite inclusion + if (update.include) { + base.include = update.include; + } + if (update.collect) { + base.collect = update.collect; + } + + // overwrite order + if (update.order) { + base.order = update.order; + } + + // overwrite pagination + if (update.limit !== undefined) { + base.limit = update.limit; + } + if (update.skip !== undefined) { + base.skip = update.skip; + } + if (update.offset !== undefined) { + base.offset = update.offset; + } + + // Overwrite fields + if (update.fields !== undefined) { + base.fields = update.fields; } return base; } From e2ab9ccc931aaa7d2512c054afefe46d1a44eb6b Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 16 Jun 2014 13:46:17 -0700 Subject: [PATCH 40/57] Adding back the remoting metadata --- lib/relation-definition.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index ae030143..38c52c93 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -520,12 +520,30 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { configurable: true, get: function() { var relation = new BelongsTo(relationDef, this); - var relationMethod = relation.related.bind(relation) + var relationMethod = relation.related.bind(relation); relationMethod.create = relation.create.bind(relation); relationMethod.build = relation.build.bind(relation); + relationMethod._targetClass = relationDef.modelTo.modelName; return relationMethod; } }); + + // Wrap the property into a function for remoting + // so that it can be accessed as /api/// + // For example, /api/orders/1/customer + var fn = function() { + var f = this[relationName]; + f.apply(this, arguments); + }; + + fn.shared = true; + fn.http = {verb: 'get', path: '/' + relationName}; + fn.accepts = {arg: 'refresh', type: 'boolean', http: {source: 'query'}}; + fn.description = 'Fetches belongsTo relation ' + relationName; + fn.returns = {arg: relationName, type: 'object', root: true}; + + modelFrom.prototype['__get__' + relationName] = fn; + }; BelongsTo.prototype.create = function(targetModelData, cb) { From 4b9d98c6ce2ec4bf32ac9d0e1b6ef6589aac8d6b Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 17 Jun 2014 09:07:55 -0700 Subject: [PATCH 41/57] Allows skip or offset See https://github.com/strongloop/loopback/issues/336 --- lib/connectors/memory.js | 6 +++--- lib/dao.js | 2 +- test/basic-querying.test.js | 15 +++++++++++++-- test/loopback-dl.test.js | 4 ++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index ab54f959..b0c81ca1 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -306,9 +306,9 @@ Memory.prototype.all = function all(model, filter, callback) { } // limit/skip - filter.skip = filter.skip || 0; - filter.limit = filter.limit || nodes.length; - nodes = nodes.slice(filter.skip, filter.skip + filter.limit); + var skip = filter.skip || filter.offset || 0; + var limit = filter.limit || nodes.length; + nodes = nodes.slice(skip, skip + limit); } process.nextTick(function () { diff --git a/lib/dao.js b/lib/dao.js index d4bd40de..c68a114a 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -432,7 +432,7 @@ DataAccessObject._normalize = function (filter) { } filter.limit = limit; filter.offset = offset; - delete filter.skip; + filter.skip = offset; } // normalize fields as array of included property names diff --git a/test/basic-querying.test.js b/test/basic-querying.test.js index eaccb348..cfb51be8 100644 --- a/test/basic-querying.test.js +++ b/test/basic-querying.test.js @@ -72,15 +72,26 @@ describe('basic-querying', function () { }); }); - it('should query offset collection with limit', function (done) { - User.find({skip: 1, limit: 4}, function (err, users) { + it('should query collection with skip & limit', function (done) { + User.find({skip: 1, limit: 4, order: 'seq'}, function (err, users) { should.exists(users); should.not.exists(err); + users[0].seq.should.be.eql(1); users.should.have.lengthOf(4); done(); }); }); + it('should query collection with offset & limit', function (done) { + User.find({offset: 2, limit: 3}, function (err, users) { + should.exists(users); + should.not.exists(err); + users[0].seq.should.be.eql(2); + users.should.have.lengthOf(3); + done(); + }); + }); + it('should query filtered collection', function (done) { User.find({where: {role: 'lead'}}, function (err, users) { should.exists(users); diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index d1d2b0a8..7b5eb2ab 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -1198,12 +1198,12 @@ describe('DataAccessObject', function () { it('should normalize limit/offset/skip', function () { filter = model._normalize({limit: '10', skip: 5}); - assert.deepEqual(filter, {limit: 10, offset: 5}); + assert.deepEqual(filter, {limit: 10, offset: 5, skip: 5}); }); it('should set the default value for limit', function () { filter = model._normalize({skip: 5}); - assert.deepEqual(filter, {limit: 100, offset: 5}); + assert.deepEqual(filter, {limit: 100, offset: 5, skip: 5}); }); it('should skip GeoPoint', function () { From 36af31bf1c2bdc8a1c9f7915fcc7c88176f43c97 Mon Sep 17 00:00:00 2001 From: crandmck Date: Tue, 17 Jun 2014 13:04:30 -0700 Subject: [PATCH 42/57] Add ModelBuilder class --- docs.json | 1 + 1 file changed, 1 insertion(+) diff --git a/docs.json b/docs.json index a7da7f44..798d8aa8 100644 --- a/docs.json +++ b/docs.json @@ -4,6 +4,7 @@ "lib/geo.js", "lib/dao.js", "lib/include.js", + "lib/model-builder.js", "lib/relations.js", "lib/validations.js" ], From 907163949e39ec40d54245bf7fc223d62ed33de1 Mon Sep 17 00:00:00 2001 From: crandmck Date: Tue, 17 Jun 2014 13:18:18 -0700 Subject: [PATCH 43/57] Initial JSDoc cleanup --- lib/model-builder.js | 52 +++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/lib/model-builder.js b/lib/model-builder.js index 786978ae..c08c1dc6 100644 --- a/lib/model-builder.js +++ b/lib/model-builder.js @@ -28,18 +28,14 @@ var slice = Array.prototype.slice; /** * ModelBuilder - A builder to define data models. - * + * + * @property {Object} definitions Definitions of the models. + * @property {Object} models Model constructors * @class */ function ModelBuilder() { // create blank models pool - /** - * @property {Object} models Model constructors - */ this.models = {}; - /** - * @property {Object} definitions Definitions of the models - */ this.definitions = {}; } @@ -276,7 +272,26 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett dataSource.attach(this); }; - // A function to extend the model + /** Extend the model with the specified model, properties, and other settings. + * For example, to extend an existing model, for example, a built-in model: + * + * ```js + * var Customer = User.extend('customer', { + * accountId: String, + * vip: Boolean + * }); + * ``` + * + * To extend the base model, essentially creating a new model: + * ```js + * var user = loopback.Model.extend('user', properties, options); + * ``` + * + * @param {String} className Name of the new model being defined. + * @options {Object} properties Properties to define for the model, added to properties of model being extended. + * @options {Object} settings Model settings, such as relations and acls. + * + */ ModelClass.extend = function (className, subclassProperties, subclassSettings) { var properties = ModelClass.definition.properties; var settings = ModelClass.definition.settings; @@ -433,17 +448,20 @@ ModelBuilder.prototype.defineProperty = function (model, propertyName, propertyD }; /** - * Extend existing model with bunch of properties + * Extend existing model with specified properties * * Example: - * Instead of doing amending the content model with competition attributes like this: + * Instead of extending a model with attributes like this (for example): * * ```js - * db.defineProperty('Content', 'competitionType', { type: String }); - * db.defineProperty('Content', 'expiryDate', { type: Date, index: true }); - * db.defineProperty('Content', 'isExpired', { type: Boolean, index: true }); + * db.defineProperty('Content', 'competitionType', + * { type: String }); + * db.defineProperty('Content', 'expiryDate', + * { type: Date, index: true }); + * db.defineProperty('Content', 'isExpired', + * { type: Boolean, index: true }); *``` - * The extendModel() method enables you to extend the content model with competition attributes. + * This method enables you to extend a model as follows (for example): * ```js * db.extendModel('Content', { * competitionType: String, @@ -453,7 +471,11 @@ ModelBuilder.prototype.defineProperty = function (model, propertyName, propertyD *``` * * @param {String} model Name of model - * @param {Object} props Hash of properties + * @options {Object} properties JSON object specifying properties. Each property is a key whos value is + * either the [type](http://docs.strongloop.com/display/DOC/LDL+data+types) or `propertyName: {options}` + * where the options are described below. + * @property {String} type Datatype of property: Must be an [LDL type](http://docs.strongloop.com/display/DOC/LDL+data+types). + * @property {Boolean} index True if the property is an index; false otherwise. */ ModelBuilder.prototype.extendModel = function (model, props) { var t = this; From 4ff6e0d707663fac50c5a5b00651ab36d2883651 Mon Sep 17 00:00:00 2001 From: crandmck Date: Tue, 17 Jun 2014 13:21:21 -0700 Subject: [PATCH 44/57] Minor JSDoc cleanup --- lib/model-builder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model-builder.js b/lib/model-builder.js index c08c1dc6..f0b126c6 100644 --- a/lib/model-builder.js +++ b/lib/model-builder.js @@ -57,7 +57,7 @@ function isModelClass(cls) { * * @param {String} name The model name * @param {Boolean} forceCreate Whether the create a stub for the given name if a model doesn't exist. - * Returns {*} The model class + * @returns {*} The model class */ ModelBuilder.prototype.getModel = function (name, forceCreate) { var model = this.models[name]; From ad3af829233721bd7fb9151a6078e54c157992c3 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 17 Jun 2014 16:30:02 -0700 Subject: [PATCH 45/57] Add support for updating multiple instances with query --- lib/connectors/memory.js | 23 +++++++++++++ lib/dao.js | 65 +++++++++++++++++++++++++++++++++++++ test/basic-querying.test.js | 34 +++++++++++++++++++ 3 files changed, 122 insertions(+) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index ab54f959..de479c83 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -476,6 +476,29 @@ Memory.prototype.count = function count(model, callback, where) { }); }; +Memory.prototype.update = + Memory.prototype.updateAll = function updateAll(model, where, data, cb) { + var self = this; + var cache = this.cache[model]; + var filter = null; + where = where || {}; + filter = applyFilter({where: where}); + + var ids = Object.keys(cache); + async.each(ids, function (id, done) { + var inst = self.fromDb(model, cache[id]); + if (!filter || filter(inst)) { + self.updateAttributes(model, id, data, done); + } else { + process.nextTick(done); + } + }, function (err) { + if (!err) { + self.saveToFile(null, cb); + } + }); + }; + Memory.prototype.updateAttributes = function updateAttributes(model, id, data, cb) { if (!id) { var err = new Error('You must provide an id when updating attributes!'); diff --git a/lib/dao.js b/lib/dao.js index d4bd40de..5f8d0c77 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -19,6 +19,7 @@ var utils = require('./utils'); var fieldsToArray = utils.fieldsToArray; var removeUndefined = utils.removeUndefined; var util = require('util'); +var assert = require('assert'); /** * Base class for all persistent objects. @@ -961,6 +962,70 @@ DataAccessObject.prototype.save = function (options, callback) { } }; +/** + * Update multiple instances that match the where clause + * + * Example: + * + *```js + * Employee.update({managerId: 'x001'}, {managerId: 'x002'}, function(err) { + * ... + * }); + * ``` + * + * @param {Object} [where] Search conditions (optional) + * @param {Object} data Changes to be made + * @param {Function} cb Callback, called with (err, count) + */ +DataAccessObject.update = +DataAccessObject.updateAll = function (where, data, cb) { + if (stillConnecting(this.getDataSource(), this, arguments)) return; + + if (arguments.length === 1) { + // update(data); + data = where; + where = null; + cb = null; + } else if (arguments.length === 2) { + if (typeof data === 'function') { + // update(data, cb); + cb = data; + data = where; + where = null; + } else { + // update(where, data); + cb = null; + } + } + + assert(typeof where === 'object', 'The where argument should be an object'); + assert(typeof data === 'object', 'The data argument should be an object'); + assert(cb === null || typeof cb === 'function', 'The data argument should be an object'); + + try { + where = removeUndefined(where); + where = this._coerce(where); + } catch (err) { + return process.nextTick(function () { + cb && cb(err); + }); + } + var connector = this.getDataSource().connector; + connector.update(this.modelName, where, data, cb); +}; + +// updateAll ~ remoting attributes +setRemoting(DataAccessObject.updateAll, { + description: 'Update instances of the model matched by where from the data source', + accepts: [ + {arg: 'where', type: 'object', http: {source: 'query'}, + description: 'Criteria to match model instances'}, + {arg: 'data', type: 'object', http: {source: 'body'}, + description: 'An object of model property name/value pairs'}, + ], + http: {verb: 'post', path: '/update'} +}); + DataAccessObject.prototype.isNewRecord = function () { return !getIdValue(this.constructor, this); }; diff --git a/test/basic-querying.test.js b/test/basic-querying.test.js index eaccb348..12ac4dcf 100644 --- a/test/basic-querying.test.js +++ b/test/basic-querying.test.js @@ -541,6 +541,40 @@ describe('basic-querying', function () { }); + describe('updateAll ', function () { + + beforeEach(seed); + + it('should only update instances that satisfy the where condition', function (done) { + User.update({name: 'John Lennon'}, {name: 'John Smith'}, function () { + User.find({where: {name: 'John Lennon'}}, function (err, data) { + should.not.exist(err); + data.length.should.equal(0); + User.find({where: {name: 'John Smith'}}, function (err, data) { + should.not.exist(err); + data.length.should.equal(1); + done(); + }); + }); + }); + }); + + it('should update all instances without where', function (done) { + User.update({name: 'John Smith'}, function () { + User.find({where: {name: 'John Lennon'}}, function (err, data) { + should.not.exist(err); + data.length.should.equal(0); + User.find({where: {name: 'John Smith'}}, function (err, data) { + should.not.exist(err); + data.length.should.equal(6); + done(); + }); + }); + }); + }); + + }); + }); function seed(done) { From a487eb57cd47eb4bef604c1d11d006601dcfe978 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 17 Jun 2014 23:19:28 -0700 Subject: [PATCH 46/57] Add like/nlike support for memory connector --- lib/connectors/memory.js | 22 +++++++++- test/memory.test.js | 93 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index de479c83..e86d7d27 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -363,8 +363,12 @@ function applyFilter(filter) { return pass; } + function replaceAll(string, find, replace) { + return string.replace(new RegExp(find, 'g'), replace); + } + function test(example, value) { - if (typeof value === 'string' && example && example.constructor.name === 'RegExp') { + if (typeof value === 'string' && (example instanceof RegExp)) { return value.match(example); } if (example === undefined || value === undefined) { @@ -386,6 +390,22 @@ function applyFilter(filter) { return false; } + if (example.like || example.nlike) { + + var like = example.like || example.nlike; + if (typeof like === 'string') { + like = replaceAll(like, '%', '.*'); + like = replaceAll(like, '_', '.'); + } + if (example.like) { + return !!new RegExp(like).test(value); + } + + if (example.nlike) { + return !new RegExp(like).test(value); + } + } + if (testInEquality(example, value)) { return true; } diff --git a/test/memory.test.js b/test/memory.test.js index 4c613270..b98462a1 100644 --- a/test/memory.test.js +++ b/test/memory.test.js @@ -4,6 +4,7 @@ var path = require('path'); var fs = require('fs'); var assert = require('assert'); var async = require('async'); +var should = require('./init.js'); describe('Memory connector', function () { var file = path.join(__dirname, 'memory.json'); @@ -91,5 +92,97 @@ describe('Memory connector', function () { }); }); + + describe('Query for memory connector', function () { + var ds = new DataSource({ + connector: 'memory' + }); + + var User = ds.define('User', { + seq: {type: Number, index: true}, + name: {type: String, index: true, sort: true}, + email: {type: String, index: true}, + birthday: {type: Date, index: true}, + role: {type: String, index: true}, + order: {type: Number, index: true, sort: true}, + vip: {type: Boolean} + }); + + before(seed); + it('should allow to find using like', function (done) { + User.find({where: {name: {like: '%St%'}}}, function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 2); + done(); + }); + }); + + it('should support like for no match', function (done) { + User.find({where: {name: {like: 'M%XY'}}}, function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + + it('should allow to find using nlike', function (done) { + User.find({where: {name: {nlike: '%St%'}}}, function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 4); + done(); + }); + }); + + it('should support nlike for no match', function (done) { + User.find({where: {name: {nlike: 'M%XY'}}}, function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 6); + done(); + }); + }); + + function seed(done) { + var count = 0; + var beatles = [ + { + seq: 0, + name: 'John Lennon', + email: 'john@b3atl3s.co.uk', + role: 'lead', + birthday: new Date('1980-12-08'), + order: 2, + vip: true + }, + { + seq: 1, + name: 'Paul McCartney', + email: 'paul@b3atl3s.co.uk', + role: 'lead', + birthday: new Date('1942-06-18'), + order: 1, + vip: true + }, + {seq: 2, name: 'George Harrison', order: 5, vip: false}, + {seq: 3, name: 'Ringo Starr', order: 6, vip: false}, + {seq: 4, name: 'Pete Best', order: 4}, + {seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true} + ]; + User.destroyAll(function () { + beatles.forEach(function (beatle) { + User.create(beatle, ok); + }); + }); + + function ok() { + if (++count === beatles.length) { + done(); + } + } + } + + }); + }); + + From b3b29d731382eeaec2763c4052f8a988f6168bb4 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 18 Jun 2014 12:37:49 -0700 Subject: [PATCH 47/57] Enhance the wildcard to regexp conversion --- lib/connectors/memory.js | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index e86d7d27..06c65772 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -363,8 +363,34 @@ function applyFilter(filter) { return pass; } - function replaceAll(string, find, replace) { - return string.replace(new RegExp(find, 'g'), replace); + function toRegExp(pattern) { + if (pattern instanceof RegExp) { + return pattern; + } + var regex = ''; + pattern = pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); + for (var i = 0, n = pattern.length; i < n; i++) { + var char = pattern.charAt(i); + if (char === '\\') { + i++; // Skip to next char + if (i < n) { + regex += pattern.charAt(i); + } + continue; + } else if (char === '%') { + regex += '.*'; + } else if (char === '_') { + regex += '.'; + } else if (char === '.') { + regex += '\\.'; + } else if (char === '*') { + regex += '\\*'; + } + else { + regex += char; + } + } + return regex; } function test(example, value) { @@ -394,8 +420,7 @@ function applyFilter(filter) { var like = example.like || example.nlike; if (typeof like === 'string') { - like = replaceAll(like, '%', '.*'); - like = replaceAll(like, '_', '.'); + like = toRegExp(like); } if (example.like) { return !!new RegExp(like).test(value); From 893768e8956544f701e79d45152e7262415d90f9 Mon Sep 17 00:00:00 2001 From: crandmck Date: Wed, 18 Jun 2014 16:42:00 -0700 Subject: [PATCH 48/57] Add hooks and include mixins --- lib/hooks.js | 7 ++++--- lib/include.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/hooks.js b/lib/hooks.js index 815d1aa2..9288f55b 100644 --- a/lib/hooks.js +++ b/lib/hooks.js @@ -1,10 +1,11 @@ -/** +/*! * Module exports */ module.exports = Hookable; -/** - * Hooks mixins +/* + * Hooks object. + * @class Hookable */ function Hookable() { diff --git a/lib/include.js b/lib/include.js index c459451b..f276b3d9 100644 --- a/lib/include.js +++ b/lib/include.js @@ -36,7 +36,7 @@ function Inclusion() { *``` Passport.include(passports, {owner: [{posts: 'images'}, 'passports']}); * * @param {Array} objects Array of instances - * @param {String}, {Object} or {Array} include Which relations to load. + * @param {String|Object|Array} include Which relations to load. * @param {Function} cb Callback called when relations are loaded * */ From 997f1b6fbe4b4e31b1fc1dd216dca6b5956ae474 Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Wed, 18 Jun 2014 16:44:11 -0700 Subject: [PATCH 49/57] Add hooks remove dao --- docs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs.json b/docs.json index 798d8aa8..0cc0d640 100644 --- a/docs.json +++ b/docs.json @@ -2,7 +2,7 @@ "content": [ "lib/datasource.js", "lib/geo.js", - "lib/dao.js", + "lib/hooks.js", "lib/include.js", "lib/model-builder.js", "lib/relations.js", From 888d15ce1cf052a511fbbf99c27dd88c4ebb0d82 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 12 Jun 2014 23:35:20 -0700 Subject: [PATCH 50/57] Optimize model instantiation and conversion --- lib/dao.js | 14 +- lib/model-builder.js | 65 ++++---- lib/model.js | 331 +++++++++++++++++++------------------- test/loopback-dl.test.js | 13 +- test/manipulation.test.js | 8 - 5 files changed, 213 insertions(+), 218 deletions(-) diff --git a/lib/dao.js b/lib/dao.js index d4bd40de..062ada91 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -162,7 +162,6 @@ DataAccessObject.create = function (data, callback) { this._adapter().create(modelName, this.constructor._forDB(obj.toObject(true)), function (err, id, rev) { if (id) { obj.__data[_idName] = id; - obj.__dataWas[_idName] = id; defineReadonlyProp(obj, _idName, id); } if (rev) { @@ -353,8 +352,7 @@ DataAccessObject.findById = function find(id, cb) { if (!getIdValue(this, data)) { setIdValue(this, data, id); } - obj = new this(); - obj._initProperties(data); + obj = new this(data, {applySetters: false}); } cb(err, obj); }.bind(this)); @@ -689,9 +687,7 @@ DataAccessObject.find = function find(query, cb) { this.getDataSource().connector.all(this.modelName, query, function (err, data) { if (data && data.forEach) { data.forEach(function (d, i) { - var obj = new self(); - - obj._initProperties(d, {fields: query.fields}); + var obj = new self(d, {fields: query.fields, applySetters: false}); if (query && query.include) { if (query.collect) { @@ -1059,12 +1055,6 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb } inst._adapter().updateAttributes(model, getIdValue(inst.constructor, inst), inst.constructor._forDB(typedData), function (err) { - if (!err) { - // update $was attrs - for (var key in data) { - inst.__dataWas[key] = inst.__data[key]; - } - } done.call(inst, function () { saveDone.call(inst, function () { if(cb) cb(err, inst); diff --git a/lib/model-builder.js b/lib/model-builder.js index f0b126c6..89c98d1e 100644 --- a/lib/model-builder.js +++ b/lib/model-builder.js @@ -264,7 +264,11 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett // A function to loop through the properties ModelClass.forEachProperty = function (cb) { - Object.keys(ModelClass.definition.properties).forEach(cb); + var props = ModelClass.definition.properties; + var keys = Object.keys(props); + for (var i = 0, n = keys.length; i < n; i++) { + cb(keys[i], props[keys[i]]); + } }; // A function to attach the model class to a data source @@ -309,27 +313,22 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett } // Merging the properties - Object.keys(properties).forEach(function (key) { + var keys = Object.keys(properties); + for (var i = 0, n = keys.length; i < n; i++) { + var key = keys[i]; + if (idFound && properties[key].id) { // don't inherit id properties - return; + continue; } if (subclassProperties[key] === undefined) { subclassProperties[key] = properties[key]; } - }); + } // Merge the settings subclassSettings = mergeSettings(settings, subclassSettings); - /* - Object.keys(settings).forEach(function (key) { - if(subclassSettings[key] === undefined) { - subclassSettings[key] = settings[key]; - } - }); - */ - // Define the subclass var subClass = modelBuilder.define(className, subclassProperties, subclassSettings, ModelClass); @@ -370,20 +369,15 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett var DataType = ModelClass.definition.properties[propertyName].type; if (Array.isArray(DataType) || DataType === Array) { DataType = List; - } else if (DataType.name === 'Date') { - var OrigDate = Date; - DataType = function Date(arg) { - return new OrigDate(arg); - }; + } else if (DataType === Date) { + DataType = DateType; } else if (typeof DataType === 'string') { DataType = modelBuilder.resolveType(DataType); } if (ModelClass.setter[propertyName]) { ModelClass.setter[propertyName].call(this, value); // Try setter first } else { - if (!this.__data) { - this.__data = {}; - } + this.__data = this.__data || {}; if (value === null || value === undefined) { this.__data[propertyName] = value; } else { @@ -401,15 +395,6 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett enumerable: true }); - // $was --> __dataWas. - Object.defineProperty(ModelClass.prototype, propertyName + '$was', { - get: function () { - return this.__dataWas && this.__dataWas[propertyName]; - }, - configurable: true, - enumerable: false - }); - // FIXME: [rfeng] Do we need to keep the raw data? // Use $ as the prefix to avoid conflicts with properties such as _id Object.defineProperty(ModelClass.prototype, '$' + propertyName, { @@ -427,7 +412,13 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett }); }; - ModelClass.forEachProperty(ModelClass.registerProperty); + var props = ModelClass.definition.properties; + var keys = Object.keys(props); + var size = keys.length; + for (i = 0; i < size; i++) { + var propertyName = keys[i]; + ModelClass.registerProperty(propertyName); + } ModelClass.emit('defined', ModelClass); @@ -435,6 +426,11 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett }; +// DataType for Date +function DateType(arg) { + return new Date(arg); +} + /** * Define single property named `propertyName` on `model` * @@ -479,10 +475,11 @@ ModelBuilder.prototype.defineProperty = function (model, propertyName, propertyD */ ModelBuilder.prototype.extendModel = function (model, props) { var t = this; - Object.keys(props).forEach(function (propName) { - var definition = props[propName]; - t.defineProperty(model, propName, definition); - }); + var keys = Object.keys(props); + for (var i = 0; i < keys.length; i++) { + var definition = props[keys[i]]; + t.defineProperty(model, keys[i], definition); + } }; ModelBuilder.prototype.copyModel = function copyModel(Master) { diff --git a/lib/model.js b/lib/model.js index 508c5b6d..aaf5e29f 100644 --- a/lib/model.js +++ b/lib/model.js @@ -8,13 +8,20 @@ module.exports = ModelBaseClass; */ var util = require('util'); -var traverse = require('traverse'); var jutil = require('./jutil'); var List = require('./list'); var Hookable = require('./hooks'); var validations = require('./validations.js'); -var BASE_TYPES = ['String', 'Boolean', 'Number', 'Date', 'Text', 'ObjectID']; +// Set up an object for quick lookup +var BASE_TYPES = { + 'String': true, + 'Boolean': true, + 'Number': true, + 'Date': true, + 'Text': true, + 'ObjectID': true +}; /** * Model class: base class for all persistent objects. @@ -33,18 +40,6 @@ function ModelBaseClass(data, options) { this._initProperties(data, options); } -// FIXME: [rfeng] We need to make sure the input data should not be mutated. Disabled cloning for now to get tests passing -function clone(data) { - /* - if(!(data instanceof ModelBaseClass)) { - if(data && (Array.isArray(data) || 'object' === typeof data)) { - return traverse(data).clone(); - } - } - */ - return data; -} - /** * Initialize the model instance with a list of properties * @param {Object} data The data object @@ -58,10 +53,10 @@ ModelBaseClass.prototype._initProperties = function (data, options) { var ctor = this.constructor; if(data instanceof ctor) { - // Convert the data to be plain object to avoid polutions + // Convert the data to be plain object to avoid pollutions data = data.toObject(false); } - var properties = ctor.definition.build(); + var properties = ctor.definition.properties; data = data || {}; options = options || {}; @@ -71,133 +66,124 @@ ModelBaseClass.prototype._initProperties = function (data, options) { if(strict === undefined) { strict = ctor.definition.settings.strict; } - Object.defineProperty(this, '__cachedRelations', { - writable: true, - enumerable: false, - configurable: true, - value: {} - }); - Object.defineProperty(this, '__data', { - writable: true, - enumerable: false, - configurable: true, - value: {} - }); + if (ctor.hideInternalProperties) { + // Object.defineProperty() is expensive. We only try to make the internal + // properties hidden (non-enumerable) if the model class has the + // `hideInternalProperties` set to true + Object.defineProperties(this, { + __cachedRelations: { + writable: true, + enumerable: false, + configurable: true, + value: {} + }, - Object.defineProperty(this, '__dataWas', { - writable: true, - enumerable: false, - configurable: true, - value: {} - }); + __data: { + writable: true, + enumerable: false, + configurable: true, + value: {} + }, - /** - * Instance level data source - */ - Object.defineProperty(this, '__dataSource', { - writable: true, - enumerable: false, - configurable: true, - value: options.dataSource - }); + // Instance level data source + __dataSource: { + writable: true, + enumerable: false, + configurable: true, + value: options.dataSource + }, - /** - * Instance level strict mode - */ - Object.defineProperty(this, '__strict', { - writable: true, - enumerable: false, - configurable: true, - value: strict - }); + // Instance level strict mode + __strict: { + writable: true, + enumerable: false, + configurable: true, + value: strict + } + }); + } else { + this.__cachedRelations = {}; + this.__data = {}; + this.__dataSource = options.dataSource; + this.__strict = strict; + } if (data.__cachedRelations) { this.__cachedRelations = data.__cachedRelations; } - for (var i in data) { - if (i in properties && typeof data[i] !== 'function') { - this.__data[i] = this.__dataWas[i] = clone(data[i]); - } else if (i in ctor.relations) { - if (ctor.relations[i].type === 'belongsTo' && data[i] !== null && data[i] !== undefined) { + var keys = Object.keys(data); + var size = keys.length; + var p, propVal; + for (var k = 0; k < size; k++) { + p = keys[k]; + propVal = data[p]; + if (typeof propVal === 'function') { + continue; + } + if (properties[p]) { + // Managed property + if (applySetters) { + self[p] = propVal; + } else { + self.__data[p] = propVal; + } + } else if (ctor.relations[p]) { + // Relation + if (ctor.relations[p].type === 'belongsTo' && propVal != null) { // If the related model is populated - this.__data[ctor.relations[i].keyFrom] = this.__dataWas[i] = data[i][ctor.relations[i].keyTo]; + self.__data[ctor.relations[p].keyFrom] = propVal[ctor.relations[p].keyTo]; } - this.__cachedRelations[i] = data[i]; + self.__cachedRelations[p] = propVal; } else { + // Un-managed property if (strict === false) { - this.__data[i] = this.__dataWas[i] = clone(data[i]); + self[p] = self.__data[p] = propVal; } else if (strict === 'throw') { - throw new Error('Unknown property: ' + i); + throw new Error('Unknown property: ' + p); } } } - var propertyName; - if (applySetters === true) { - for (propertyName in data) { - if (typeof data[propertyName] !== 'function' && ((propertyName in properties) || (propertyName in ctor.relations))) { - self[propertyName] = self.__data[propertyName] || data[propertyName]; + keys = Object.keys(properties); + size = keys.length; + + for (k = 0; k < size; k++) { + p = keys[k]; + propVal = self.__data[p]; + + // Set default values + if (propVal === undefined) { + var def = properties[p]['default']; + if (def !== undefined) { + if (typeof def === 'function') { + self.__data[p] = def(); + } else { + self.__data[p] = def; + } } } - } - // Set the unknown properties as properties to the object - if (strict === false) { - for (propertyName in data) { - if (typeof data[propertyName] !== 'function' && !(propertyName in properties)) { - self[propertyName] = self.__data[propertyName] || data[propertyName]; - } - } - } - - ctor.forEachProperty(function (propertyName) { - - if (undefined === self.__data[propertyName]) { - self.__data[propertyName] = self.__dataWas[propertyName] = getDefault(propertyName); - } else { - self.__dataWas[propertyName] = self.__data[propertyName]; - } - - }); - - ctor.forEachProperty(function (propertyName) { - - var type = properties[propertyName].type; - - if (BASE_TYPES.indexOf(type.name) === -1) { - if (typeof self.__data[propertyName] !== 'object' && self.__data[propertyName]) { + // Handle complex types (JSON/Object) + var type = properties[p].type; + if (! BASE_TYPES[type.name]) { + if (typeof self.__data[p] !== 'object' && self.__data[p]) { try { - self.__data[propertyName] = JSON.parse(self.__data[propertyName] + ''); + self.__data[p] = JSON.parse(self.__data[p] + ''); } catch (e) { - self.__data[propertyName] = String(self.__data[propertyName]); + self.__data[p] = String(self.__data[p]); } } if (type.name === 'Array' || Array.isArray(type)) { - if (!(self.__data[propertyName] instanceof List) - && self.__data[propertyName] !== undefined - && self.__data[propertyName] !== null ) { - self.__data[propertyName] = List(self.__data[propertyName], type, self); + if (!(self.__data[p] instanceof List) + && self.__data[p] !== undefined + && self.__data[p] !== null ) { + self.__data[p] = List(self.__data[p], type, self); } } } - - }); - - function getDefault(propertyName) { - var def = properties[propertyName]['default']; - if (def !== undefined) { - if (typeof def === 'function') { - return def(); - } else { - return def; - } - } else { - return undefined; - } } - this.trigger('initialize'); }; @@ -242,7 +228,7 @@ ModelBaseClass.toString = function () { * @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, removeHidden) { - if(onlySchema === undefined) { + if (onlySchema === undefined) { onlySchema = true; } var data = {}; @@ -250,47 +236,63 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden) { var Model = this.constructor; // if it is already an Object - if(Model === Object) return self; + if (Model === Object) { + return self; + } var strict = this.__strict; var schemaLess = (strict === false) || !onlySchema; - this.constructor.forEachProperty(function (propertyName) { - if (removeHidden && Model.isHiddenProperty(propertyName)) { - return; - } - if (typeof self[propertyName] === 'function') { - return; - } - - if (self[propertyName] instanceof List) { - data[propertyName] = self[propertyName].toObject(!schemaLess, removeHidden); - } else if (self.__data.hasOwnProperty(propertyName)) { - if (self[propertyName] !== undefined && self[propertyName] !== null && self[propertyName].toObject) { - data[propertyName] = self[propertyName].toObject(!schemaLess, removeHidden); - } else { - data[propertyName] = self[propertyName]; - } - } else { - data[propertyName] = null; - } - }); + var props = Model.definition.properties; + var keys = Object.keys(props); + var propertyName, val; + for (var i = 0; i < keys.length; i++) { + propertyName = keys[i]; + val = self[propertyName]; + + // Exclude functions + if (typeof val === 'function') { + continue; + } + // Exclude hidden properties + if (removeHidden && Model.isHiddenProperty(propertyName)) { + continue; + } + + if (val instanceof List) { + data[propertyName] = val.toObject(!schemaLess, removeHidden); + } else { + if (val !== undefined && val !== null && val.toObject) { + data[propertyName] = val.toObject(!schemaLess, removeHidden); + } else { + data[propertyName] = val; + } + } + } - var val = null; if (schemaLess) { // Find its own properties which can be set via myModel.myProperty = 'myValue'. // If the property is not declared in the model definition, no setter will be // triggered to add it to __data - for (var propertyName in self) { - if(removeHidden && Model.isHiddenProperty(propertyName)) { + keys = Object.keys(self); + var size = keys.length; + for (i = 0; i < size; i++) { + propertyName = keys[i]; + if (props[propertyName]) { continue; } - if(self.hasOwnProperty(propertyName) && (!data.hasOwnProperty(propertyName))) { - val = self[propertyName]; + if (propertyName.indexOf('__') === 0) { + continue; + } + if (removeHidden && Model.isHiddenProperty(propertyName)) { + continue; + } + val = self[propertyName]; + if (val !== undefined && data[propertyName] === undefined) { if (typeof val === 'function') { continue; } - if (val !== undefined && val !== null && val.toObject) { + if (val !== null && val.toObject) { data[propertyName] = val.toObject(!schemaLess, removeHidden); } else { data[propertyName] = val; @@ -298,15 +300,25 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden) { } } // Now continue to check __data - for (propertyName in self.__data) { - if (!data.hasOwnProperty(propertyName)) { - if(removeHidden && Model.isHiddenProperty(propertyName)) { + keys = Object.keys(self.__data); + size = keys.length; + for (i = 0; i < size; i++) { + propertyName = keys[i]; + if (propertyName.indexOf('__') === 0) { + continue; + } + if (data[propertyName] === undefined) { + if (removeHidden && Model.isHiddenProperty(propertyName)) { continue; } - val = self.hasOwnProperty(propertyName) ? self[propertyName] : self.__data[propertyName]; + var ownVal = self[propertyName]; + // The ownVal can be a relation function + val = (ownVal !== undefined && (typeof ownVal !== 'function')) + ? ownVal : self.__data[propertyName]; if (typeof val === 'function') { continue; } + if (val !== undefined && val !== null && val.toObject) { data[propertyName] = val.toObject(!schemaLess, removeHidden); } else { @@ -319,12 +331,20 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden) { return data; }; -ModelBaseClass.isHiddenProperty = function(propertyName) { +ModelBaseClass.isHiddenProperty = function (propertyName) { var Model = this; var settings = Model.definition && Model.definition.settings; - var hiddenProperties = settings && settings.hidden; - if(hiddenProperties) { - return ~hiddenProperties.indexOf(propertyName); + var hiddenProperties = settings && (settings.hiddenProperties || settings.hidden); + if (Array.isArray(hiddenProperties)) { + // Cache the hidden properties as an object for quick lookup + settings.hiddenProperties = {}; + for (var i = 0; i < hiddenProperties.length; i++) { + settings.hiddenProperties[hiddenProperties[i]] = true; + } + hiddenProperties = settings.hiddenProperties; + } + if (hiddenProperties) { + return hiddenProperties[propertyName]; } else { return false; } @@ -340,16 +360,6 @@ ModelBaseClass.prototype.fromObject = function (obj) { } }; -/** - * Checks is property changed based on current property and initial value - * - * @param {String} propertyName Property name - * @return Boolean - */ -ModelBaseClass.prototype.propertyChanged = function propertyChanged(propertyName) { - return this.__data[propertyName] !== this.__dataWas[propertyName]; -}; - /** * Reset dirty attributes. * This method does not perform any database operations; it just resets the object to its @@ -361,9 +371,6 @@ ModelBaseClass.prototype.reset = function () { if (k !== 'id' && !obj.constructor.dataSource.definitions[obj.constructor.modelName].properties[k]) { delete obj[k]; } - if (obj.propertyChanged(k)) { - obj[k] = obj[k + '$was']; - } } }; diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index d1d2b0a8..3fbba174 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -1299,15 +1299,24 @@ describe('Load models from json', function () { customer.should.not.have.property('bio'); // The properties are defined at prototype level - assert.equal(Object.keys(customer).length, 0); + assert.equal(Object.keys(customer).filter(function (k) { + // Remove internal properties + return k.indexOf('__') === -1; + }).length, 0); var count = 0; for (var p in customer) { + if (p.indexOf('__') === 0) { + continue; + } if (typeof customer[p] !== 'function') { count++; } } assert.equal(count, 7); // Please note there is an injected id from User prototype - assert.equal(Object.keys(customer.toObject()).length, 6); + assert.equal(Object.keys(customer.toObject()).filter(function (k) { + // Remove internal properties + return k.indexOf('__') === -1; + }).length, 6); done(null, customer); }); diff --git a/test/manipulation.test.js b/test/manipulation.test.js index 5fd6ce84..d31b3b9d 100644 --- a/test/manipulation.test.js +++ b/test/manipulation.test.js @@ -133,14 +133,11 @@ describe('manipulation', function () { Person.findOne(function (err, p) { should.not.exist(err); p.name = 'Hans'; - p.propertyChanged('name').should.be.true; p.save(function (err) { should.not.exist(err); - p.propertyChanged('name').should.be.false; Person.findOne(function (err, p) { should.not.exist(err); p.name.should.equal('Hans'); - p.propertyChanged('name').should.be.false; done(); }); }); @@ -157,10 +154,8 @@ describe('manipulation', function () { p.name = 'Nana'; p.save(function (err) { should.exist(err); - p.propertyChanged('name').should.be.true; p.save({validate: false}, function (err) { should.not.exist(err); - p.propertyChanged('name').should.be.false; done(); }); }); @@ -244,10 +239,7 @@ describe('manipulation', function () { person = new Person({name: hw}); person.name.should.equal(hw); - person.propertyChanged('name').should.be.false; person.name = 'Goodbye, Lenin'; - person.name$was.should.equal(hw); - person.propertyChanged('name').should.be.true; (person.createdAt >= now).should.be.true; person.isNewRecord().should.be.true; }); From 177752e144a6f53febc5342588bb20ad7caa4d29 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 19 Jun 2014 11:13:24 -0700 Subject: [PATCH 51/57] Add more jsdocs --- lib/relation-definition.js | 49 ++++++++++++++++++++++++++++---------- test/relations.test.js | 1 - 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 38c52c93..13366d7a 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -631,17 +631,19 @@ BelongsTo.prototype.related = function (refresh, params) { }; /** - * 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: + * 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: * ``` * User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'}); * ``` * - * @param {String|Object} modelTo Model object (or String name of model) to which you are creating the relationship. - * the relation + * @param {String|Object} modelTo Model object (or String name of model) to + * which you are creating the relationship. * @options {Object} params Configuration parameters; see below. - * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. + * @property {String} as Name of the property in the referring model that + * corresponds to the foreign key field in the related model. * @property {String} foreignKey Property name of foreign key field. * @property {Object} model Model object */ @@ -676,10 +678,19 @@ RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, }; /** - * HasOne - * @param modelFrom - * @param modelTo - * @param params + * A HasOne relation creates a one-to-one connection from modelFrom to modelTo. + * This relation indicates that each instance of a model contains or possesses + * one instance of another model. For example, each supplier in your application + * has only one account. + * + * @param {Function} modelFrom The declaring model class + * @param {String|Function} modelTo Model object (or String name of model) to + * which you are creating the relationship. + * @options {Object} params Configuration parameters; see below. + * @property {String} as Name of the property in the referring model that + * corresponds to the foreign key field in the related model. + * @property {String} foreignKey Property name of foreign key field. + * @property {Object} model Model object */ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { params = params || {}; @@ -723,6 +734,13 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { }); }; +/** + * Create a target model instance + * @param {Object} targetModelData The target model data + * @callback {Function} [cb] Callback function + * @param {String|Object} err Error string or object + * @param {Object} The newly created target model instance + */ HasOne.prototype.create = function(targetModelData, cb) { var modelTo = this.definition.modelTo; var fk = this.definition.keyTo; @@ -740,6 +758,11 @@ HasOne.prototype.create = function(targetModelData, cb) { }); }; +/** + * Build a target model instance + * @param {Object} targetModelData The target model data + * @returns {Object} The newly built target model instance + */ HasOne.prototype.build = function(targetModelData) { var modelTo = this.definition.modelTo; var pk = this.definition.keyFrom; @@ -756,9 +779,9 @@ HasOne.prototype.build = function(targetModelData) { * - order.customer(customer): Synchronous setter of the target model instance * - order.customer(): Synchronous getter of the target model instance * - * @param refresh - * @param params - * @returns {*} + * @param {Boolean} refresh Reload from the data source + * @param {Object|Function} params Query parameters + * @returns {Object} */ HasOne.prototype.related = function (refresh, params) { var modelTo = this.definition.modelTo; diff --git a/test/relations.test.js b/test/relations.test.js index d0cd0ea9..4e1f5873 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -193,7 +193,6 @@ describe('relations', function () { Supplier.hasOne(Account); Object.keys((new Account()).toObject()).should.include('supplierId'); (new Supplier()).account.should.be.an.instanceOf(Function); - }); it('can be used to query data', function (done) { From 18333522381c0dfab35f241421489900ee95a402 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 19 Jun 2014 12:00:49 -0700 Subject: [PATCH 52/57] More clean up for the scope processing --- lib/scope.js | 93 ++++++++++++++++++++++------------------------ test/scope.test.js | 2 +- 2 files changed, 45 insertions(+), 50 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index 185ad0d3..8a4485df 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -38,7 +38,7 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres if (!self.__cachedRelations || self.__cachedRelations[name] === undefined || actualRefresh) { // It either doesn't hit the cache or refresh is required - var params = mergeParams(actualCond, scopeParams); + var params = mergeQuery(actualCond, scopeParams); return this.targetModel.find(params, function (err, data) { if (!err && saveOnCache) { defineCachedRelations(self); @@ -134,7 +134,7 @@ function defineScope(cls, targetClass, name, params, methods) { Object.defineProperty(f, name, { enumerable: false, get: function () { - mergeParams(f._scope, targetClass._scopeMeta[name]); + mergeQuery(f._scope, targetClass._scopeMeta[name]); return f; } }); @@ -183,15 +183,40 @@ function defineScope(cls, targetClass, name, params, methods) { cls['__delete__' + name] = fn_delete; + /* + * Extracting fixed property values for the scope from the where clause into + * the data object + * + * @param {Object} The data object + * @param {Object} The where clause + */ + function setScopeValuesFromWhere(data, where) { + for (var i in where) { + if (i === 'and') { + // Find fixed property values from each subclauses + for (var w = 0, n = where[i].length; w < n; w++) { + setScopeValuesFromWhere(data, where[i][w]); + } + continue; + } + var prop = targetClass.definition.properties[i]; + if (prop) { + var val = where[i]; + if (typeof val !== 'object' || val instanceof prop.type) { + // Only pick the {propertyName: propertyValue} + data[i] = where[i]; + } + } + } + } + // and it should have create/build methods with binded thisModelNameId param function build(data) { data = data || {}; - var params = mergeParams(this._scope, {where: data}).where; - delete params['and']; - delete params['or']; - delete params['nor']; - - return new targetClass(params); + // Find all fixed property values for the scope + var where = (this._scope && this._scope.where) || {}; + setScopeValuesFromWhere(data, where); + return new targetClass(data); } function create(data, cb) { @@ -209,58 +234,28 @@ function defineScope(cls, targetClass, name, params, methods) { - If fetching the Elements on which destroyAll is called results in an error */ function destroyAll(cb) { - targetClass.find(this._scope, function (err, data) { - if (err) { - cb(err); - } else { - (function loopOfDestruction(data) { - if (data.length > 0) { - data.shift().destroy(function (err) { - if (err && cb) cb(err); - loopOfDestruction(data); - }); - } else { - if (cb) cb(); - } - }(data)); - } - }); + targetClass.destroyAll(this._scope, cb); } } -/*! - * Merge `base` and `update` params - * @param {Object} base - base object (updating this object) - * @param {Object} update - object with new data to update base - * @returns {Object} `base` - * @private - */ -function mergeWhere(base, update) { - base = base || {}; - if (update) { - var keys = Object.keys(update); - for (var k = 0; k < keys.length; k++) { - var key = keys[k]; - base[key] = update[key]; - } - } - return base; -} - /*! * Merge query parameters - * @param base - * @param update - * @returns {*|{}} + * @param {Object} base The base object to contain the merged results + * @param {Object} update The object containing updates to be merged + * @returns {*|Object} The base object * @private */ -function mergeParams(base, update) { +function mergeQuery(base, update) { if (!update) { return; } base = base || {}; - if (update.where) { - base.where = mergeWhere(base.where, update.where); + if (update.where && Object.keys(update.where).length > 0) { + if (base.where && Object.keys(base.where).length > 0) { + base.where = {and: [base.where, update.where]}; + } else { + base.where = update.where; + } } // Overwrite inclusion diff --git a/test/scope.test.js b/test/scope.test.js index d927bcc7..ecaa8727 100644 --- a/test/scope.test.js +++ b/test/scope.test.js @@ -3,7 +3,7 @@ var should = require('./init.js'); var db, Railway, Station; -describe('sc0pe', function () { +describe('scope', function () { before(function () { db = getSchema(); From 27f4c1b7b437f1d15efd18876f3ed1a9fd4d39ad Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 19 Jun 2014 15:09:19 -0700 Subject: [PATCH 53/57] Fix the error msg --- lib/list.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/list.js b/lib/list.js index 3e6faa1e..4c4158eb 100644 --- a/lib/list.js +++ b/lib/list.js @@ -13,7 +13,7 @@ function List(items, itemType, parent) { try { items = JSON.parse(items); } catch (e) { - throw new Error('could not create List from JSON string: ', items); + throw new Error(util.format('could not create List from JSON string: %j', items)); } } @@ -22,7 +22,7 @@ function List(items, itemType, parent) { items = items || []; if (!Array.isArray(items)) { - throw new Error('Items must be an array: ' + items); + throw new Error(util.format('Items must be an array: %j', items)); } if(!itemType) { @@ -92,12 +92,3 @@ List.prototype.toString = function () { return JSON.stringify(this.toJSON()); }; -/* - var strArray = new List(['1', 2], String); - strArray.push(3); - console.log(strArray); - console.log(strArray.length); - - console.log(strArray.toJSON()); - console.log(strArray.toString()); - */ From a1836662a78e1338553b5d212ad336d3f3efc5fc Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 20 Jun 2014 12:05:32 -0700 Subject: [PATCH 54/57] Clean up comments --- lib/connectors/memory.js | 2 ++ lib/dao.js | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index 06c65772..97531f13 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -368,6 +368,8 @@ function applyFilter(filter) { return pattern; } var regex = ''; + // Escaping user input to be treated as a literal string within a regular expression + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Writing_a_Regular_Expression_Pattern pattern = pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); for (var i = 0, n = pattern.length; i < n; i++) { var char = pattern.charAt(i); diff --git a/lib/dao.js b/lib/dao.js index 5f8d0c77..c788413e 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -982,25 +982,25 @@ DataAccessObject.updateAll = function (where, data, cb) { if (stillConnecting(this.getDataSource(), this, arguments)) return; if (arguments.length === 1) { - // update(data); + // update(data) is being called data = where; where = null; cb = null; } else if (arguments.length === 2) { if (typeof data === 'function') { - // update(data, cb); + // update(data, cb) is being called cb = data; data = where; where = null; } else { - // update(where, data); + // update(where, data) is being called cb = null; } } assert(typeof where === 'object', 'The where argument should be an object'); assert(typeof data === 'object', 'The data argument should be an object'); - assert(cb === null || typeof cb === 'function', 'The data argument should be an object'); + assert(cb === null || typeof cb === 'function', 'The cb argument should be a function'); try { where = removeUndefined(where); From b07c36eab7b3fe5f889d8e55b20dda9585d9d975 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 20 Jun 2014 12:05:42 -0700 Subject: [PATCH 55/57] Use async for flow control --- test/basic-querying.test.js | 16 ++++++---------- test/memory.test.js | 15 +++++---------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/test/basic-querying.test.js b/test/basic-querying.test.js index 12ac4dcf..28fa210b 100644 --- a/test/basic-querying.test.js +++ b/test/basic-querying.test.js @@ -1,5 +1,6 @@ // This test written in mocha+should.js var should = require('./init.js'); +var async = require('async'); var db, User; describe('basic-querying', function () { @@ -578,7 +579,6 @@ describe('basic-querying', function () { }); function seed(done) { - var count = 0; var beatles = [ { seq: 0, @@ -603,15 +603,11 @@ function seed(done) { {seq: 4, name: 'Pete Best', order: 4}, {seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true} ]; - User.destroyAll(function () { - beatles.forEach(function (beatle) { - User.create(beatle, ok); - }); - }); - function ok() { - if (++count === beatles.length) { - done(); + async.series([ + User.destroyAll.bind(User), + function(cb) { + async.each(beatles, User.create.bind(User), cb); } - } + ], done); } diff --git a/test/memory.test.js b/test/memory.test.js index b98462a1..4016209e 100644 --- a/test/memory.test.js +++ b/test/memory.test.js @@ -142,7 +142,6 @@ describe('Memory connector', function () { }); function seed(done) { - var count = 0; var beatles = [ { seq: 0, @@ -167,17 +166,13 @@ describe('Memory connector', function () { {seq: 4, name: 'Pete Best', order: 4}, {seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true} ]; - User.destroyAll(function () { - beatles.forEach(function (beatle) { - User.create(beatle, ok); - }); - }); - function ok() { - if (++count === beatles.length) { - done(); + async.series([ + User.destroyAll.bind(User), + function(cb) { + async.each(beatles, User.create.bind(User), cb); } - } + ], done); } }); From a2a91499ddb8e305a13ceae1eb8c1283fded4b41 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 20 Jun 2014 21:07:26 -0700 Subject: [PATCH 56/57] Fix the test case --- test/basic-querying.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/basic-querying.test.js b/test/basic-querying.test.js index e5a41549..2a235def 100644 --- a/test/basic-querying.test.js +++ b/test/basic-querying.test.js @@ -84,7 +84,7 @@ describe('basic-querying', function () { }); it('should query collection with offset & limit', function (done) { - User.find({offset: 2, limit: 3}, function (err, users) { + User.find({offset: 2, limit: 3, order: 'seq'}, function (err, users) { should.exists(users); should.not.exists(err); users[0].seq.should.be.eql(2); From 58131029cfe3d0a2e6570fb2eecdbc5c0c4eb482 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 20 Jun 2014 21:16:39 -0700 Subject: [PATCH 57/57] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 246c21a1..8e633c6d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "1.5.5", + "version": "1.6.1", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop",