Skip to content

Commit

Permalink
feat(schema): add retainKeyOrder prop
Browse files Browse the repository at this point in the history
Fix #4542
  • Loading branch information
vkarpov15 committed Oct 16, 2016
1 parent 908ed3c commit 72c8a5a
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 70 deletions.
118 changes: 71 additions & 47 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,18 @@ function init(self, obj, doc, prefix) {
var i;
var index = 0;

while (index < len) {
i = keys[index++];
if (self.schema.options.retainKeyOrder) {
while (index < len) {
_init(index++);
}
} else {
while (len--) {
_init(len);
}
}

function _init(index) {
i = keys[index];
path = prefix + i;
schema = self.schema.path(path);

Expand Down Expand Up @@ -516,57 +526,67 @@ Document.prototype.set = function(path, val, type, options) {
return this;
}

while (i < len) {
key = keys[i++];
var pathName = prefix + key;
pathtype = this.schema.pathType(pathName);

if (path[key] !== null
&& path[key] !== void 0
// need to know if plain object - no Buffer, ObjectId, ref, etc
&& utils.isObject(path[key])
&& (!path[key].constructor || utils.getFunctionName(path[key].constructor) === 'Object')
&& pathtype !== 'virtual'
&& pathtype !== 'real'
&& !(this.$__path(pathName) instanceof MixedSchema)
&& !(this.schema.paths[pathName] &&
this.schema.paths[pathName].options &&
this.schema.paths[pathName].options.ref)) {
this.set(path[key], prefix + key, constructing);
} else if (strict) {
// Don't overwrite defaults with undefined keys (gh-3981)
if (constructing && path[key] === void 0 &&
this.get(key) !== void 0) {
continue;
}

if (pathtype === 'real' || pathtype === 'virtual') {
// Check for setting single embedded schema to document (gh-3535)
if (this.schema.paths[pathName] &&
this.schema.paths[pathName].$isSingleNested &&
path[key] instanceof Document) {
path[key] = path[key].toObject({virtuals: false});
}
this.set(prefix + key, path[key], constructing);
} else if (pathtype === 'nested' && path[key] instanceof Document) {
this.set(prefix + key,
path[key].toObject({transform: false}), constructing);
} else if (strict === 'throw') {
if (pathtype === 'nested') {
throw new ObjectExpectedError(key, path[key]);
} else {
throw new StrictModeError(key);
}
}
} else if (path[key] !== void 0) {
this.set(prefix + key, path[key], constructing);
if (this.schema.options.retainKeyOrder) {
while (i < len) {
_handleIndex.call(this, i++);
}
} else {
while (len--) {
_handleIndex.call(this, len);
}
}

return this;
}
}

function _handleIndex(i) {
key = keys[i];
var pathName = prefix + key;
pathtype = this.schema.pathType(pathName);

if (path[key] !== null
&& path[key] !== void 0
// need to know if plain object - no Buffer, ObjectId, ref, etc
&& utils.isObject(path[key])
&& (!path[key].constructor || utils.getFunctionName(path[key].constructor) === 'Object')
&& pathtype !== 'virtual'
&& pathtype !== 'real'
&& !(this.$__path(pathName) instanceof MixedSchema)
&& !(this.schema.paths[pathName] &&
this.schema.paths[pathName].options &&
this.schema.paths[pathName].options.ref)) {
this.set(path[key], prefix + key, constructing);
} else if (strict) {
// Don't overwrite defaults with undefined keys (gh-3981)
if (constructing && path[key] === void 0 &&
this.get(key) !== void 0) {
return;
}

if (pathtype === 'real' || pathtype === 'virtual') {
// Check for setting single embedded schema to document (gh-3535)
if (this.schema.paths[pathName] &&
this.schema.paths[pathName].$isSingleNested &&
path[key] instanceof Document) {
path[key] = path[key].toObject({virtuals: false});
}
this.set(prefix + key, path[key], constructing);
} else if (pathtype === 'nested' && path[key] instanceof Document) {
this.set(prefix + key,
path[key].toObject({transform: false}), constructing);
} else if (strict === 'throw') {
if (pathtype === 'nested') {
throw new ObjectExpectedError(key, path[key]);
} else {
throw new StrictModeError(key);
}
}
} else if (path[key] !== void 0) {
this.set(prefix + key, path[key], constructing);
}
}

// ensure _strict is honored for obj props
// docschema = new Schema({ path: { nest: 'string' }})
// doc.set('path', obj);
Expand Down Expand Up @@ -1996,7 +2016,11 @@ Document.prototype.$__handleReject = function handleReject(err) {
*/

Document.prototype.$toObject = function(options, json) {
var defaultOptions = {transform: true, json: json};
var defaultOptions = {
transform: true,
json: json,
retainKeyOrder: this.schema.options.retainKeyOrder
};

if (options && options.depopulate && !options._skipDepopulateTopLevel && this.$__.wasPopulated) {
// populated paths that we set to a document
Expand Down
4 changes: 2 additions & 2 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Model.prototype.$__handleSave = function(options, callback) {
// send entire doc
var toObjectOptions = {};

toObjectOptions.retainKeyOrder = true;
toObjectOptions.retainKeyOrder = this.schema.options.retainKeyOrder;
toObjectOptions.depopulate = 1;
toObjectOptions._skipDepopulateTopLevel = true;
toObjectOptions.transform = false;
Expand Down Expand Up @@ -3222,7 +3222,7 @@ Model.compile = function compile(name, schema, collectionName, connection, base)
// Create custom query constructor
model.Query = function() {
Query.apply(this, arguments);
this.options.retainKeyOrder = true;
this.options.retainKeyOrder = model.schema.options.retainKeyOrder;
};
model.Query.prototype = Object.create(Query.prototype);
model.Query.base = Query.base;
Expand Down
12 changes: 7 additions & 5 deletions lib/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -1162,26 +1162,28 @@ Query.prototype.find = function(conditions, callback) {
* @return {Query} this
*/

Query.prototype.merge = function (source) {
Query.prototype.merge = function(source) {
if (!source) {
return this;
}

var opts = { retainKeyOrder: this.options.retainKeyOrder, overwrite: true };

if (source instanceof Query) {
// if source has a feature, apply it to ourselves

if (source._conditions) {
utils.merge(this._conditions, source._conditions);
utils.merge(this._conditions, source._conditions, opts);
}

if (source._fields) {
this._fields || (this._fields = {});
utils.merge(this._fields, source._fields);
utils.merge(this._fields, source._fields, opts);
}

if (source.options) {
this.options || (this.options = {});
utils.merge(this.options, source.options);
utils.merge(this.options, source.options, opts);
}

if (source._update) {
Expand All @@ -1197,7 +1199,7 @@ Query.prototype.merge = function (source) {
}

// plain object
utils.merge(this._conditions, source);
utils.merge(this._conditions, source, opts);

return this;
};
Expand Down
3 changes: 2 additions & 1 deletion lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,8 @@ Schema.prototype.defaultOptions = function(options) {
_id: true,
noVirtualId: false, // deprecated, use { id: false }
id: true,
typeKey: 'type'
typeKey: 'type',
retainKeyOrder: false
}, options);

if (options.read) {
Expand Down
5 changes: 4 additions & 1 deletion lib/schema/embedded.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ function Embedded(schema, path, options) {
_embedded.$isSingleNested = true;
_embedded.prototype.$basePath = path;
_embedded.prototype.toBSON = function() {
return this.toObject({ transform: false, retainKeyOrder: true });
return this.toObject({
transform: false,
retainKeyOrder: schema.options.retainKeyOrder
});
};

// apply methods
Expand Down
30 changes: 23 additions & 7 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,18 +380,34 @@ exports.random = function() {
* @api private
*/

exports.merge = function merge(to, from) {
exports.merge = function merge(to, from, options) {
options = options || {};
var keys = Object.keys(from);
var i = 0;
var len = keys.length;
var key;

while (i < len) {
key = keys[i++];
if (typeof to[key] === 'undefined') {
to[key] = from[key];
} else if (exports.isObject(from[key])) {
merge(to[key], from[key]);
if (options.retainKeyOrder) {
while (i < len) {
key = keys[i++];
if (typeof to[key] === 'undefined') {
to[key] = from[key];
} else if (exports.isObject(from[key])) {
merge(to[key], from[key]);
} else if (options.overwrite) {
to[key] = from[key];
}
}
} else {
while (len--) {
key = keys[len];
if (typeof to[key] === 'undefined') {
to[key] = from[key];
} else if (exports.isObject(from[key])) {
merge(to[key], from[key]);
} else if (options.overwrite) {
to[key] = from[key];
}
}
}
};
Expand Down
2 changes: 1 addition & 1 deletion test/model.query.casting.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ describe('model query casting', function() {
}
});

it('with objects (lv426)', function(done) {
it('with objects', function(done) {
var db = start(),
Test = db.model('Geo5', geoSchemaObject, 'y' + random());

Expand Down
4 changes: 2 additions & 2 deletions test/query.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1153,12 +1153,12 @@ describe('Query', function() {
prod.save(function(err) {
assert.ifError(err);

Product.findOne(prod, function(err, product) {
Product.findOne({ _id: prod._id }, function(err, product) {
assert.ifError(err);
assert.equal(product.comments.length, 1);
assert.equal(product.comments[0].text, 'hello');

Product.update(product, prod2doc, function(err) {
Product.update({ _id: prod._id }, prod2doc, function(err) {
assert.ifError(err);

Product.collection.findOne({_id: product._id}, function(err, doc) {
Expand Down
6 changes: 3 additions & 3 deletions test/query.toconstructor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ describe('Query:', function() {

var nq = prodC(null, {limit: 3});
assert.deepEqual(nq._mongooseOptions, {lean: true, limit: 3});
assert.deepEqual(nq.options, {sort: {title: 1}, limit: 3});
assert.deepEqual(nq.options, {sort: {title: 1}, limit: 3, retainKeyOrder: false});
done();
});

Expand All @@ -125,7 +125,7 @@ describe('Query:', function() {

var nq = prodC(null, {limit: 3});
assert.deepEqual(nq._mongooseOptions, {lean: true, limit: 3});
assert.deepEqual(nq.options, {sort: {title: 1}, limit: 3});
assert.deepEqual(nq.options, {sort: {title: 1}, limit: 3, retainKeyOrder: false});
var nq2 = prodC(null, {limit: 5});
assert.deepEqual(nq._mongooseOptions, {lean: true, limit: 3});
assert.deepEqual(nq2._mongooseOptions, {lean: true, limit: 5});
Expand All @@ -136,7 +136,7 @@ describe('Query:', function() {
it('creates subclasses of mquery', function(done) {
var Product = db.model(prodName);

var opts = {safe: {w: 'majority'}, readPreference: 'p'};
var opts = {safe: {w: 'majority'}, readPreference: 'p', retainKeyOrder: true};
var match = {title: 'test', count: {$gt: 101}};
var select = {name: 1, count: 0};
var update = {$set: {title: 'thing'}};
Expand Down
2 changes: 1 addition & 1 deletion test/schema.validation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -959,7 +959,7 @@ describe('schema', function() {
// Remove the cast error using array.set()
bad.foods.set(1, {eggs: 1});
bad.validate(function(error) {
assert.deepEqual(['foods.0.eggs','id'], Object.keys(error.errors).sort());
assert.deepEqual(['foods.0.eggs', 'id'], Object.keys(error.errors).sort());
assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError);

done();
Expand Down

0 comments on commit 72c8a5a

Please sign in to comment.