Skip to content

Commit

Permalink
fix(model): apply projection parameter to hydrate()
Browse files Browse the repository at this point in the history
Fix #11375
  • Loading branch information
vkarpov15 committed Feb 13, 2022
1 parent 7b0c2f6 commit 2b197b2
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 34 deletions.
4 changes: 2 additions & 2 deletions lib/cursor/AggregationCursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,10 @@ if (Symbol.asyncIterator != null) {
* @method map
*/

AggregationCursor.prototype.map = function(fn) {
/* AggregationCursor.prototype.map = function(fn) {
this._transforms.push(fn);
return this;
};
}; */

/*!
* Marks this cursor as errored
Expand Down
4 changes: 2 additions & 2 deletions lib/cursor/QueryCursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,10 @@ QueryCursor.prototype._read = function() {
* @method map
*/

QueryCursor.prototype.map = function(fn) {
/* QueryCursor.prototype.map = function(fn) {
this._transforms.push(fn);
return this;
};
}; */

/*!
* Marks this cursor as errored
Expand Down
28 changes: 1 addition & 27 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const StrictModeError = require('./error/strict');
const ValidationError = require('./error/validation');
const ValidatorError = require('./error/validator');
const VirtualType = require('./virtualtype');
const $__hasIncludedChildren = require('./helpers/projection/hasIncludedChildren');
const promiseOrCallback = require('./helpers/promiseOrCallback');
const cleanModifiedSubpaths = require('./helpers/document/cleanModifiedSubpaths');
const compile = require('./helpers/document/compile').compile;
Expand Down Expand Up @@ -417,33 +418,6 @@ Object.defineProperty(Document.prototype, '$op', {
}
});

/*!
* ignore
*/

function $__hasIncludedChildren(fields) {
const hasIncludedChildren = {};
const keys = Object.keys(fields);

for (const key of keys) {
if (key.indexOf('.') === -1) {
hasIncludedChildren[key] = 1;
continue;
}
const parts = key.split('.');
let c = parts[0];

for (let i = 0; i < parts.length; ++i) {
hasIncludedChildren[c] = 1;
if (i + 1 < parts.length) {
c = c + '.' + parts[i + 1];
}
}
}

return hasIncludedChildren;
}

/*!
* ignore
*/
Expand Down
77 changes: 77 additions & 0 deletions lib/helpers/projection/applyProjection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
'use strict';

const hasIncludedChildren = require('./hasIncludedChildren');
const isExclusive = require('./isExclusive');
const isInclusive = require('./isInclusive');
const isPOJO = require('../../utils').isPOJO;

module.exports = function applyProjection(doc, projection, _hasIncludedChildren) {
if (projection == null) {
return doc;
}
if (doc == null) {
return doc;
}

let exclude = null;
if (isInclusive(projection)) {
exclude = false;
} else if (isExclusive(projection)) {
exclude = true;
}

if (exclude == null) {
return doc;
} else if (exclude) {
_hasIncludedChildren = _hasIncludedChildren || hasIncludedChildren(projection);
return applyExclusiveProjection(doc, projection, _hasIncludedChildren);
} else {
_hasIncludedChildren = _hasIncludedChildren || hasIncludedChildren(projection);
return applyInclusiveProjection(doc, projection, _hasIncludedChildren);
}
};

function applyExclusiveProjection(doc, projection, hasIncludedChildren, projectionLimb, prefix) {
if (doc == null || typeof doc !== 'object') {
return doc;
}
const ret = { ...doc };
projectionLimb = prefix ? (projectionLimb || {}) : projection;

for (const key of Object.keys(ret)) {
const fullPath = prefix ? prefix + '.' + key : key;
if (projection.hasOwnProperty(fullPath) || projectionLimb.hasOwnProperty(key)) {
if (isPOJO(projection[fullPath]) || isPOJO(projectionLimb[key])) {
ret[key] = applyExclusiveProjection(ret[key], projection, hasIncludedChildren, projectionLimb[key], fullPath);
} else {
delete ret[key];
}
} else if (hasIncludedChildren[fullPath]) {
ret[key] = applyExclusiveProjection(ret[key], projection, hasIncludedChildren, projectionLimb[key], fullPath);
}
}
return ret;
}

function applyInclusiveProjection(doc, projection, hasIncludedChildren, projectionLimb, prefix) {
if (doc == null || typeof doc !== 'object') {
return doc;
}
const ret = { ...doc };
projectionLimb = prefix ? (projectionLimb || {}) : projection;

for (const key of Object.keys(ret)) {
const fullPath = prefix ? prefix + '.' + key : key;
if (projection.hasOwnProperty(fullPath) || projectionLimb.hasOwnProperty(key)) {
if (isPOJO(projection[fullPath]) || isPOJO(projectionLimb[key])) {
ret[key] = applyInclusiveProjection(ret[key], projection, hasIncludedChildren, projectionLimb[key], fullPath);
}
continue;
} else if (hasIncludedChildren[fullPath]) {
ret[key] = applyInclusiveProjection(ret[key], projection, hasIncludedChildren, projectionLimb[key], fullPath);
} else {
delete ret[key];
}
}
return ret;
}
36 changes: 36 additions & 0 deletions lib/helpers/projection/hasIncludedChildren.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';

/*!
* Creates an object that precomputes whether a given path has child fields in
* the projection.
*
* ####Example:
* const res = hasIncludedChildren({ 'a.b.c': 0 });
* res.a; // 1
* res['a.b']; // 1
* res['a.b.c']; // 1
* res['a.c']; // undefined
*/

module.exports = function hasIncludedChildren(fields) {
const hasIncludedChildren = {};
const keys = Object.keys(fields);

for (const key of keys) {
if (key.indexOf('.') === -1) {
hasIncludedChildren[key] = 1;
continue;
}
const parts = key.split('.');
let c = parts[0];

for (let i = 0; i < parts.length; ++i) {
hasIncludedChildren[c] = 1;
if (i + 1 < parts.length) {
c = c + '.' + parts[i + 1];
}
}
}

return hasIncludedChildren;
};
7 changes: 5 additions & 2 deletions lib/helpers/projection/isExclusive.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ module.exports = function isExclusive(projection) {
while (ki--) {
// Does this projection explicitly define inclusion/exclusion?
// Explicitly avoid `$meta` and `$slice`
if (keys[ki] !== '_id' && isDefiningProjection(projection[keys[ki]])) {
exclude = !projection[keys[ki]];
const key = keys[ki];
if (key !== '_id' && isDefiningProjection(projection[key])) {
exclude = (projection[key] != null && typeof projection[key] === 'object') ?
isExclusive(projection[key]) :
!projection[key];
break;
}
}
Expand Down
6 changes: 5 additions & 1 deletion lib/helpers/projection/isInclusive.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ module.exports = function isInclusive(projection) {
// If field is truthy (1, true, etc.) and not an object, then this
// projection must be inclusive. If object, assume its $meta, $slice, etc.
if (isDefiningProjection(projection[prop]) && !!projection[prop]) {
return true;
if (projection[prop] != null && typeof projection[prop] === 'object') {
return isInclusive(projection[prop]);
} else {
return !!projection[prop];
}
}
}

Expand Down
8 changes: 8 additions & 0 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const ParallelSaveError = require('./error/parallelSave');
const applyQueryMiddleware = require('./helpers/query/applyQueryMiddleware');
const applyHooks = require('./helpers/model/applyHooks');
const applyMethods = require('./helpers/model/applyMethods');
const applyProjection = require('./helpers/projection/applyProjection');
const applyStaticHooks = require('./helpers/model/applyStaticHooks');
const applyStatics = require('./helpers/model/applyStatics');
const applyWriteConcern = require('./helpers/schema/applyWriteConcern');
Expand Down Expand Up @@ -3774,6 +3775,13 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
Model.hydrate = function(obj, projection) {
_checkContext(this, 'hydrate');

if (projection != null) {
if (obj != null && obj.$__ != null) {
obj = obj.toObject(internalToObjectOptions);
}
obj = applyProjection(obj, projection);
}

const document = require('./queryhelpers').createModel(this, obj, projection);
document.$init(obj);
return document;
Expand Down
22 changes: 22 additions & 0 deletions test/helpers/projection.applyProjection.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

const applyProjection = require('../../lib/helpers/projection/applyProjection');
const assert = require('assert');

describe('applyProjection', function() {
it('handles deep inclusive projections', function() {
const obj = { str: 'test', nested: { str2: 'test2', num3: 42 } };

assert.deepEqual(applyProjection(obj, { str: 1 }), { str: 'test' });
assert.deepEqual(applyProjection(obj, { 'nested.str2': 1 }), { nested: { str2: 'test2' } });
assert.deepEqual(applyProjection(obj, { str: 1, nested: { num3: 1 } }), { str: 'test', nested: { num3: 42 } });
});

it('handles deep exclusive projections', function() {
const obj = { str: 'test', nested: { str2: 'test2', num3: 42 } };

assert.deepEqual(applyProjection(obj, { nested: 0 }), { str: 'test' });
assert.deepEqual(applyProjection(obj, { 'nested.str2': 0 }), { str: 'test', nested: { num3: 42 } });
assert.deepEqual(applyProjection(obj, { nested: { num3: 0 } }), { str: 'test', nested: { str2: 'test2' } });
});
});
9 changes: 9 additions & 0 deletions test/model.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7684,6 +7684,15 @@ describe('Model', function() {

assert.ok(book.author instanceof mongoose.Types.ObjectId);
});

it('respects `hydrate()` projection (gh-11375)', function() {
const PieSchema = Schema({ filling: String, hoursToMake: Number, tasteRating: Number });
const Test = db.model('Test', PieSchema);
const doc = Test.hydrate({ filling: 'cherry', hoursToMake: 2 }, { filling: 1 });

assert.equal(doc.filling, 'cherry');
assert.equal(doc.hoursToMake, null);
});
});


Expand Down

0 comments on commit 2b197b2

Please sign in to comment.