Skip to content

Commit 4f2c167

Browse files
author
Kevin Delisle
authored
Merge pull request #1153 from strongloop/fix/filter-on-related-model
Apply filter on related model
2 parents b95224b + 6c8e806 commit 4f2c167

File tree

4 files changed

+201
-46
lines changed

4 files changed

+201
-46
lines changed

lib/include.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var includeUtils = require('./include_utils');
1313
var isPlainObject = utils.isPlainObject;
1414
var defineCachedRelations = utils.defineCachedRelations;
1515
var uniq = utils.uniq;
16+
var idName = utils.idName;
1617

1718
/*!
1819
* Normalize the include to be an array
@@ -68,15 +69,6 @@ IncludeScope.prototype.include = function() {
6869
return this._include;
6970
};
7071

71-
/**
72-
* Find the idKey of a Model.
73-
* @param {ModelConstructor} m - Model Constructor
74-
* @returns {String}
75-
*/
76-
function idName(m) {
77-
return m.definition.idName() || 'id';
78-
}
79-
8072
/*!
8173
* Look up a model by name from the list of given models
8274
* @param {Object} models Models keyed by name

lib/scope.js

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ var defineCachedRelations = utils.defineCachedRelations;
1010
var setScopeValuesFromWhere = utils.setScopeValuesFromWhere;
1111
var mergeQuery = utils.mergeQuery;
1212
var DefaultModelBaseClass = require('./model.js');
13+
var collectTargetIds = utils.collectTargetIds;
14+
var idName = utils.idName;
1315

1416
/**
1517
* Module exports
@@ -86,12 +88,50 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres
8688
// It either doesn't hit the cache or refresh is required
8789
var params = mergeQuery(actualCond, scopeParams, {nestedInclude: true});
8890
var targetModel = this.targetModel(receiver);
91+
92+
// If there is a through model
93+
// run another query to apply filter on relatedModel(targetModel)
94+
// see github.com/strongloop/loopback-datasource-juggler/issues/166
95+
var scopeOnRelatedModel = params.collect &&
96+
params.include.scope !== null &&
97+
typeof params.include.scope === 'object';
98+
if (scopeOnRelatedModel) {
99+
var filter = params.include;
100+
// The filter applied on relatedModel
101+
var queryRelated = filter.scope;
102+
delete params.include.scope;
103+
};
104+
89105
targetModel.find(params, options, function(err, data) {
90106
if (!err && saveOnCache) {
91107
defineCachedRelations(self);
92108
self.__cachedRelations[name] = data;
93109
}
94-
cb(err, data);
110+
111+
if (scopeOnRelatedModel === true) {
112+
var relatedModel = targetModel.relations[filter.relation].modelTo;
113+
var IdKey = idName(relatedModel);
114+
115+
// Merge queryRelated filter and targetId filter
116+
var buildWhere = function() {
117+
var IdKeyCondition = {};
118+
IdKeyCondition[IdKey] = collectTargetIds(data, IdKey);
119+
var mergedWhere = {
120+
and: [IdKeyCondition, queryRelated.where],
121+
};
122+
return mergedWhere;
123+
};
124+
if (queryRelated.where !== undefined) {
125+
queryRelated.where = buildWhere();
126+
} else {
127+
queryRelated.where = {};
128+
queryRelated.where[IdKey] = collectTargetIds(data, IdKey);
129+
}
130+
131+
relatedModel.find(queryRelated, cb);
132+
} else {
133+
cb(err, data);
134+
}
95135
});
96136
} else {
97137
// Return from cache
@@ -198,15 +238,6 @@ function defineScope(cls, targetClass, name, params, methods, options) {
198238
// see https://github.com/strongloop/loopback/issues/1076
199239
if (f._scope.collect &&
200240
condOrRefresh !== null && typeof condOrRefresh === 'object') {
201-
//extract the paging filters to the through model
202-
['limit', 'offset', 'skip', 'order'].forEach(function(pagerFilter) {
203-
if (typeof(condOrRefresh[pagerFilter]) !== 'undefined') {
204-
f._scope[pagerFilter] = condOrRefresh[pagerFilter];
205-
delete condOrRefresh[pagerFilter];
206-
}
207-
});
208-
// Adjust the include so that the condition will be applied to
209-
// the target model
210241
f._scope.include = {
211242
relation: f._scope.collect,
212243
scope: condOrRefresh,

lib/utils.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ exports.toRegExp = toRegExp;
2222
exports.hasRegExpFlags = hasRegExpFlags;
2323
exports.idEquals = idEquals;
2424
exports.findIndexOf = findIndexOf;
25+
exports.collectTargetIds = collectTargetIds;
26+
exports.idName = idName;
2527

2628
var g = require('strong-globalize')();
2729
var traverse = require('traverse');
@@ -587,3 +589,30 @@ function findIndexOf(arr, target, isEqual) {
587589

588590
return -1;
589591
}
592+
593+
/**
594+
* Returns an object that queries targetIds.
595+
* @param {Array} The array of targetData
596+
* @param {String} The Id property name of target model
597+
* @returns {Object} The object that queries targetIds
598+
*/
599+
function collectTargetIds(targetData, idPropertyName) {
600+
var targetIds = [];
601+
for (var i = 0; i < targetData.length; i++) {
602+
var targetId = targetData[i][idPropertyName];
603+
targetIds.push(targetId);
604+
};
605+
var IdQuery = {
606+
inq: uniq(targetIds),
607+
};
608+
return IdQuery;
609+
}
610+
611+
/**
612+
* Find the idKey of a Model.
613+
* @param {ModelConstructor} m - Model Constructor
614+
* @returns {String}
615+
*/
616+
function idName(m) {
617+
return m.definition.idName() || 'id';
618+
}

test/relations.test.js

Lines changed: 130 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// This test written in mocha+should.js
77
'use strict';
88
var should = require('./init.js');
9+
var assert = require('assert');
910
var jdb = require('../');
1011
var DataSource = jdb.DataSource;
1112
var createPromiseCallback = require('../lib/utils.js').createPromiseCallback;
@@ -562,7 +563,7 @@ describe('relations', function() {
562563
before(function(done) {
563564
// db = getSchema();
564565
Physician = db.define('Physician', {name: String});
565-
Patient = db.define('Patient', {name: String});
566+
Patient = db.define('Patient', {name: String, age: Number});
566567
Appointment = db.define('Appointment', {date: {type: Date,
567568
default: function() {
568569
return new Date();
@@ -714,40 +715,142 @@ describe('relations', function() {
714715
}
715716
});
716717

717-
it('should fetch scoped instances with paging filters', function(done) {
718-
Physician.create(function(err, physician) {
719-
physician.patients.create({name: 'a'}, function() {
720-
physician.patients.create({name: 'z'}, function() {
721-
physician.patients.create({name: 'c'}, function() {
722-
verify(physician);
718+
describe('fetch scoped instances with paging filters', function() {
719+
var samplePatientId;
720+
var physician;
721+
722+
beforeEach(createSampleData);
723+
724+
context('with filter skip', function() {
725+
it('skips the first patient', function(done) {
726+
physician.patients({skip: 1}, function(err, ch) {
727+
should.not.exist(err);
728+
should.exist(ch);
729+
ch.should.have.lengthOf(2);
730+
ch[0].name.should.eql('z');
731+
ch[1].name.should.eql('c');
732+
done();
733+
});
734+
});
735+
});
736+
context('with filter order', function() {
737+
it('orders the result by patient name', function(done) {
738+
physician.patients({order: 'name DESC'}, function(err, ch) {
739+
should.not.exist(err);
740+
should.exist(ch);
741+
ch.should.have.lengthOf(3);
742+
ch[0].name.should.eql('z');
743+
ch[2].name.should.eql('a');
744+
done();
745+
});
746+
});
747+
});
748+
context('with filter limit', function() {
749+
it('limits to 1 result', function(done) {
750+
physician.patients({limit: 1}, function(err, ch) {
751+
should.not.exist(err);
752+
should.exist(ch);
753+
ch.should.have.lengthOf(1);
754+
ch[0].name.should.eql('a');
755+
done();
756+
});
757+
});
758+
});
759+
context('with filter fields', function() {
760+
it('includes field \'name\' but not \'age\'', function(done) {
761+
var fieldsFilter = {fields: {name: true, age: false}};
762+
physician.patients(fieldsFilter, function(err, ch) {
763+
should.not.exist(err);
764+
should.exist(ch);
765+
should.exist(ch[0].name);
766+
ch[0].name.should.eql('a');
767+
should.not.exist(ch[0].age);
768+
done();
769+
});
770+
});
771+
});
772+
context('with filter include', function() {
773+
it('returns physicians inluced in patient', function(done) {
774+
var includeFilter = {include: 'physicians'};
775+
physician.patients(includeFilter, function(err, ch) {
776+
should.not.exist(err);
777+
ch.should.have.lengthOf(3);
778+
should.exist(ch[0].physicians);
779+
done();
780+
});
781+
});
782+
});
783+
context('with filter where', function() {
784+
it('returns patient where id equal to samplePatientId', function(done) {
785+
var whereFilter = {where: {id: samplePatientId}};
786+
physician.patients(whereFilter, function(err, ch) {
787+
should.not.exist(err);
788+
should.exist(ch);
789+
ch.should.have.lengthOf(1);
790+
ch[0].id.should.eql(samplePatientId);
791+
done();
792+
});
793+
});
794+
it('returns patients where id in an array', function(done) {
795+
var idArr = [];
796+
var whereFilter;
797+
physician.patients.create({name: 'b'}, function(err, p) {
798+
idArr.push(samplePatientId, p.id);
799+
whereFilter = {where: {id: {inq: idArr}}};
800+
physician.patients(whereFilter, function(err, ch) {
801+
should.not.exist(err);
802+
should.exist(ch);
803+
ch.should.have.lengthOf(2);
804+
var resultIdArr = [ch[0].id, ch[1].id];
805+
assert.deepEqual(resultIdArr, idArr);
806+
done();
723807
});
724808
});
725809
});
726810
});
727-
function verify(physician) {
728-
//limit plus skip
729-
physician.patients({limit: 1, skip: 1}, function(err, ch) {
730-
should.not.exist(err);
731-
should.exist(ch);
732-
ch.should.have.lengthOf(1);
733-
ch[0].name.should.eql('z');
734-
//offset plus skip
735-
physician.patients({limit: 1, offset: 1}, function(err1, ch1) {
736-
should.not.exist(err1);
737-
should.exist(ch1);
738-
ch1.should.have.lengthOf(1);
739-
ch1[0].name.should.eql('z');
740-
//order
741-
physician.patients({order: 'patientId DESC'}, function(err2, ch2) {
742-
should.not.exist(err2);
743-
should.exist(ch2);
744-
ch2.should.have.lengthOf(3);
745-
ch2[0].name.should.eql('c');
811+
context('findById with filter include', function() {
812+
it('returns patient where id equal to \'samplePatientId\'' +
813+
'with included physicians', function(done) {
814+
var includeFilter = {include: 'physicians'};
815+
physician.patients.findById(samplePatientId,
816+
includeFilter, function(err, ch) {
817+
should.not.exist(err);
818+
should.exist(ch);
819+
ch.id.should.eql(samplePatientId);
820+
should.exist(ch.physicians);
821+
done();
822+
});
823+
});
824+
});
825+
context('findById with filter fields', function() {
826+
it('returns patient where id equal to \'samplePatientId\'' +
827+
'with field \'name\' but not \'age\'', function(done) {
828+
var fieldsFilter = {fields: {name: true, age: false}};
829+
physician.patients.findById(samplePatientId,
830+
fieldsFilter, function(err, ch) {
831+
should.not.exist(err);
832+
should.exist(ch);
833+
should.exist(ch.name);
834+
ch.name.should.eql('a');
835+
should.not.exist(ch.age);
746836
done();
747837
});
838+
});
839+
});
840+
841+
function createSampleData(done) {
842+
Physician.create(function(err, result) {
843+
result.patients.create({name: 'a', age: '10'}, function(err, p) {
844+
samplePatientId = p.id;
845+
result.patients.create({name: 'z', age: '20'}, function() {
846+
result.patients.create({name: 'c'}, function() {
847+
physician = result;
848+
done();
849+
});
850+
});
748851
});
749852
});
750-
}
853+
};
751854
});
752855

753856
it('should find scoped record', function(done) {

0 commit comments

Comments
 (0)