diff --git a/CHANGES.md b/CHANGES.md index 6642bc26..8ff138aa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,42 @@ -2014-12-09, Version 2.12.0 +2015-01-07, Version 2.13.0 +========================== + + * added test for sorting undefined values (Christian Vette) + + * Fix the floating number comparison (Raymond Feng) + + * Fix bad CLA URL in CONTRIBUTING.md (Ryan Graham) + + * replace deprecated function __defineGetter__ (bitmage) + + * add a flag to callback of findOrCreate to indicate find or create (Clark Wang) + + * fix sorting of undefined values with multiple columns (Christian Vette) + + * code style (cvette) + + * fix sorting with undefined in memory connector (cvette) + + * Added support for inline parameters like: new GeoPoint(-34, 150) (Simo Moujami) + + * fix default include in default scope fails findById (Clark Wang) + + * Added test for toString() (Simo Moujami) + + * Additional formatting (Simo Moujami) + + * Fixed constructor parameters and added bdd tests for constructor validation (Simo Moujami) + + * Fixed indentation (Simo Moujami) + + * Added mocha tests for GeoPoint (Simo Moujami) + + * renamed intermediary variable (Simo Moujami) + + * Fixed the haversine formula to calculate distance between 2 points properly (Simo Moujami) + + +2014-12-08, Version 2.12.0 ========================== * Relax the id comparison (Raymond Feng) @@ -391,19 +429,16 @@ * Cleanup mixin tests (Fabien Franzen) - * Fix a name conflict in scope metadata (Raymond Feng) - - -2014-08-08, Version 2.3.0 -========================= - - 2014-08-08, Version 2.3.1 ========================= * Fix a name conflict in scope metadata (Raymond Feng) + +2014-08-08, Version 2.3.0 +========================= + * Fix the test case so that it works with other DBs (Raymond Feng) * Bump version (Raymond Feng) @@ -516,6 +551,8 @@ * Implemented embedsMany relation (Fabien Franzen) + * Fix a regression where undefined id should not match any record (Raymond Feng) + * Minor tweaks; pass-through properties/scope for hasAndBelongsToMany (Fabien Franzen) * Implemented polymorphic hasMany through inverse (Fabien Franzen) @@ -531,6 +568,11 @@ * Implemented polymorphic hasMany (Fabien Franzen) +2014-07-27, Version 2.1.0 +========================= + + + 2014-07-27, Version 2.1.1 ========================= @@ -538,12 +580,6 @@ * Fix a regression where undefined id should not match any record (Raymond Feng) - -2014-07-27, Version 2.1.0 -========================= - - * Bump version (Raymond Feng) - * datasource: support connectors without `getTypes` (Miroslav Bajtoš) * relation: add `scope._target` for `hasOne` (Miroslav Bajtoš) @@ -628,6 +664,10 @@ * Add missing inflection dep back (Raymond Feng) + +2014-07-15, Version 2.0.0-beta3 +=============================== + * Bump version (Raymond Feng) * 2.0.0-beta2 (Miroslav Bajtoš) @@ -664,17 +704,12 @@ 2014-07-15, Version 1.7.0 ========================= - - -2014-07-15, Version 2.0.0-beta3 -=============================== - - * Bump version (Raymond Feng) - * Make sure related properties are defined for RDBMS (Raymond Feng) * Test instance or id by the model type (Raymond Feng) + * Bump version (Raymond Feng) + * Allow before hooks to pass arguments to next() (Raymond Feng) * Remoting methods for hasMany through (Raymond Feng) @@ -699,36 +734,6 @@ * DAO.prototype.exists should return 'boolean' type. (Samuel Reed) - * 2.0.0-beta2 (Miroslav Bajtoš) - - * validations: support non-V8 browsers (Miroslav Bajtoš) - - * Remove remoting metadata (Raymond Feng) - - * Fix the forEach closure (Raymond Feng) - - * ModelBuilder: add `prototype.defineValueType` (Miroslav Bajtoš) - - * Replace connector base with loopback-connector (Miroslav Bajtoš) - - * Remove unsupported connectors (Miroslav Bajtoš) - - * 2.0.0-beta1 (Ritchie Martori) - - * Keep undefined/null values for the array type (Raymond Feng) - - * Remove JSDocs for scopeMethods.add(acInst) and scopeMethods.remove(acInst) (crandmck) - - * Copy info from api-model.md to JSDoc (crandmck) - - * !fixup Remove additional remoting (Ritchie Martori) - - * !fixup Require ._delegate for fn override (Ritchie Martori) - - * Remove relation remoting (Ritchie Martori) - - * Remove remoting metadata (Ritchie Martori) - 2014-07-03, Version 1.6.3 ========================= @@ -1091,8 +1096,6 @@ * Simplify the test case (Raymond Feng) - * Revert the inflection version due to regression in camelize (Raymond Feng) - * Add unit test for datatype handling in updateAttributes. (arlaneenalra) * Move new var into thunk. (arlaneenalra) @@ -1100,11 +1103,6 @@ * Use type converted data when writing back to database. (arlaneenalra) -2014-02-11, Version 1.3.0 -========================= - - - 2014-02-11, Version 1.3.1 ========================= @@ -1112,6 +1110,10 @@ * Revert the inflection version due to regression in camelize (Raymond Feng) + +2014-02-11, Version 1.3.0 +========================= + * Bump version and update deps (Raymond Feng) * Add a test case (Raymond Feng) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f091e9e3..f76cb42e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ Contributing to `loopback-datasource-juggler` is easy. In a few simple steps: * Adhere to code style outlined in the [Google C++ Style Guide][] and [Google Javascript Style Guide][]. - * Sign the [Contributor License Agreement](https://cla.strongloop.com/strongloop/loopback-datasource-juggler) + * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/loopback-datasource-juggler) * Submit a pull request through Github. diff --git a/index.js b/index.js index a1e5420e..05001c5f 100644 --- a/index.js +++ b/index.js @@ -4,11 +4,11 @@ exports.ModelBaseClass = require('./lib/model.js'); exports.GeoPoint = require('./lib/geo.js').GeoPoint; exports.ValidationError = require('./lib/validations.js').ValidationError; -exports.__defineGetter__('version', function () { - return require('./package.json').version; +Object.defineProperty(exports, 'version', { + get: function() {return require('./package.json').version;} }); var commonTest = './test/common_test'; -exports.__defineGetter__('test', function () { - return require(commonTest); +Object.defineProperty(exports, 'test', { + get: function() {return require(commonTest);} }); diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index c43e5093..4d98e05e 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -347,13 +347,19 @@ Memory.prototype.all = function all(model, filter, callback) { }); function sorting(a, b) { + var undefinedA, undefinedB; + for (var i = 0, l = this.length; i < l; i++) { - if (a[this[i].key] > b[this[i].key]) { + undefinedB = b[this[i].key] === undefined && a[this[i].key] !== undefined; + undefinedA = a[this[i].key] === undefined && b[this[i].key] !== undefined; + + if (undefinedB || a[this[i].key] > b[this[i].key]) { return 1 * this[i].reverse; - } else if (a[this[i].key] < b[this[i].key]) { + } else if (undefinedA || a[this[i].key] < b[this[i].key]) { return -1 * this[i].reverse; } } + return 0; } }; diff --git a/lib/dao.js b/lib/dao.js index 42a4b9dc..f800d79c 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -58,7 +58,6 @@ function byIdQuery(m, id) { var pk = idName(m); var query = { where: {} }; query.where[pk] = id; - m.applyScope(query); return query; } @@ -317,7 +316,7 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data * @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) + * @param {Function} cb Callback called with (err, instance, created) */ DataAccessObject.findOrCreate = function findOrCreate(query, data, callback) { if (query === undefined) { @@ -335,8 +334,10 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, callback) { var t = this; this.findOne(query, function (err, record) { if (err) return callback(err); - if (record) return callback(null, record); - t.create(data, callback); + if (record) return callback(null, record, false); + t.create(data, function (err, record) { + callback(err, record, record != null); + }); }); }; diff --git a/lib/geo.js b/lib/geo.js index b5b8cd3b..e0fb6964 100644 --- a/lib/geo.js +++ b/lib/geo.js @@ -111,7 +111,11 @@ exports.GeoPoint = GeoPoint; * * @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. + * @property {Number} lng The longitude point in degrees. Range: -180 to 180. + * + * @options {Array} Options Array with two Number entries: [lat,long]. + * @property {Number} lat The latitude point in degrees. Range: -90 to 90. + * @property {Number} lng The longitude point in degrees. Range: -180 to 180. */ function GeoPoint(data) { @@ -119,14 +123,23 @@ function GeoPoint(data) { return new GeoPoint(data); } + if(arguments.length === 2) { + data = { + lat: arguments[0], + lng: arguments[1] + }; + } + + assert(Array.isArray(data) || typeof data === 'object' || typeof data === 'string', 'must provide valid geo-coordinates array [lat, lng] or object or a "lat, lng" string'); + if (typeof data === 'string') { data = data.split(/,\s*/); - assert(data.length === 2, 'must provide a string "lng,lat" creating a GeoPoint with a string'); + assert(data.length === 2, 'must provide a string "lat,lng" creating a GeoPoint with a string'); } if (Array.isArray(data)) { data = { - lng: Number(data[0]), - lat: Number(data[1]) + lat: Number(data[0]), + lng: Number(data[1]) }; } else { data.lng = Number(data.lng); @@ -177,7 +190,7 @@ GeoPoint.distanceBetween = function distanceBetween(a, b, options) { var y2 = b.lng; return geoDistance(x1, y1, x2, y2, options); -} +}; /** * Determine the spherical distance to the given point. @@ -202,26 +215,22 @@ GeoPoint.distanceBetween = function distanceBetween(a, b, options) { GeoPoint.prototype.distanceTo = function (point, options) { return GeoPoint.distanceBetween(this, point, options); -} +}; /** * Simple serialization. */ GeoPoint.prototype.toString = function () { - return this.lng + ',' + this.lat; -} + return this.lat + ',' + this.lng; +}; /** - * @property {Number} PI - Ratio of a circle's circumference to its diameter. * @property {Number} DEG2RAD - Factor to convert degrees to radians. * @property {Number} RAD2DEG - Factor to convert radians to degrees. * @property {Object} EARTH_RADIUS - Radius of the earth. */ -// ratio of a circle's circumference to its diameter -var PI = 3.1415926535897932384626433832795; - // factor to convert degrees to radians var DEG2RAD = 0.01745329252; @@ -239,18 +248,23 @@ var EARTH_RADIUS = { }; function geoDistance(x1, y1, x2, y2, options) { + + var type = (options && options.type) || 'miles'; + // Convert to radians x1 = x1 * DEG2RAD; y1 = y1 * DEG2RAD; x2 = x2 * DEG2RAD; y2 = y2 * DEG2RAD; - var a = Math.pow(Math.sin(( y2 - y1 ) / 2.0), 2); - var b = Math.pow(Math.sin(( x2 - x1 ) / 2.0), 2); - var c = Math.sqrt(a + Math.cos(y2) * Math.cos(y1) * b); + // use the haversine formula to calculate distance for any 2 points on a sphere. + // ref http://en.wikipedia.org/wiki/Haversine_formula + var haversine = function(a) { + return Math.pow(Math.sin(a / 2.0), 2); + }; - var type = (options && options.type) || 'miles'; + var f = Math.sqrt(haversine(x2 - x1) + Math.cos(x2) * Math.cos(x1) * haversine(y2 - y1)); - return 2 * Math.asin(c) * EARTH_RADIUS[type]; + return 2 * Math.asin(f) * EARTH_RADIUS[type]; } diff --git a/package.json b/package.json index 528b7bf3..e15b9492 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "2.12.0", + "version": "2.13.0", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", diff --git a/test/common_test.js b/test/common_test.js index 2f91b5a7..0b251e59 100644 --- a/test/common_test.js +++ b/test/common_test.js @@ -1122,12 +1122,14 @@ function testOrm(dataSource) { it('should find or create', function (test) { var email = 'some email ' + Math.random(); - User.findOrCreate({where: {email: email}}, function (err, u) { + User.findOrCreate({where: {email: email}}, function (err, u, created) { test.ok(u); test.ok(!u.age); - User.findOrCreate({where: {email: email}}, {age: 21}, function (err, u2) { + test.ok(created); + User.findOrCreate({where: {email: email}}, {age: 21}, function (err, u2, created) { test.equals(u.id.toString(), u2.id.toString(), 'Same user ids'); test.ok(!u2.age); + test.ok(!created); test.done(); }); }); diff --git a/test/default-scope.test.js b/test/default-scope.test.js index 0d510cff..ca6c8a9b 100644 --- a/test/default-scope.test.js +++ b/test/default-scope.test.js @@ -2,7 +2,7 @@ var should = require('./init.js'); var async = require('async'); -var db, Category, Product, Tool, Widget, Thing; +var db, Category, Product, Tool, Widget, Thing, Person; // This test requires a connector that can // handle a custom collection or table name @@ -86,6 +86,10 @@ describe('default scope', function () { mongodb: { collection: 'Product' }, memory: { collection: 'Product' } }); + + Person = db.define('Person', { name: String }, { + scope: { include: 'things' } + }); // inst is only valid for instance methods // like save, updateAttributes @@ -115,6 +119,9 @@ describe('default scope', function () { Tool.belongsTo(Category); Widget.belongsTo(Category); Thing.belongsTo(Category); + + Person.hasMany(Thing); + Thing.belongsTo(Person); db.automigrate(done); }); @@ -802,7 +809,33 @@ describe('default scope', function () { }); }); }); - + }); - + + describe('with include option', function() { + + before(function (done) { + db.automigrate(done); + }); + + before(function (done) { + Person.create({ id: 1, name: 'Person A' }, function(err, person) { + person.things.create({ name: 'Thing A' }, done); + }); + }); + + it('should find a scoped instance with included relation - things', function(done) { + Person.findById(1, function(err, person) { + should.not.exist(err); + should.exist(person); + var things = person.things(); + should.exist(things); + things.should.be.an.instanceOf(Array); + things.should.have.length(1); + done(); + }); + }); + + }); + }); diff --git a/test/geo.test.js b/test/geo.test.js new file mode 100644 index 00000000..b552ee5a --- /dev/null +++ b/test/geo.test.js @@ -0,0 +1,139 @@ +/*global describe,it*/ +/*jshint expr:true */ + +require('should'); + +var GeoPoint = require('../lib/geo').GeoPoint; +var DELTA = 0.0000001; + +describe('GeoPoint', function () { + + describe('constructor', function() { + + it('should support a valid array', function () { + var point = new GeoPoint([-34, 150]); + + point.lat.should.equal(-34); + point.lng.should.equal(150); + }); + + it('should support a valid object', function () { + var point = new GeoPoint({ lat: -34, lng: 150 }); + + point.lat.should.equal(-34); + point.lng.should.equal(150); + }); + + it('should support valid string geo coordinates', function () { + var point = new GeoPoint('-34,150'); + + point.lat.should.equal(-34); + point.lng.should.equal(150); + }); + + it('should support coordinates as inline parameters', function () { + var point = new GeoPoint(-34, 150); + + point.lat.should.equal(-34); + point.lng.should.equal(150); + }); + + it('should reject invalid parameters', function () { + /*jshint -W024 */ + var fn = function() { + new GeoPoint('150,-34'); + }; + fn.should.throw(); + + fn = function() { + new GeoPoint('invalid_string'); + }; + fn.should.throw(); + + fn = function() { + new GeoPoint([150, -34]); + }; + fn.should.throw(); + + fn = function() { + new GeoPoint({ + lat: 150, + lng: null + }); + }; + fn.should.throw(); + + fn = function() { + new GeoPoint(150, -34); + }; + fn.should.throw(); + + fn = function() { + new GeoPoint(); + }; + fn.should.throw(); + }); + + }); + + describe('toString()', function() { + + it('should return a string in the form "lat,lng"', function() { + + var point = new GeoPoint({ lat: -34, lng: 150 }); + point.toString().should.equal('-34,150'); + }); + + }); + + describe('distance calculation between two points', function () { + + var here = new GeoPoint({ lat: 40.77492964101182, lng: -73.90950187151662 }); + var there = new GeoPoint({ lat: 40.7753227, lng: -73.909217 }); + + it('should return value in miles by default', function () { + + var distance = GeoPoint.distanceBetween(here, there); + distance.should.be.a.Number; + distance.should.be.approximately(0.03097916611592679, DELTA); + }); + + it('should return value using specified unit', function () { + + /* Supported units: + * - `radians` + * - `kilometers` + * - `meters` + * - `miles` + * - `feet` + * - `degrees` + */ + + var distance = here.distanceTo(there, { type: 'radians'}); + distance.should.be.a.Number; + distance.should.be.approximately(0.000007825491914348416, DELTA); + + distance = here.distanceTo(there, { type: 'kilometers'}); + distance.should.be.a.Number; + distance.should.be.approximately(0.04985613511367009, DELTA); + + distance = here.distanceTo(there, { type: 'meters'}); + distance.should.be.a.Number; + distance.should.be.approximately(49.856135113670085, DELTA); + + distance = here.distanceTo(there, { type: 'miles'}); + distance.should.be.a.Number; + distance.should.be.approximately(0.03097916611592679, DELTA); + + distance = here.distanceTo(there, { type: 'feet'}); + distance.should.be.a.Number; + distance.should.be.approximately(163.56999709209347, DELTA); + + distance = here.distanceTo(there, { type: 'degrees'}); + distance.should.be.a.Number; + distance.should.be.approximately(0.0004483676593058972, DELTA); + }); + + }); + +}); diff --git a/test/memory.test.js b/test/memory.test.js index f07ab015..f3f16fd0 100644 --- a/test/memory.test.js +++ b/test/memory.test.js @@ -183,7 +183,7 @@ describe('Memory connector', function () { }); }); - it('support order with multiple fields', function (done) { + it('should support order with multiple fields', function (done) { User.find({order: 'vip ASC, seq DESC'}, function (err, posts) { should.not.exist(err); posts[0].seq.should.be.eql(4); @@ -192,6 +192,17 @@ describe('Memory connector', function () { }); }); + it('should sort undefined values to the end when ordered DESC', function (done) { + User.find({order: 'vip ASC, order DESC'}, function (err, posts) { + console.log(posts); + should.not.exist(err); + + posts[4].seq.should.be.eql(1); + posts[5].seq.should.be.eql(0); + done(); + }); + }); + it('should throw if order has wrong direction', function (done) { User.find({order: 'seq ABC'}, function (err, posts) { should.exist(err); @@ -200,11 +211,11 @@ describe('Memory connector', function () { }); it('should support neq operator for number', function (done) { - User.find({where: {order: {neq: 6}}}, function (err, users) { + User.find({where: {seq: {neq: 4}}}, function (err, users) { should.not.exist(err); users.length.should.be.equal(5); for (var i = 0; i < users.length; i++) { - users[i].order.should.not.be.equal(6); + users[i].seq.should.not.be.equal(4); } done(); }); @@ -242,7 +253,6 @@ describe('Memory connector', function () { email: 'john@b3atl3s.co.uk', role: 'lead', birthday: new Date('1980-12-08'), - order: 2, vip: true }, {