From dc9c9b2bd4762234abc33ad0e3a586d3095ac142 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 16 May 2014 12:33:17 -0700 Subject: [PATCH 01/11] Remove remoting metadata --- lib/dao.js | 89 ---------------------------------------- lib/jutil.js | 28 +------------ test/loopback-dl.test.js | 37 ----------------- 3 files changed, 1 insertion(+), 153 deletions(-) diff --git a/lib/dao.js b/lib/dao.js index aad2cc6c6..92cc1d69b 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -182,29 +182,6 @@ DataAccessObject.create = function (data, callback) { return obj; }; -/*! - * Configure the remoting attributes for a given function - * @param {Function} fn The function - * @param {Object} options The options - * @private - */ -function setRemoting(fn, options) { - options = options || {}; - for (var opt in options) { - if (options.hasOwnProperty(opt)) { - fn[opt] = options[opt]; - } - } - fn.shared = true; -} - -setRemoting(DataAccessObject.create, { - description: 'Create a new instance of the model and persist it into the data source', - accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, - returns: {arg: 'data', type: 'object', root: true}, - http: {verb: 'post', path: '/'} -}); - function stillConnecting(dataSource, obj, args) { return dataSource.ready(obj, args); } @@ -260,14 +237,6 @@ DataAccessObject.upsert = DataAccessObject.updateOrCreate = function upsert(data } }; -// upsert ~ remoting attributes -setRemoting(DataAccessObject.upsert, { - description: 'Update an existing model instance or insert a new one into the data source', - accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, - returns: {arg: 'data', type: 'object', root: true}, - http: {verb: 'put', path: '/'} -}); - /** * Find one record, same as `all`, limited by 1 and return object, not collection, * if not found, create using data provided as second argument @@ -313,15 +282,6 @@ DataAccessObject.exists = function exists(id, cb) { } }; -// exists ~ remoting attributes -setRemoting(DataAccessObject.exists, { - description: 'Check whether a model instance exists in the data source', - accepts: {arg: 'id', type: 'any', description: 'Model id', required: true, - http: {source: 'path'}}, - returns: {arg: 'exists', type: 'any'}, - http: {verb: 'get', path: '/:id/exists'} -}); - /** * Find object by id * @@ -344,16 +304,6 @@ DataAccessObject.findById = function find(id, cb) { }.bind(this)); }; -// find ~ remoting attributes -setRemoting(DataAccessObject.findById, { - description: 'Find a model instance by id from the data source', - accepts: {arg: 'id', type: 'any', description: 'Model id', required: true, - http: {source: 'path'}}, - returns: {arg: 'data', type: 'any', root: true}, - http: {verb: 'get', path: '/:id'}, - rest: {after: convertNullToNotFoundError} -}); - function convertNullToNotFoundError(ctx, cb) { if (ctx.result !== null) return cb(); @@ -604,14 +554,6 @@ DataAccessObject.find = function find(query, cb) { }); }; -// all ~ remoting attributes -setRemoting(DataAccessObject.find, { - description: 'Find all instances 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: 'array', root: true}, - http: {verb: 'get', path: '/'} -}); - /** * Find one record, same as `all`, limited by 1 and return object, not collection * @@ -633,13 +575,6 @@ DataAccessObject.findOne = function findOne(query, cb) { }); }; -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'} -}); - /** * Destroy all matching records * @param {Object} [where] An object that defines the criteria @@ -688,14 +623,6 @@ DataAccessObject.removeById = DataAccessObject.deleteById = DataAccessObject.des }.bind(this)); }; -// deleteById ~ remoting attributes -setRemoting(DataAccessObject.deleteById, { - description: 'Delete a model instance by id from the data source', - accepts: {arg: 'id', type: 'any', description: 'Model id', required: true, - http: {source: 'path'}}, - http: {verb: 'del', path: '/:id'} -}); - /** * Return count of matched records * @@ -714,14 +641,6 @@ DataAccessObject.count = function (where, cb) { this.getDataSource().connector.count(this.modelName, cb, where); }; -// count ~ remoting attributes -setRemoting(DataAccessObject.count, { - description: 'Count instances of the model matched by where from the data source', - accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'}, - returns: {arg: 'count', type: 'number'}, - http: {verb: 'get', path: '/count'} -}); - /** * Save instance. If the instance does not have an ID, call `create` instead. * Triggers: validate, save, update or create. @@ -917,14 +836,6 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb }, data); }; -// updateAttributes ~ remoting attributes -setRemoting(DataAccessObject.prototype.updateAttributes, { - description: 'Update attributes for a model instance and persist it into the data source', - accepts: {arg: 'data', type: 'object', http: {source: 'body'}, description: 'An object of model property name/value pairs'}, - returns: {arg: 'data', type: 'object', root: true}, - http: {verb: 'put', path: '/'} -}); - /** * Reload object from persistence * Requires `id` member of `object` to be able to call `find` diff --git a/lib/jutil.js b/lib/jutil.js index c33ec24c1..bcab34e65 100644 --- a/lib/jutil.js +++ b/lib/jutil.js @@ -73,37 +73,11 @@ function mixInto(sourceScope, targetScope, options) { var sourceProperty = Object.getOwnPropertyDescriptor(sourceScope, propertyName); var targetProperty = targetPropertyExists && Object.getOwnPropertyDescriptor(targetScope, propertyName); var sourceIsFunc = typeof sourceProperty.value === 'function'; - var isFunc = targetPropertyExists && typeof targetProperty.value === 'function'; - var isDelegate = isFunc && targetProperty.value._delegate; - var shouldOverride = options.override || !targetPropertyExists || isDelegate; + var shouldOverride = options.override || !targetPropertyExists || sourceIsFunc; if (shouldOverride) { - if (sourceIsFunc) { - sourceProperty.value = exports.proxy(sourceProperty.value, proxies); - } - Object.defineProperty(targetScope, propertyName, sourceProperty); } }); } -exports.proxy = function createProxy(fn, proxies) { - // Make sure same methods referenced by different properties have the same proxy - // For example, deleteById is an alias of removeById - proxies = proxies || []; - for (var i = 0; i < proxies.length; i++) { - if (proxies[i]._delegate === fn) { - return proxies[i]; - } - } - var f = function () { - return fn.apply(this, arguments); - }; - f._delegate = fn; - proxies.push(f); - Object.keys(fn).forEach(function (x) { - f[x] = fn[x]; - }); - return f; -}; - diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index 6a1e013db..2e3093a2e 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -1285,43 +1285,6 @@ describe('DataSource constructor', function () { }); }); -describe('Injected methods from connectors', function () { - it('are not shared across models for remote methods', function () { - var ds = new DataSource('memory'); - var M1 = ds.createModel('M1'); - var M2 = ds.createModel('M2'); - // Remotable methods are not shared across models - assert.notEqual(M1.create, M2.create, 'Remotable methods are not shared'); - assert.equal(M1.create.shared, true, 'M1.create is remotable'); - assert.equal(M2.create.shared, true, 'M2.create is remotable'); - M1.create.shared = false; - assert.equal(M1.create.shared, false, 'M1.create should be local now'); - assert.equal(M2.create.shared, true, 'M2.create should stay remotable'); - }); - - it('are not shared across models for non-remote methods', function () { - var ds = new DataSource('memory'); - var M1 = ds.createModel('M1'); - var M2 = ds.createModel('M2'); - var m1 = M1.prototype.save; - var m2 = M2.prototype.save; - assert.notEqual(m1, m2, 'non-remote methods are not shared'); - assert.equal(!!m1.shared, false, 'M1.save is not remotable'); - assert.equal(!!m2.shared, false, 'M2.save is not remotable'); - m1.shared = true; - assert.equal(m1.shared, true, 'M1.save is now remotable'); - assert.equal(!!m2.shared, false, 'M2.save is not remotable'); - - assert.equal(M1.deleteById, M1.removeById, - 'Same methods on the same model should have the same proxy'); - - assert.notEqual(M1.deleteById, M2.deleteById, - 'Same methods on differnt models should have different proxies'); - - }); - -}); - describe('ModelBuilder options.models', function () { it('should inject model classes from models', function () { var builder = new ModelBuilder(); From 05410d56e18189ae61899f225b34a5b12c3f9747 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 02/11] 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 200bf6ba2..a12a81271 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 f01414aaf..c3b5c8d4c 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 934b5a3fa9cc5b3d23b2df99a253dc30cb37b572 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 03/11] 1.5.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 09efe9556..fb56d97ec 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 072999775e3a93e9d6666d307fbe743c779e9c97 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 20 May 2014 12:47:14 -0700 Subject: [PATCH 04/11] Remove relation remoting --- lib/relations.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/relations.js b/lib/relations.js index a18da4e9e..ea9afc23f 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -412,13 +412,6 @@ Relation.belongsTo = function (anotherClass, params) { 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; }; From e724efd95f8e2685f03771145415ca8b99d5dc42 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 20 May 2014 13:44:25 -0700 Subject: [PATCH 05/11] !fixup Require ._delegate for fn override --- lib/jutil.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/jutil.js b/lib/jutil.js index bcab34e65..68b0637b0 100644 --- a/lib/jutil.js +++ b/lib/jutil.js @@ -66,18 +66,21 @@ exports.mixin = function (newClass, mixinClass, options) { }; function mixInto(sourceScope, targetScope, options) { - var proxies = []; - Object.keys(sourceScope).forEach(function (propertyName, options) { var targetPropertyExists = targetScope.hasOwnProperty(propertyName); var sourceProperty = Object.getOwnPropertyDescriptor(sourceScope, propertyName); var targetProperty = targetPropertyExists && Object.getOwnPropertyDescriptor(targetScope, propertyName); var sourceIsFunc = typeof sourceProperty.value === 'function'; - var shouldOverride = options.override || !targetPropertyExists || sourceIsFunc; + var isFunc = targetPropertyExists && typeof targetProperty.value === 'function'; + var isDelegate = isFunc && targetProperty.value._delegate; + var shouldOverride = options.override || !targetPropertyExists || isDelegate; if (shouldOverride) { + if (sourceIsFunc) { + sourceProperty.value = sourceProperty.value; + } + Object.defineProperty(targetScope, propertyName, sourceProperty); } }); } - From 8849a4b49a2d38fa5e6e47c7952958575a4553b7 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 20 May 2014 13:48:23 -0700 Subject: [PATCH 06/11] !fixup Remove additional remoting --- lib/scope.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index ac14efc77..17ee0e47d 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -118,12 +118,6 @@ function defineScope(cls, targetClass, name, params, methods) { f.apply(this[name], arguments); }; - fn.shared = true; - fn.http = {verb: 'get', path: '/' + name}; - fn.accepts = {arg: 'filter', type: 'object'}; - fn.description = 'Queries ' + name + ' of this model.'; - fn.returns = {arg: name, type: 'array', root: true}; - cls['__get__' + name] = fn; var fn_create = function () { @@ -131,22 +125,12 @@ function defineScope(cls, targetClass, name, params, methods) { f.apply(this[name], arguments); }; - fn_create.shared = true; - fn_create.http = {verb: 'post', path: '/' + name}; - fn_create.accepts = {arg: 'data', type: 'object', http: {source: 'body'}}; - fn_create.description = 'Creates a new instance in ' + name + ' of this model.'; - fn_create.returns = {arg: 'data', type: 'object', root: true}; - cls['__create__' + name] = fn_create; var fn_delete = function () { var f = this[name].destroyAll; f.apply(this[name], arguments); }; - fn_delete.shared = true; - fn_delete.http = {verb: 'delete', path: '/' + name}; - fn_delete.description = 'Deletes all ' + name + ' of this model.'; - fn_delete.returns = {arg: 'data', type: 'object', root: true}; cls['__delete__' + name] = fn_delete; From 104ba1aab8009f0292a0c9330da3848a38c62f0c Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 20 May 2014 13:59:53 -0700 Subject: [PATCH 07/11] 1.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fb56d97ec..97e92efa5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "1.5.2", + "version": "1.6.0", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", From 00226dde0d02f115f2f274a2ef496e218aec9cce Mon Sep 17 00:00:00 2001 From: crandmck Date: Wed, 21 May 2014 17:50:44 -0700 Subject: [PATCH 08/11] Copy info from api-model.md to JSDoc --- lib/dao.js | 134 ++++++++++++++++++++++++++++++++++++----------- lib/relations.js | 44 +++++++++++++--- 2 files changed, 141 insertions(+), 37 deletions(-) diff --git a/lib/dao.js b/lib/dao.js index 92cc1d69b..e75ddd481 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; @@ -187,7 +192,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). @@ -238,10 +246,12 @@ DataAccessObject.upsert = DataAccessObject.updateOrCreate = function upsert(data }; /** - * 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) */ @@ -283,7 +293,14 @@ DataAccessObject.exists = function exists(id, cb) { }; /** - * 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) @@ -427,18 +444,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 + * 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. * - * @param {Object} [query] the query object + * ```js + * User.find({ + * where: { + * age: {gt: 21}}, + * order: 'age DESC', + * limit: 10, + * skip: 10, + * fields: {password: false} + * }, + * console.log + * ); + * ``` * - * - where: Object `{ key: val, key2: {gt: 'val2'}}` - * - include: String, Object or Array. See `DataAccessObject.include()`. - * - order: String - * - limit: Number - * - skip: Number + * @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) called with two arguments: err (null or Error), array of instances + * @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) { @@ -555,10 +613,11 @@ DataAccessObject.find = function find(query, cb) { }; /** - * 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; @@ -576,8 +635,16 @@ DataAccessObject.findOne = function findOne(query, cb) { }; /** - * 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) { @@ -624,9 +691,16 @@ DataAccessObject.removeById = DataAccessObject.deleteById = DataAccessObject.des }; /** - * 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 ea9afc23f..9be55e149 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. * @@ -439,11 +468,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 5937f0c0d56038f35a6b1de11701f4216fd3555d Mon Sep 17 00:00:00 2001 From: crandmck Date: Thu, 22 May 2014 15:02:57 -0700 Subject: [PATCH 09/11] 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 9be55e149..a938df094 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. * */ @@ -471,7 +471,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 fc758abc2b7da2c31995ce773a1cbbc53126a0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 23 May 2014 11:03:24 +0200 Subject: [PATCH 10/11] Replace connector base with loopback-connector Remove references to Connector and BaseSQL, connectors should require() loopback-connector instead of loopback-datasource-juggler. --- index.js | 8 - lib/connector.js | 170 ----------------- lib/connectors/memory.js | 2 +- lib/sql.js | 392 --------------------------------------- package.json | 1 + 5 files changed, 2 insertions(+), 571 deletions(-) delete mode 100644 lib/connector.js delete mode 100644 lib/sql.js diff --git a/index.js b/index.js index 0090da6b0..e3eacac0c 100644 --- a/index.js +++ b/index.js @@ -1,16 +1,8 @@ exports.ModelBuilder = exports.LDL = require('./lib/model-builder.js').ModelBuilder; exports.DataSource = exports.Schema = require('./lib/datasource.js').DataSource; exports.ModelBaseClass = require('./lib/model.js'); -exports.Connector = require('./lib/connector.js'); exports.ValidationError = require('./lib/validations.js').ValidationError; -var baseSQL = './lib/sql'; - -exports.__defineGetter__('BaseSQL', function () { - return require(baseSQL); -}); - - exports.__defineGetter__('version', function () { return require('./package.json').version; }); diff --git a/lib/connector.js b/lib/connector.js deleted file mode 100644 index 86f0998c7..000000000 --- a/lib/connector.js +++ /dev/null @@ -1,170 +0,0 @@ -module.exports = Connector; - -/** - * Base class for LooopBack connector. This is more a collection of useful - * methods for connectors than a super class - * @constructor - */ -function Connector(name, settings) { - this._models = {}; - this.name = name; - this.settings = settings || {}; -} - -/** - * Set the relational property to indicate the backend is a relational DB - * @type {boolean} - */ -Connector.prototype.relational = false; - -/** - * Get types associated with the connector - * @returns {String[]} The types for the connector - */ -Connector.prototype.getTypes = function() { - return ['db', 'nosql']; -}; - -/** - * Get the default data type for ID - * @returns {Function} The default type for ID - */ -Connector.prototype.getDefaultIdType = function() { - return String; -}; - -/** - * Get the metadata for the connector - * @returns {Object} The metadata object - * @property {String} type The type for the backend - * @property {Function} defaultIdType The default id type - * @property {Boolean} [isRelational] If the connector represents a relational database - * @property {Object} schemaForSettings The schema for settings object - */ -Connector.prototype.getMedadata = function () { - if (!this._metadata) { - this._metadata = { - types: this.getTypes(), - defaultIdType: this.getDefaultIdType(), - isRelational: this.isRelational || (this.getTypes().indexOf('rdbms') !== -1), - schemaForSettings: {} - }; - } - return this._metadata; -}; - -/** - * Execute a command with given parameters - * @param {String} command The command such as SQL - * @param {Object[]} [params] An array of parameters - * @param {Function} [callback] The callback function - */ -Connector.prototype.execute = function (command, params, callback) { - throw new Error('query method should be declared in connector'); -}; - -/** - * Look up the data source by model name - * @param {String} model The model name - * @returns {DataSource} The data source - */ -Connector.prototype.getDataSource = function (model) { - var m = this._models[model]; - if (!m) { - console.trace('Model not found: ' + model); - } - return m && m.model.dataSource; -}; - -/** - * Get the id property name - * @param {String} model The model name - * @returns {String} The id property name - */ -Connector.prototype.idName = function (model) { - return this.getDataSource(model).idName(model); -}; - -/** - * Get the id property names - * @param {String} model The model name - * @returns {[String]} The id property names - */ -Connector.prototype.idNames = function (model) { - return this.getDataSource(model).idNames(model); -}; - -/** - * Get the id index (sequence number, starting from 1) - * @param {String} model The model name - * @param {String} prop The property name - * @returns {Number} The id index, undefined if the property is not part of the primary key - */ -Connector.prototype.id = function (model, prop) { - var p = this._models[model].properties[prop]; - if (!p) { - console.trace('Property not found: ' + model + '.' + prop); - } - return p.id; -}; - -/** - * Hook to be called by DataSource for defining a model - * @param {Object} modelDefinition The model definition - */ -Connector.prototype.define = function (modelDefinition) { - if (!modelDefinition.settings) { - modelDefinition.settings = {}; - } - this._models[modelDefinition.model.modelName] = modelDefinition; -}; - -/** - * Hook to be called by DataSource for defining a model property - * @param {String} model The model name - * @param {String} propertyName The property name - * @param {Object} propertyDefinition The object for property metadata - */ -Connector.prototype.defineProperty = function (model, propertyName, propertyDefinition) { - this._models[model].properties[propertyName] = propertyDefinition; -}; - -/** - * Disconnect from the connector - */ -Connector.prototype.disconnect = function disconnect(cb) { - // NO-OP - cb && process.nextTick(cb); -}; - -/** - * Get the id value for the given model - * @param {String} model The model name - * @param {Object} data The model instance data - * @returns {*} The id value - * - */ -Connector.prototype.getIdValue = function (model, data) { - return data && data[this.idName(model)]; -}; - -/** - * Set the id value for the given model - * @param {String} model The model name - * @param {Object} data The model instance data - * @param {*} value The id value - * - */ -Connector.prototype.setIdValue = function (model, data, value) { - if (data) { - data[this.idName(model)] = value; - } -}; - -Connector.prototype.getType = function () { - return this.type; -}; - - - - diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index ce466221f..b36d9d31e 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -1,5 +1,5 @@ var util = require('util'); -var Connector = require('../connector'); +var Connector = require('loopback-connector').Connector; var geo = require('../geo'); var utils = require('../utils'); var fs = require('fs'); diff --git a/lib/sql.js b/lib/sql.js deleted file mode 100644 index 1777602e1..000000000 --- a/lib/sql.js +++ /dev/null @@ -1,392 +0,0 @@ -var util = require('util'); -var async = require('async'); -var assert = require('assert'); -var Connector = require('./connector'); - -module.exports = BaseSQL; - -/** - * Base class for connectors that are backed by relational databases/SQL - * @class - */ -function BaseSQL() { - Connector.apply(this, [].slice.call(arguments)); -} - -util.inherits(BaseSQL, Connector); - -/** - * Set the relational property to indicate the backend is a relational DB - * @type {boolean} - */ -BaseSQL.prototype.relational = true; - -/** - * Get types associated with the connector - * Returns {String[]} The types for the connector - */ - BaseSQL.prototype.getTypes = function() { - return ['db', 'rdbms', 'sql']; -}; - -/*! - * Get the default data type for ID - * Returns {Function} - */ -BaseSQL.prototype.getDefaultIdType = function() { - return Number; -}; - -BaseSQL.prototype.query = function () { - throw new Error('query method should be declared in connector'); -}; - -BaseSQL.prototype.command = function (sql, params, callback) { - return this.query(sql, params, callback); -}; - -BaseSQL.prototype.queryOne = function (sql, callback) { - return this.query(sql, function (err, data) { - if (err) { - return callback(err); - } - callback(err, data && data[0]); - }); -}; - -/** - * Get the table name for a given model. - * Returns the table name (String). - * @param {String} model The model name - */ -BaseSQL.prototype.table = function (model) { - var name = this.getDataSource(model).tableName(model); - var dbName = this.dbName; - if (typeof dbName === 'function') { - name = dbName(name); - } - return name; -}; - -/** - * Get the column name for given model property - * @param {String} model The model name - * @param {String} property The property name - * @returns {String} The column name - */ -BaseSQL.prototype.column = function (model, property) { - var name = this.getDataSource(model).columnName(model, property); - var dbName = this.dbName; - if (typeof dbName === 'function') { - name = dbName(name); - } - return name; -}; - -/** - * Get the column name for given model property - * @param {String} model The model name - * @param {String} property The property name - * @returns {Object} The column metadata - */ -BaseSQL.prototype.columnMetadata = function (model, property) { - return this.getDataSource(model).columnMetadata(model, property); -}; - -/** - * Get the corresponding property name for a given column name - * @param {String} model The model name - * @param {String} column The column name - * @returns {String} The property name for a given column - */ -BaseSQL.prototype.propertyName = function (model, column) { - var props = this._models[model].properties; - for (var p in props) { - if (this.column(model, p) === column) { - return p; - } - } - return null; -}; - -/** - * Get the id column name - * @param {String} model The model name - * @returns {String} The column name - */ -BaseSQL.prototype.idColumn = function (model) { - var name = this.getDataSource(model).idColumnName(model); - var dbName = this.dbName; - if (typeof dbName === 'function') { - name = dbName(name); - } - return name; -}; - -/** - * Get the escaped id column name - * @param {String} model The model name - * @returns {String} the escaped id column name - */ -BaseSQL.prototype.idColumnEscaped = function (model) { - return this.escapeName(this.getDataSource(model).idColumnName(model)); -}; - -/** - * Escape the name for the underlying database - * @param {String} name The name - */ -BaseSQL.prototype.escapeName = function (name) { - throw new Error('escapeName method should be declared in connector'); -}; - -/** - * Get the escaped table name - * @param {String} model The model name - * @returns {String} the escaped table name - */ -BaseSQL.prototype.tableEscaped = function (model) { - return this.escapeName(this.table(model)); -}; - -/** - * Get the escaped column name for a given model property - * @param {String} model The model name - * @param {String} property The property name - * @returns {String} The escaped column name - */ -BaseSQL.prototype.columnEscaped = function (model, property) { - return this.escapeName(this.column(model, property)); -}; - -function isIdValuePresent(idValue, callback, returningNull) { - try { - assert(idValue !== null && idValue !== undefined, 'id value is required'); - return true; - } catch (err) { - process.nextTick(function () { - callback && callback(returningNull ? null: err); - }); - return false; - } -} -/** - * Save the model instance into the backend store - * @param {String} model The model name - * @param {Object} data The model instance data - * @param {Function} callback The callback function - */ -BaseSQL.prototype.save = function (model, data, callback) { - var idName = this.getDataSource(model).idName(model); - var idValue = data[idName]; - - if (!isIdValuePresent(idValue, callback)) { - return; - } - - idValue = this._escapeIdValue(model, idValue); - var sql = 'UPDATE ' + this.tableEscaped(model) + ' SET ' - + this.toFields(model, data) + ' WHERE ' + this.idColumnEscaped(model) + ' = ' - + idValue; - - this.query(sql, function (err, result) { - callback && callback(err, result); - }); -}; - -/** - * Check if a model instance exists for the given id value - * @param {String} model The model name - * @param {*} id The id value - * @param {Function} callback The callback function - */ -BaseSQL.prototype.exists = function (model, id, callback) { - if (!isIdValuePresent(id, callback, true)) { - return; - } - var sql = 'SELECT 1 FROM ' + - this.tableEscaped(model) + ' WHERE ' + this.idColumnEscaped(model) + ' = ' - + this._escapeIdValue(model, id) + ' LIMIT 1'; - - this.query(sql, function (err, data) { - if (err) { - return callback && callback(err); - } - callback && callback(null, data.length >= 1); - }); -}; - -/** - * Find a model instance by id - * @param {String} model The model name - * @param {*} id The id value - * @param {Function} callback The callback function - */ -BaseSQL.prototype.find = function find(model, id, callback) { - if (!isIdValuePresent(id, callback, true)) { - return; - } - var self = this; - var idQuery = this.idColumnEscaped(model) + ' = ' + this._escapeIdValue(model, id); - var sql = 'SELECT * FROM ' + - this.tableEscaped(model) + ' WHERE ' + idQuery + ' LIMIT 1'; - - this.query(sql, function (err, data) { - var result = (data && data.length >= 1) ? data[0] : null; - callback && callback(err, self.fromDatabase(model, result)); - }); -}; - -/** - * Delete a model instance by id value - * @param {String} model The model name - * @param {*} id The id value - * @param {Function} callback The callback function - */ -BaseSQL.prototype.delete = BaseSQL.prototype.destroy = function destroy(model, id, callback) { - if (!isIdValuePresent(id, callback, true)) { - return; - } - var sql = 'DELETE FROM ' + - this.tableEscaped(model) + ' WHERE ' + this.idColumnEscaped(model) + ' = ' - + this._escapeIdValue(model, id); - - this.command(sql, function (err, result) { - callback && callback(err, result); - }); -}; - -BaseSQL.prototype._escapeIdValue = function(model, idValue) { - var idProp = this.getDataSource(model).idProperty(model); - if(typeof this.toDatabase === 'function') { - return this.toDatabase(idProp, idValue); - } else { - if(idProp.type === Number) { - return idValue; - } else { - return "'" + idValue + "'"; - } - } -}; - -/** - * Delete all model instances - * - * @param {String} model The model name - * @param {Function} callback The callback function - */ -BaseSQL.prototype.deleteAll = BaseSQL.prototype.destroyAll = function destroyAll(model, callback) { - this.command('DELETE FROM ' + this.tableEscaped(model), function (err, result) { - callback && callback(err, result); - }); -}; - -/** - * Count all model instances by the where filter - * - * @param {String} model The model name - * @param {Function} callback The callback function - * @param {Object} where The where clause - */ -BaseSQL.prototype.count = function count(model, callback, where) { - var self = this; - var props = this._models[model].properties; - - this.queryOne('SELECT count(*) as cnt FROM ' + - this.tableEscaped(model) + ' ' + buildWhere(where), function (err, res) { - if (err) { - return callback(err); - } - callback(err, res && res.cnt); - }); - - function buildWhere(conds) { - var cs = []; - Object.keys(conds || {}).forEach(function (key) { - var keyEscaped = self.columnEscaped(model, key); - if (conds[key] === null) { - cs.push(keyEscaped + ' IS NULL'); - } else { - cs.push(keyEscaped + ' = ' + self.toDatabase(props[key], conds[key])); - } - }); - return cs.length ? ' WHERE ' + cs.join(' AND ') : ''; - } -}; - -/** - * Update attributes for a given model instance - * @param {String} model The model name - * @param {*} id The id value - * @param {Object} data The model data instance containing all properties to be updated - * @param {Function} cb The callback function - */ -BaseSQL.prototype.updateAttributes = function updateAttrs(model, id, data, cb) { - if (!isIdValuePresent(id, cb)) { - return; - } - var idName = this.getDataSource(model).idName(model); - data[idName] = id; - this.save(model, data, cb); -}; - -/** - * Disconnect from the connector - */ -BaseSQL.prototype.disconnect = function disconnect() { - // No operation -}; - -/** - * Recreate the tables for the given models - * @param {[String]|String} [models] A model name or an array of model names, - * if not present, apply to all models defined in the connector - * @param {Function} [cb] The callback function - */ -BaseSQL.prototype.automigrate = function (models, cb) { - var self = this; - - if ((!cb) && ('function' === typeof models)) { - cb = models; - models = undefined; - } - // First argument is a model name - if ('string' === typeof models) { - models = [models]; - } - - models = models || Object.keys(self._models); - async.each(models, function (model, callback) { - if (model in self._models) { - self.dropTable(model, function (err, result) { - self.createTable(model, function (err, result) { - if (err) { - console.error(err); - } - callback(err, result); - }); - }); - } - }, cb); -}; - -/** - * Drop the table for the given model from the database - * @param {String} model The model name - * @param {Function} [cb] The callback function - */ -BaseSQL.prototype.dropTable = function (model, cb) { - this.command('DROP TABLE IF EXISTS ' + this.tableEscaped(model), cb); -}; - -/** - * Create the table for the given model - * @param {String} model The model name - * @param {Function} [cb] The callback function - */ - -BaseSQL.prototype.createTable = function (model, cb) { - this.command('CREATE TABLE ' + this.tableEscaped(model) + - ' (\n ' + this.propertiesSQL(model) + '\n)', cb); -}; - diff --git a/package.json b/package.json index 97e92efa5..6c6036838 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "dependencies": { "async": "~0.8.0", "inflection": "~1.3.5", + "loopback-connector": "^1.0", "traverse": "~0.6.6", "qs": "~0.6.6", "debug": "~0.8.1" From 487f7c8e03809a5676b1c78096a4e2fd87b68603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 23 May 2014 13:18:06 +0200 Subject: [PATCH 11/11] ModelBuilder: add `prototype.defineValueType` Add a shortcut for registering a new value type. The current implementation registers the type in the singleton registry `ModelBuilder.schemaTypes`. The API should allow us to to change the implementation to register the type in the scope of ModelBuilder instance only. --- lib/model-builder.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/model-builder.js b/lib/model-builder.js index 786978ae3..90601ccf5 100644 --- a/lib/model-builder.js +++ b/lib/model-builder.js @@ -432,6 +432,15 @@ ModelBuilder.prototype.defineProperty = function (model, propertyName, propertyD this.models[model].registerProperty(propertyName); }; +/** + * Define a new value type that can be used in model schemas as a property type. + * @param {function()} type Type constructor. + * @param {string[]=} aliases Optional list of alternative names for this type. + */ +ModelBuilder.prototype.defineValueType = function(type, aliases) { + ModelBuilder.registerType(type, aliases); +}; + /** * Extend existing model with bunch of properties *