Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More toObject() perf improvements #14623

Merged
merged 10 commits into from
Jun 2, 2024
100 changes: 36 additions & 64 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -3798,11 +3798,6 @@ Document.prototype.$__handleReject = function handleReject(err) {
*/

Document.prototype.$toObject = function(options, json) {
const defaultOptions = {
transform: true,
flattenDecimals: true
};

const path = json ? 'toJSON' : 'toObject';
const baseOptions = this.constructor &&
this.constructor.base &&
Expand All @@ -3811,7 +3806,7 @@ Document.prototype.$toObject = function(options, json) {
const schemaOptions = this.$__schema && this.$__schema.options || {};
// merge base default options with Schema's set default options if available.
// `clone` is necessary here because `utils.options` directly modifies the second input.
Object.assign(defaultOptions, baseOptions, schemaOptions[path]);
const defaultOptions = Object.assign({}, baseOptions, schemaOptions[path]);

// If options do not exist or is not an object, set it to empty object
options = utils.isPOJO(options) ? { ...options } : {};
Expand All @@ -3826,45 +3821,17 @@ Document.prototype.$toObject = function(options, json) {
_minimize = schemaOptions.minimize;
}

let flattenMaps;
if (options._calledWithOptions.flattenMaps != null) {
flattenMaps = options.flattenMaps;
} else if (defaultOptions.flattenMaps != null) {
flattenMaps = defaultOptions.flattenMaps;
} else {
flattenMaps = schemaOptions.flattenMaps;
}

let flattenObjectIds;
if (options._calledWithOptions.flattenObjectIds != null) {
flattenObjectIds = options.flattenObjectIds;
} else if (defaultOptions.flattenObjectIds != null) {
flattenObjectIds = defaultOptions.flattenObjectIds;
} else {
flattenObjectIds = schemaOptions.flattenObjectIds;
}

// The original options that will be passed to `clone()`. Important because
// `clone()` will recursively call `$toObject()` on embedded docs, so we
// need the original options the user passed in, plus `_isNested` and
// `_parentOptions` for checking whether we need to depopulate.
const cloneOptions = {
...options,
_isNested: true,
json: json,
minimize: _minimize,
flattenMaps: flattenMaps,
flattenObjectIds: flattenObjectIds,
_seen: (options && options._seen) || new Map(),
_calledWithOptions: options._calledWithOptions
};
options.minimize = _minimize;
options._seen = options._seen || new Map();

const depopulate = options.depopulate ||
(options._parentOptions && options._parentOptions.depopulate || false);
const depopulate = options._calledWithOptions.depopulate
?? options._parentOptions?.depopulate
?? defaultOptions.depopulate
?? false;
// _isNested will only be true if this is not the top level document, we
// should never depopulate the top-level document
if (depopulate && options._isNested && this.$__.wasPopulated) {
return clone(this.$__.wasPopulated.value || this._id, cloneOptions);
return clone(this.$__.wasPopulated.value || this._id, options);
}

// merge default options with input options.
Expand All @@ -3877,33 +3844,47 @@ Document.prototype.$toObject = function(options, json) {
options.json = json;
options.minimize = _minimize;

cloneOptions._parentOptions = options;
options._parentOptions = options;

cloneOptions._skipSingleNestedGetters = false;
options._skipSingleNestedGetters = false;
// remember the root transform function
// to save it from being overwritten by sub-transform functions
const originalTransform = options.transform;
// const originalTransform = options.transform;

let ret = clone(this._doc, cloneOptions) || {};
let ret = clone(this._doc, options) || {};

cloneOptions._skipSingleNestedGetters = true;
if (options.getters) {
applyGetters(this, ret, cloneOptions);
options._skipSingleNestedGetters = true;
const getters = options._calledWithOptions.getters
?? options.getters
?? defaultOptions.getters
?? false;
if (getters) {
applyGetters(this, ret, options);

if (options.minimize) {
ret = minimize(ret) || {};
}
}

if (options.virtuals || (options.getters && options.virtuals !== false)) {
applyVirtuals(this, ret, cloneOptions, options);
const virtuals = options._calledWithOptions.virtuals
?? defaultOptions.virtuals
?? undefined;
hasezoey marked this conversation as resolved.
Show resolved Hide resolved

if (virtuals || (getters && virtuals !== false)) {
applyVirtuals(this, ret, options, options);
}

if (options.versionKey === false && this.$__schema.options.versionKey) {
delete ret[this.$__schema.options.versionKey];
}

let transform = options.transform;
const transform = options._calledWithOptions.transform ?? true;
let transformFunction = undefined;
if (transform === true) {
transformFunction = defaultOptions.transform;
} else if (typeof transform === 'function') {
transformFunction = transform;
}

// In the case where a subdocument has its own transform function, we need to
// check and see if the parent has a transform (options.transform) and if the
Expand All @@ -3918,18 +3899,8 @@ Document.prototype.$toObject = function(options, json) {
omitDeselectedFields(this, ret);
}

if (transform === true || (schemaOptions.toObject && transform)) {
const opts = options.json ? schemaOptions.toJSON : schemaOptions.toObject;

if (opts) {
transform = (typeof options.transform === 'function' ? options.transform : opts.transform);
}
} else {
options.transform = originalTransform;
}

if (typeof transform === 'function') {
const xformed = transform(this, ret, options);
if (typeof transformFunction === 'function') {
const xformed = transformFunction(this, ret, options);
if (typeof xformed !== 'undefined') {
ret = xformed;
}
Expand Down Expand Up @@ -4146,7 +4117,8 @@ function applyVirtuals(self, json, options, toObjectOptions) {
assignPath = path.substring(options.path.length + 1);
}
if (assignPath.indexOf('.') === -1 && assignPath === path) {
v = clone(self.get(path, { noDottedPath: true }), options);
v = self.get(path, null, { noDottedPath: true });
v = clone(v, options);
if (v === void 0) {
continue;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/helpers/clone.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function clone(obj, options, isArrayChild) {
// Single nested subdocs should apply getters later in `applyGetters()`
// when calling `toObject()`. See gh-7442, gh-8295
if (options._skipSingleNestedGetters && obj.$isSingleNested) {
options = Object.assign({}, options, { getters: false });
options._calledWithOptions = Object.assign({}, options._calledWithOptions || {}, { getters: false });
}
if (options.retainDocuments && obj.$__ != null) {
const clonedDoc = obj.$clone();
Expand Down
22 changes: 15 additions & 7 deletions test/document.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -711,18 +711,19 @@ describe('document', function() {
name: String,
email: String
});
const topicSchema = new Schema({
title: String,
email: String,
followers: [userSchema]
});

userSchema.options.toObject = {
transform: function(doc, ret) {
delete ret.email;
}
};

const topicSchema = new Schema({
title: String,
email: String,
followers: [userSchema]
});

topicSchema.options.toObject = {
transform: function(doc, ret) {
ret.title = ret.title.toLowerCase();
Expand Down Expand Up @@ -833,7 +834,10 @@ describe('document', function() {
const userSchema = Schema({
firstName: String,
company: {
type: { companyId: { type: Schema.Types.ObjectId }, companyName: String }
type: {
companyId: { type: Schema.Types.ObjectId },
companyName: String
}
}
}, {
toObject: {
Expand All @@ -849,6 +853,7 @@ describe('document', function() {

const User = db.model('User', userSchema);
const user = new User({ firstName: 'test', company: { companyName: 'foo' } });
assert.equal(transformCalls.length, 0);
const obj = user.toObject();
assert.strictEqual(obj.company.details, 42);
assert.equal(transformCalls.length, 1);
Expand Down Expand Up @@ -1045,7 +1050,10 @@ describe('document', function() {
const userSchema = Schema({
firstName: String,
company: {
type: { companyId: { type: Schema.Types.ObjectId }, companyName: String }
type: {
companyId: { type: Schema.Types.ObjectId },
companyName: String
}
}
}, {
id: false,
Expand Down
2 changes: 1 addition & 1 deletion test/helpers/clone.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ describe('clone', () => {
$isSingleNested: true,
toObject(cloneOpts) {
assert.deepStrictEqual(
Object.assign({}, baseOpts, { getters: false }),
Object.assign({}, baseOpts),
cloneOpts
);
const obj = JSON.parse(JSON.stringify(base));
Expand Down