Skip to content

Commit

Permalink
Fix bug #1795 Count issues related models
Browse files Browse the repository at this point in the history
  • Loading branch information
regevbr committed Dec 7, 2019
1 parent d19e189 commit 7c98ecb
Show file tree
Hide file tree
Showing 2 changed files with 654 additions and 54 deletions.
290 changes: 236 additions & 54 deletions lib/scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres
actualRefresh;
if (refreshIsNeeded) {
// It either doesn't hit the cache or refresh is required
const params = mergeQuery(actualCond, scopeParams, {nestedInclude: true});
let params = mergeQuery(actualCond, scopeParams, {nestedInclude: true});
const targetModel = this.targetModel(receiver);

// If there is a through model
Expand All @@ -100,64 +100,39 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres
const scopeOnRelatedModel = params.collect &&
params.include.scope !== null &&
typeof params.include.scope === 'object';
let filter, queryRelated;
let filter, queryRelated, keyFrom, relatedModel, IdKey;
if (scopeOnRelatedModel) {
filter = params.include;
// The filter applied on relatedModel
queryRelated = filter.scope;
delete params.include.scope;
delete params.include;
relatedModel = targetModel.relations[filter.relation].modelTo;
IdKey = idName(relatedModel);
keyFrom = targetModel.relations[filter.relation].keyFrom || IdKey;
params.fields = [keyFrom];
if (queryRelated && queryRelated.where && queryRelated.where[IdKey]) {
params = mergeQuery(params, {
where: {
[keyFrom]: queryRelated.where [IdKey],
},
});
}
}

targetModel.find(params, options, function(err, data) {
targetModel.find(params, options, function(err, findData) {
if (!err && saveOnCache) {
defineCachedRelations(self);
self.__cachedRelations[name] = data;
self.__cachedRelations[name] = findData;
}

if (scopeOnRelatedModel === true) {
const relatedModel = targetModel.relations[filter.relation].modelTo;
const IdKey = idName(relatedModel);

// return {inq: [1,2,3]}}
const smartMerge = function(idCollection, qWhere) {
if (!qWhere[IdKey]) return idCollection;
let merged = {};

const idsA = idCollection.inq;
const idsB = qWhere[IdKey].inq ? qWhere[IdKey].inq : [qWhere[IdKey]];

const intersect = _.intersectionWith(idsA, idsB, _.isEqual);
if (intersect.length === 1) merged = intersect[0];
if (intersect.length > 1) merged = {inq: intersect};

return merged;
};

if (queryRelated.where !== undefined) {
// Merge queryRelated filter and targetId filter
const IdKeyCondition = {};
IdKeyCondition[IdKey] = smartMerge(collectTargetIds(data, IdKey),
queryRelated.where);

// if the id in filter doesn't exist after the merge,
// return empty result
if (_.isObject(IdKeyCondition[IdKey]) && _.isEmpty(IdKeyCondition[IdKey])) return cb(null, []);

const mergedWhere = {
and: [
IdKeyCondition,
_.omit(queryRelated.where, IdKey),
],
};
queryRelated.where = mergedWhere;
} else {
queryRelated.where = {};
queryRelated.where[IdKey] = collectTargetIds(data, IdKey);
const smartMergeSuccessful = smartMergeRelatedModelQuery(findData, queryRelated, keyFrom, IdKey);
if (!smartMergeSuccessful) {
return cb(null, []);
}

relatedModel.find(queryRelated, options, cb);
} else {
cb(err, data);
cb(err, findData);
}
});
} else {
Expand Down Expand Up @@ -298,6 +273,16 @@ function defineScope(cls, targetClass, name, params, methods, options) {
options = {};
}
options = options || {};
// Check if there is a through model
// see https://github.com/strongloop/loopback/issues/1076
if (f._scope.collect &&
condOrRefresh !== null && typeof condOrRefresh === 'object') {
f._scope.include = {
relation: f._scope.collect,
scope: condOrRefresh,
};
condOrRefresh = {};
}
return definition.related(self, f._scope, condOrRefresh, options, cb);
};

Expand Down Expand Up @@ -431,11 +416,49 @@ function defineScope(cls, targetClass, name, params, methods, options) {
options = {};
}
options = options || {};

cb = cb || utils.createPromiseCallback();
const targetModel = definition.targetModel(this._receiver);
// If there is a through model
// run another query to apply filter on relatedModel(targetModel)
// see github.com/strongloop/loopback-datasource-juggler/issues/1795
let scopeOnRelatedModel = false;
let queryRelated, keyFrom, relatedModel, IdKey, fieldsRelated;
if (this._scope && this._scope.collect &&
where !== null && typeof where === 'object') {
queryRelated = {
relation: this._scope.collect,
scope: {
where: where,
},
};
scopeOnRelatedModel = true;
relatedModel = targetModel.relations[queryRelated.relation].modelTo;
IdKey = idName(relatedModel);
keyFrom = targetModel.relations[queryRelated.relation].keyFrom || IdKey;
fieldsRelated = [keyFrom];
if (where[IdKey]) {
where = {
[keyFrom]: where[IdKey],
};
} else {
where = {};
}
}
const scoped = (this._scope && this._scope.where) || {};
const filter = mergeQuery({where: scoped}, {where: where || {}});
return targetModel.destroyAll(filter.where, options, cb);
const filter = mergeQuery({where: scoped}, {where: where || {}, fields: fieldsRelated});
if (!scopeOnRelatedModel) {
targetModel.destroyAll(filter.where, options, cb);
} else {
targetModel.find(filter, options, function(err, findData) {
const smartMergeSuccessful =
smartMergeRelatedModelQuery(findData, queryRelated.scope, keyFrom, IdKey);
if (!smartMergeSuccessful) {
return cb(null, {count: 0});
}
return relatedModel.destroyAll(queryRelated.scope.where, options, cb);
});
}
return cb.promise;
}

function updateAll(where, data, options, cb) {
Expand All @@ -452,10 +475,49 @@ function defineScope(cls, targetClass, name, params, methods, options) {
options = {};
}
options = options || {};
cb = cb || utils.createPromiseCallback();
const targetModel = definition.targetModel(this._receiver);
// If there is a through model
// run another query to apply filter on relatedModel(targetModel)
// see github.com/strongloop/loopback-datasource-juggler/issues/1795
let scopeOnRelatedModel = false;
let queryRelated, keyFrom, relatedModel, IdKey, fieldsRelated;
if (this._scope && this._scope.collect &&
where !== null && typeof where === 'object') {
queryRelated = {
relation: this._scope.collect,
scope: {
where: where,
},
};
scopeOnRelatedModel = true;
relatedModel = targetModel.relations[queryRelated.relation].modelTo;
IdKey = idName(relatedModel);
keyFrom = targetModel.relations[queryRelated.relation].keyFrom || IdKey;
fieldsRelated = [keyFrom];
if (where[IdKey]) {
where = {
[keyFrom]: where[IdKey],
};
} else {
where = {};
}
}
const scoped = (this._scope && this._scope.where) || {};
const filter = mergeQuery({where: scoped}, {where: where || {}});
return targetModel.updateAll(filter.where, data, options, cb);
const filter = mergeQuery({where: scoped}, {where: where || {}, fields: fieldsRelated});
if (!scopeOnRelatedModel) {
targetModel.updateAll(filter.where, data, options, cb);
} else {
targetModel.find(filter, options, function(err, findData) {
const smartMergeSuccessful =
smartMergeRelatedModelQuery(findData, queryRelated.scope, keyFrom, IdKey);
if (!smartMergeSuccessful) {
return cb(null, {count: 0});
}
return relatedModel.updateAll(queryRelated.scope.where, data, options, cb);
});
}
return cb.promise;
}

function findById(id, filter, options, cb) {
Expand Down Expand Up @@ -500,10 +562,51 @@ function defineScope(cls, targetClass, name, params, methods, options) {
options = {};
}
options = options || {};
cb = cb || utils.createPromiseCallback();
const targetModel = definition.targetModel(this._receiver);
// If there is a through model
// run another query to apply filter on relatedModel(targetModel)
// see github.com/strongloop/loopback-datasource-juggler/issues/1795
let scopeOnRelatedModel = false;
let queryRelated, keyFrom, relatedModel, IdKey;
if (this._scope && this._scope.collect &&
filter && filter.where !== null && typeof filter.where === 'object') {
queryRelated = {
relation: this._scope.collect,
scope: filter,
};
scopeOnRelatedModel = true;
relatedModel = targetModel.relations[queryRelated.relation].modelTo;
IdKey = idName(relatedModel);
keyFrom = targetModel.relations[queryRelated.relation].keyFrom || IdKey;
if (filter.where[IdKey]) {
filter = {
fields: [keyFrom],
where: {
[keyFrom]: filter.where [IdKey],
},
};
} else {
filter = {
fields: [keyFrom],
};
}
}
const scoped = (this._scope && this._scope.where) || {};
filter = mergeQuery({where: scoped}, filter || {});
return targetModel.findOne(filter, options, cb);
if (!scopeOnRelatedModel) {
targetModel.findOne(filter, options, cb);
} else {
targetModel.find(filter, options, function(err, findData) {
const smartMergeSuccessful =
smartMergeRelatedModelQuery(findData, queryRelated.scope, keyFrom, IdKey);
if (!smartMergeSuccessful) {
return cb(null, null);
}
return relatedModel.findOne(queryRelated.scope, options, cb);
});
}
return cb.promise;
}

function count(where, options, cb) {
Expand All @@ -517,12 +620,91 @@ function defineScope(cls, targetClass, name, params, methods, options) {
options = {};
}
options = options || {};

cb = cb || utils.createPromiseCallback();
const targetModel = definition.targetModel(this._receiver);
// If there is a through model
// run another query to apply filter on relatedModel(targetModel)
// see github.com/strongloop/loopback-datasource-juggler/issues/1795
let scopeOnRelatedModel = false;
let queryRelated, keyFrom, relatedModel, IdKey, fieldsRelated;
if (this._scope && this._scope.collect &&
where !== null && typeof where === 'object') {
queryRelated = {
relation: this._scope.collect,
scope: {
where: where,
},
};
scopeOnRelatedModel = true;
relatedModel = targetModel.relations[queryRelated.relation].modelTo;
IdKey = idName(relatedModel);
keyFrom = targetModel.relations[queryRelated.relation].keyFrom || IdKey;
fieldsRelated = [keyFrom];
if (where[IdKey]) {
where = {
[keyFrom]: where[IdKey],
};
} else {
where = {};
}
}
const scoped = (this._scope && this._scope.where) || {};
const filter = mergeQuery({where: scoped}, {where: where || {}});
return targetModel.count(filter.where, options, cb);
const filter = mergeQuery({where: scoped}, {where: where || {}, fields: fieldsRelated});
if (!scopeOnRelatedModel) {
targetModel.count(filter.where, options, cb);
} else {
targetModel.find(filter, options, function(err, findData) {
const smartMergeSuccessful =
smartMergeRelatedModelQuery(findData, queryRelated.scope, keyFrom, IdKey);
if (!smartMergeSuccessful) {
return cb(null, 0);
}
return relatedModel.count(queryRelated.scope.where, options, cb);
});
}
return cb.promise;
}

return definition;
}

function smartMergeRelatedModelQuery(findData, queryRelated, keyFrom, IdKey) {
const smartMerge = function(idCollection, qWhere) {
if (!qWhere[IdKey]) return idCollection;
let merged = {};

const idsA = idCollection.inq;
const idsB = qWhere[IdKey].inq ? qWhere[IdKey].inq : [qWhere[IdKey]];

const intersect = _.intersectionWith(idsA, idsB, _.isEqual);
if (intersect.length === 1) merged = intersect[0];
if (intersect.length > 1) merged = {inq: intersect};

return merged;
};

if (queryRelated.where !== undefined) {
// Merge queryRelated filter and targetId filter
const IdKeyCondition = {};
IdKeyCondition[IdKey] = smartMerge(collectTargetIds(findData, keyFrom),
queryRelated.where);

// if the id in filter doesn't exist after the merge,
// return empty result
if (_.isObject(IdKeyCondition[IdKey]) && _.isEmpty(IdKeyCondition[IdKey])) {
return false;
}

const mergedWhere = {
and: [
IdKeyCondition,
_.omit(queryRelated.where, IdKey),
],
};
queryRelated.where = mergedWhere;
} else {
queryRelated.where = {};
queryRelated.where[IdKey] = collectTargetIds(findData, keyFrom);
}
return true;
}
Loading

0 comments on commit 7c98ecb

Please sign in to comment.