Apply hasManyThrough filter on target model

This commit is contained in:
jannyHou 2016-10-27 10:18:54 -04:00
parent b95224bd83
commit 6c8e806bc8
4 changed files with 202 additions and 47 deletions

View File

@ -13,6 +13,7 @@ var includeUtils = require('./include_utils');
var isPlainObject = utils.isPlainObject; var isPlainObject = utils.isPlainObject;
var defineCachedRelations = utils.defineCachedRelations; var defineCachedRelations = utils.defineCachedRelations;
var uniq = utils.uniq; var uniq = utils.uniq;
var idName = utils.idName;
/*! /*!
* Normalize the include to be an array * Normalize the include to be an array
@ -68,15 +69,6 @@ IncludeScope.prototype.include = function() {
return this._include; return this._include;
}; };
/**
* Find the idKey of a Model.
* @param {ModelConstructor} m - Model Constructor
* @returns {String}
*/
function idName(m) {
return m.definition.idName() || 'id';
}
/*! /*!
* Look up a model by name from the list of given models * Look up a model by name from the list of given models
* @param {Object} models Models keyed by name * @param {Object} models Models keyed by name

View File

@ -10,6 +10,8 @@ var defineCachedRelations = utils.defineCachedRelations;
var setScopeValuesFromWhere = utils.setScopeValuesFromWhere; var setScopeValuesFromWhere = utils.setScopeValuesFromWhere;
var mergeQuery = utils.mergeQuery; var mergeQuery = utils.mergeQuery;
var DefaultModelBaseClass = require('./model.js'); var DefaultModelBaseClass = require('./model.js');
var collectTargetIds = utils.collectTargetIds;
var idName = utils.idName;
/** /**
* Module exports * Module exports
@ -86,12 +88,50 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres
// It either doesn't hit the cache or refresh is required // It either doesn't hit the cache or refresh is required
var params = mergeQuery(actualCond, scopeParams, {nestedInclude: true}); var params = mergeQuery(actualCond, scopeParams, {nestedInclude: true});
var targetModel = this.targetModel(receiver); var targetModel = this.targetModel(receiver);
// If there is a through model
// run another query to apply filter on relatedModel(targetModel)
// see github.com/strongloop/loopback-datasource-juggler/issues/166
var scopeOnRelatedModel = params.collect &&
params.include.scope !== null &&
typeof params.include.scope === 'object';
if (scopeOnRelatedModel) {
var filter = params.include;
// The filter applied on relatedModel
var queryRelated = filter.scope;
delete params.include.scope;
};
targetModel.find(params, options, function(err, data) { targetModel.find(params, options, function(err, data) {
if (!err && saveOnCache) { if (!err && saveOnCache) {
defineCachedRelations(self); defineCachedRelations(self);
self.__cachedRelations[name] = data; self.__cachedRelations[name] = data;
} }
cb(err, data);
if (scopeOnRelatedModel === true) {
var relatedModel = targetModel.relations[filter.relation].modelTo;
var IdKey = idName(relatedModel);
// Merge queryRelated filter and targetId filter
var buildWhere = function() {
var IdKeyCondition = {};
IdKeyCondition[IdKey] = collectTargetIds(data, IdKey);
var mergedWhere = {
and: [IdKeyCondition, queryRelated.where],
};
return mergedWhere;
};
if (queryRelated.where !== undefined) {
queryRelated.where = buildWhere();
} else {
queryRelated.where = {};
queryRelated.where[IdKey] = collectTargetIds(data, IdKey);
}
relatedModel.find(queryRelated, cb);
} else {
cb(err, data);
}
}); });
} else { } else {
// Return from cache // Return from cache
@ -198,15 +238,6 @@ function defineScope(cls, targetClass, name, params, methods, options) {
// see https://github.com/strongloop/loopback/issues/1076 // see https://github.com/strongloop/loopback/issues/1076
if (f._scope.collect && if (f._scope.collect &&
condOrRefresh !== null && typeof condOrRefresh === 'object') { condOrRefresh !== null && typeof condOrRefresh === 'object') {
//extract the paging filters to the through model
['limit', 'offset', 'skip', 'order'].forEach(function(pagerFilter) {
if (typeof(condOrRefresh[pagerFilter]) !== 'undefined') {
f._scope[pagerFilter] = condOrRefresh[pagerFilter];
delete condOrRefresh[pagerFilter];
}
});
// Adjust the include so that the condition will be applied to
// the target model
f._scope.include = { f._scope.include = {
relation: f._scope.collect, relation: f._scope.collect,
scope: condOrRefresh, scope: condOrRefresh,

View File

@ -22,6 +22,8 @@ exports.toRegExp = toRegExp;
exports.hasRegExpFlags = hasRegExpFlags; exports.hasRegExpFlags = hasRegExpFlags;
exports.idEquals = idEquals; exports.idEquals = idEquals;
exports.findIndexOf = findIndexOf; exports.findIndexOf = findIndexOf;
exports.collectTargetIds = collectTargetIds;
exports.idName = idName;
var g = require('strong-globalize')(); var g = require('strong-globalize')();
var traverse = require('traverse'); var traverse = require('traverse');
@ -587,3 +589,30 @@ function findIndexOf(arr, target, isEqual) {
return -1; return -1;
} }
/**
* Returns an object that queries targetIds.
* @param {Array} The array of targetData
* @param {String} The Id property name of target model
* @returns {Object} The object that queries targetIds
*/
function collectTargetIds(targetData, idPropertyName) {
var targetIds = [];
for (var i = 0; i < targetData.length; i++) {
var targetId = targetData[i][idPropertyName];
targetIds.push(targetId);
};
var IdQuery = {
inq: uniq(targetIds),
};
return IdQuery;
}
/**
* Find the idKey of a Model.
* @param {ModelConstructor} m - Model Constructor
* @returns {String}
*/
function idName(m) {
return m.definition.idName() || 'id';
}

View File

@ -6,6 +6,7 @@
// This test written in mocha+should.js // This test written in mocha+should.js
'use strict'; 'use strict';
var should = require('./init.js'); var should = require('./init.js');
var assert = require('assert');
var jdb = require('../'); var jdb = require('../');
var DataSource = jdb.DataSource; var DataSource = jdb.DataSource;
var createPromiseCallback = require('../lib/utils.js').createPromiseCallback; var createPromiseCallback = require('../lib/utils.js').createPromiseCallback;
@ -562,7 +563,7 @@ describe('relations', function() {
before(function(done) { before(function(done) {
// db = getSchema(); // db = getSchema();
Physician = db.define('Physician', {name: String}); Physician = db.define('Physician', {name: String});
Patient = db.define('Patient', {name: String}); Patient = db.define('Patient', {name: String, age: Number});
Appointment = db.define('Appointment', {date: {type: Date, Appointment = db.define('Appointment', {date: {type: Date,
default: function() { default: function() {
return new Date(); return new Date();
@ -714,40 +715,142 @@ describe('relations', function() {
} }
}); });
it('should fetch scoped instances with paging filters', function(done) { describe('fetch scoped instances with paging filters', function() {
Physician.create(function(err, physician) { var samplePatientId;
physician.patients.create({name: 'a'}, function() { var physician;
physician.patients.create({name: 'z'}, function() {
physician.patients.create({name: 'c'}, function() { beforeEach(createSampleData);
verify(physician);
}); context('with filter skip', function() {
it('skips the first patient', function(done) {
physician.patients({skip: 1}, function(err, ch) {
should.not.exist(err);
should.exist(ch);
ch.should.have.lengthOf(2);
ch[0].name.should.eql('z');
ch[1].name.should.eql('c');
done();
}); });
}); });
}); });
function verify(physician) { context('with filter order', function() {
//limit plus skip it('orders the result by patient name', function(done) {
physician.patients({limit: 1, skip: 1}, function(err, ch) { physician.patients({order: 'name DESC'}, function(err, ch) {
should.not.exist(err); should.not.exist(err);
should.exist(ch); should.exist(ch);
ch.should.have.lengthOf(1); ch.should.have.lengthOf(3);
ch[0].name.should.eql('z'); ch[0].name.should.eql('z');
//offset plus skip ch[2].name.should.eql('a');
physician.patients({limit: 1, offset: 1}, function(err1, ch1) { done();
should.not.exist(err1); });
should.exist(ch1); });
ch1.should.have.lengthOf(1); });
ch1[0].name.should.eql('z'); context('with filter limit', function() {
//order it('limits to 1 result', function(done) {
physician.patients({order: 'patientId DESC'}, function(err2, ch2) { physician.patients({limit: 1}, function(err, ch) {
should.not.exist(err2); should.not.exist(err);
should.exist(ch2); should.exist(ch);
ch2.should.have.lengthOf(3); ch.should.have.lengthOf(1);
ch2[0].name.should.eql('c'); ch[0].name.should.eql('a');
done();
});
});
});
context('with filter fields', function() {
it('includes field \'name\' but not \'age\'', function(done) {
var fieldsFilter = {fields: {name: true, age: false}};
physician.patients(fieldsFilter, function(err, ch) {
should.not.exist(err);
should.exist(ch);
should.exist(ch[0].name);
ch[0].name.should.eql('a');
should.not.exist(ch[0].age);
done();
});
});
});
context('with filter include', function() {
it('returns physicians inluced in patient', function(done) {
var includeFilter = {include: 'physicians'};
physician.patients(includeFilter, function(err, ch) {
should.not.exist(err);
ch.should.have.lengthOf(3);
should.exist(ch[0].physicians);
done();
});
});
});
context('with filter where', function() {
it('returns patient where id equal to samplePatientId', function(done) {
var whereFilter = {where: {id: samplePatientId}};
physician.patients(whereFilter, function(err, ch) {
should.not.exist(err);
should.exist(ch);
ch.should.have.lengthOf(1);
ch[0].id.should.eql(samplePatientId);
done();
});
});
it('returns patients where id in an array', function(done) {
var idArr = [];
var whereFilter;
physician.patients.create({name: 'b'}, function(err, p) {
idArr.push(samplePatientId, p.id);
whereFilter = {where: {id: {inq: idArr}}};
physician.patients(whereFilter, function(err, ch) {
should.not.exist(err);
should.exist(ch);
ch.should.have.lengthOf(2);
var resultIdArr = [ch[0].id, ch[1].id];
assert.deepEqual(resultIdArr, idArr);
done(); done();
}); });
}); });
}); });
} });
context('findById with filter include', function() {
it('returns patient where id equal to \'samplePatientId\'' +
'with included physicians', function(done) {
var includeFilter = {include: 'physicians'};
physician.patients.findById(samplePatientId,
includeFilter, function(err, ch) {
should.not.exist(err);
should.exist(ch);
ch.id.should.eql(samplePatientId);
should.exist(ch.physicians);
done();
});
});
});
context('findById with filter fields', function() {
it('returns patient where id equal to \'samplePatientId\'' +
'with field \'name\' but not \'age\'', function(done) {
var fieldsFilter = {fields: {name: true, age: false}};
physician.patients.findById(samplePatientId,
fieldsFilter, function(err, ch) {
should.not.exist(err);
should.exist(ch);
should.exist(ch.name);
ch.name.should.eql('a');
should.not.exist(ch.age);
done();
});
});
});
function createSampleData(done) {
Physician.create(function(err, result) {
result.patients.create({name: 'a', age: '10'}, function(err, p) {
samplePatientId = p.id;
result.patients.create({name: 'z', age: '20'}, function() {
result.patients.create({name: 'c'}, function() {
physician = result;
done();
});
});
});
});
};
}); });
it('should find scoped record', function(done) { it('should find scoped record', function(done) {