Apply hasManyThrough filter on target model
This commit is contained in:
parent
b95224bd83
commit
6c8e806bc8
|
@ -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
|
||||||
|
|
51
lib/scope.js
51
lib/scope.js
|
@ -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,
|
||||||
|
|
29
lib/utils.js
29
lib/utils.js
|
@ -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';
|
||||||
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue