diff --git a/lib/dao.js b/lib/dao.js index 224e6cdf..279b05ae 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -10,7 +10,6 @@ var util = require('util'); var jutil = require('./jutil'); var validations = require('./validations.js'); var ValidationError = validations.ValidationError; -var List = require('./list.js'); require('./relations.js'); var Inclusion = require('./include.js'); var Relation = require('./relations.js'); @@ -351,6 +350,83 @@ DataAccessObject.all = function () { DataAccessObject.find.apply(this, arguments); }; +var operators = { + gt: '>', + gte: '>=', + lt: '<', + lte: '<=', + between: 'BETWEEN', + inq: 'IN', + nin: 'NOT IN', + neq: '!=', + like: 'LIKE', + nlike: 'NOT LIKE' +}; + +DataAccessObject._coerce = function (where) { + if (!where) { + return where; + } + + var props = this.getDataSource().getModelDefinition(this.modelName).properties; + for (var p in where) { + var DataType = props[p] && props[p].type; + if (!DataType) { + continue; + } + if (Array.isArray(DataType) || DataType === Array) { + DataType = DataType[0]; + } + if (DataType === Date) { + var OrigDate = Date; + DataType = function Date(arg) { + return new OrigDate(arg); + }; + } else if (DataType === Boolean) { + DataType = function(val) { + if(val === 'true') { + return true; + } else if(val === 'false') { + return false; + } else { + return Boolean(val); + } + }; + } + if (!DataType) { + continue; + } + var val = where[p]; + // Check there is an operator + var operator = null; + if ('object' === typeof val && Object.keys(val).length === 1) { + for (var op in operators) { + if (op in val) { + val = val[op]; + operator = op; + break; + } + } + } + // Coerce the array items + if (Array.isArray(val)) { + for (var i = 0; i < val.length; i++) { + val[i] = DataType(val[i]); + } + } else { + val = DataType(val); + } + // Rebuild {property: {operator: value}} + if (operator) { + var value = {}; + value[operator] = val; + val = value; + } + where[p] = val; + } + return where; +}; + /** * Find all instances of Model, matched by query * make sure you have marked as `index: true` fields for filter or sort @@ -389,6 +465,9 @@ DataAccessObject.find = function find(params, cb) { } params = removeUndefined(params); + if(params.where) { + params.where = this._coerce(params.where); + } if(near) { if(supportsGeo) { // convert it @@ -511,6 +590,7 @@ DataAccessObject.destroyAll = function destroyAll(where, cb) { } else { // Support an optional where object where = removeUndefined(where); + where = this._coerce(where); this.getDataSource().connector.destroyAll(this.modelName, where, function (err, data) { cb && cb(err, data); }.bind(this)); @@ -556,6 +636,7 @@ DataAccessObject.count = function (where, cb) { where = null; } where = removeUndefined(where); + where = this._coerce(where); this.getDataSource().connector.count(this.modelName, cb, where); }; diff --git a/lib/model-builder.js b/lib/model-builder.js index b152bbf6..89cd6793 100644 --- a/lib/model-builder.js +++ b/lib/model-builder.js @@ -9,6 +9,7 @@ var assert = require('assert'); var DefaultModelBaseClass = require('./model.js'); var List = require('./list.js'); var ModelDefinition = require('./model-definition.js'); +var mergeSettings = require('./utils').mergeSettings; // Set up types require('./types')(ModelBuilder); @@ -283,11 +284,15 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett }); // 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); diff --git a/lib/utils.js b/lib/utils.js index 56a24334..2dd6da47 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -3,6 +3,7 @@ exports.fieldsToArray = fieldsToArray; exports.selectFields = selectFields; exports.removeUndefined = removeUndefined; exports.parseSettings = parseSettings; +exports.mergeSettings = mergeSettings; var traverse = require('traverse'); @@ -127,3 +128,53 @@ function parseSettings(urlStr) { } return settings; } + +/** + * Merge model settings + * + * Folked from https://github.com/nrf110/deepmerge/blob/master/index.js + * + * The original function tries to merge array items if they are objects + * + * @param {Object} target The target settings object + * @param {Object} src The source settings object + * @returns {Object} The merged settings object + */ +function mergeSettings(target, src) { + var array = Array.isArray(src); + var dst = array && [] || {}; + + if (array) { + target = target || []; + dst = dst.concat(target); + src.forEach(function (e, i) { + if (typeof target[i] === 'undefined') { + dst[i] = e; + } else { + if (target.indexOf(e) === -1) { + dst.push(e); + } + } + }); + } else { + if (target && typeof target === 'object') { + Object.keys(target).forEach(function (key) { + dst[key] = target[key]; + }); + } + Object.keys(src).forEach(function (key) { + if (typeof src[key] !== 'object' || !src[key]) { + dst[key] = src[key]; + } + else { + if (!target[key]) { + dst[key] = src[key] + } else { + dst[key] = mergeSettings(target[key], src[key]) + } + } + }); + } + + return dst; +} \ No newline at end of file diff --git a/package.json b/package.json index 34712bfc..e276cb1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "1.2.6", + "version": "1.2.7", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index 75c0115e..a000e8de 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -572,6 +572,83 @@ describe('Load models with relations', function () { }); +describe('DataAccessObject', function () { + var ds, model, where; + + before(function () { + ds = new DataSource('memory'); + model = ds.createModel('M1', { + id: {type: String, id: true}, + age: Number, + vip: Boolean, + date: Date, + scores: [Number] + }); + }); + + it('should be able to coerce where clause for string types', function () { + where = model._coerce({id: 1}); + assert.deepEqual(where, {id: '1'}); + where = model._coerce({id: '1'}); + assert.deepEqual(where, {id: '1'}); + }); + + it('should be able to coerce where clause for number types', function () { + where = model._coerce({age: '10'}); + assert.deepEqual(where, {age: 10}); + + where = model._coerce({age: 10}); + assert.deepEqual(where, {age: 10}); + + where = model._coerce({age: {gt: 10}}); + assert.deepEqual(where, {age: {gt: 10}}); + + where = model._coerce({age: {gt: '10'}}); + assert.deepEqual(where, {age: {gt: 10}}); + + where = model._coerce({age: {between: ['10', '20']}}); + assert.deepEqual(where, {age: {between: [10, 20]}}); + }); + + it('should be able to coerce where clause for array types', function () { + where = model._coerce({scores: ['10', '20']}); + assert.deepEqual(where, {scores: [10, 20]}); + }); + + it('should be able to coerce where clause for date types', function () { + var d = new Date(); + where = model._coerce({date: d}); + assert.deepEqual(where, {date: d}); + + where = model._coerce({date: d.toISOString()}); + assert.deepEqual(where, {date: d}); + }); + + it('should be able to coerce where clause for boolean types', function () { + where = model._coerce({vip: 'true'}); + assert.deepEqual(where, {vip: true}); + + where = model._coerce({vip: true}); + assert.deepEqual(where, {vip: true}); + + where = model._coerce({vip: 'false'}); + assert.deepEqual(where, {vip: false}); + + where = model._coerce({vip: false}); + assert.deepEqual(where, {vip: false}); + + where = model._coerce({vip: '1'}); + assert.deepEqual(where, {vip: true}); + + where = model._coerce({vip: 0}); + assert.deepEqual(where, {vip: false}); + + where = model._coerce({vip: ''}); + assert.deepEqual(where, {vip: false}); + + }); +}); + describe('Load models from json', function () { it('should be able to define models from json', function () { var path = require('path'), @@ -649,6 +726,96 @@ describe('Load models from json', function () { done(null, customer); }); + + it('should be able to extend models with merged settings', function (done) { + var modelBuilder = new ModelBuilder(); + + var User = modelBuilder.define('User', { + name: String + }, { + defaultPermission: 'ALLOW', + acls: [ + { + principalType: 'ROLE', + principalId: '$everyone', + permission: 'ALLOW' + } + ], + relations: { + posts: { + type: 'hasMany', + model:'Post' + } + } + }); + + var Customer = User.extend('Customer', + {customerId: {type: String, id: true}}, + { + defaultPermission: 'DENY', + acls: [ + { + principalType: 'ROLE', + principalId: '$unauthenticated', + permission: 'DENY' + } + ], + relations: { + orders: { + type: 'hasMany', + model:'Order' + } + } + } + ); + + assert.deepEqual(User.settings, { + defaultPermission: 'ALLOW', + acls: [ + { + principalType: 'ROLE', + principalId: '$everyone', + permission: 'ALLOW' + } + ], + relations: { + posts: { + type: 'hasMany', + model:'Post' + } + }, + strict: false + }); + + assert.deepEqual(Customer.settings, { + defaultPermission: 'DENY', + acls: [ + { + principalType: 'ROLE', + principalId: '$everyone', + permission: 'ALLOW' + }, + { + principalType: 'ROLE', + principalId: '$unauthenticated', + permission: 'DENY' + } + ], + relations: { + posts: { + type: 'hasMany', + model:'Post' + }, + orders: { + type: 'hasMany', + model:'Order' + } + }, + strict: false + }); + + done(); + }); }); describe('DataSource constructor', function(){