node_modules updated, buefy add to lib folder

This commit is contained in:
2019-06-04 14:29:48 +02:00
parent 0a3f6ff240
commit 5d28bc36e1
3957 changed files with 306575 additions and 21738 deletions
+89
View File
@@ -0,0 +1,89 @@
'use strict';
/*!
* Module dependencies.
*/
const Decimal128 = require('../types/decimal128');
const ObjectId = require('../types/objectid');
const utils = require('../utils');
exports.flatten = flatten;
exports.modifiedPaths = modifiedPaths;
/*!
* ignore
*/
function flatten(update, path, options) {
let keys;
if (update && utils.isMongooseObject(update) && !Buffer.isBuffer(update)) {
keys = Object.keys(update.toObject({ transform: false, virtuals: false }));
} else {
keys = Object.keys(update || {});
}
const numKeys = keys.length;
const result = {};
path = path ? path + '.' : '';
for (let i = 0; i < numKeys; ++i) {
const key = keys[i];
const val = update[key];
result[path + key] = val;
if (shouldFlatten(val)) {
if (options && options.skipArrays && Array.isArray(val)) {
continue;
}
const flat = flatten(val, path + key, options);
for (const k in flat) {
result[k] = flat[k];
}
if (Array.isArray(val)) {
result[path + key] = val;
}
}
}
return result;
}
/*!
* ignore
*/
function modifiedPaths(update, path, result) {
const keys = Object.keys(update || {});
const numKeys = keys.length;
result = result || {};
path = path ? path + '.' : '';
for (let i = 0; i < numKeys; ++i) {
const key = keys[i];
let val = update[key];
result[path + key] = true;
if (utils.isMongooseObject(val) && !Buffer.isBuffer(val)) {
val = val.toObject({ transform: false, virtuals: false });
}
if (shouldFlatten(val)) {
modifiedPaths(val, path + key, result);
}
}
return result;
}
/*!
* ignore
*/
function shouldFlatten(val) {
return val &&
typeof val === 'object' &&
!(val instanceof Date) &&
!(val instanceof ObjectId) &&
(!Array.isArray(val) || val.length > 0) &&
!(val instanceof Buffer) &&
!(val instanceof Decimal128);
}
+71
View File
@@ -0,0 +1,71 @@
'use strict';
/*!
* Module dependencies.
*/
const async = require('async');
const utils = require('../../utils');
/**
* Execute `fn` for every document in the cursor. If `fn` returns a promise,
* will wait for the promise to resolve before iterating on to the next one.
* Returns a promise that resolves when done.
*
* @param {Function} next the thunk to call to get the next document
* @param {Function} fn
* @param {Object} options
* @param {Function} [callback] executed when all docs have been processed
* @return {Promise}
* @api public
* @method eachAsync
*/
module.exports = function eachAsync(next, fn, options, callback) {
const parallel = options.parallel || 1;
const handleNextResult = function(doc, callback) {
const promise = fn(doc);
if (promise && typeof promise.then === 'function') {
promise.then(
function() { callback(null); },
function(error) { callback(error || new Error('`eachAsync()` promise rejected without error')); });
} else {
callback(null);
}
};
const iterate = function(callback) {
let drained = false;
const nextQueue = async.queue(function(task, cb) {
if (drained) return cb();
next(function(err, doc) {
if (err) return cb(err);
if (!doc) drained = true;
cb(null, doc);
});
}, 1);
const getAndRun = function(cb) {
nextQueue.push({}, function(err, doc) {
if (err) return cb(err);
if (!doc) return cb();
handleNextResult(doc, function(err) {
if (err) return cb(err);
// Make sure to clear the stack re: gh-4697
setTimeout(function() {
getAndRun(cb);
}, 0);
});
});
};
async.times(parallel, function(n, cb) {
getAndRun(cb);
}, callback);
};
return utils.promiseOrCallback(callback, cb => {
iterate(cb);
});
};
@@ -0,0 +1,12 @@
'use strict';
module.exports = function checkEmbeddedDiscriminatorKeyProjection(userProjection, path, schema, selected, addedPaths) {
const userProjectedInPath = Object.keys(userProjection).
reduce((cur, key) => cur || key.startsWith(path + '.'), false);
const _discriminatorKey = path + '.' + schema.options.discriminatorKey;
if (!userProjectedInPath &&
addedPaths.length === 1 &&
addedPaths[0] === _discriminatorKey) {
selected.splice(selected.indexOf(_discriminatorKey), 1);
}
};
+28
View File
@@ -0,0 +1,28 @@
'use strict';
/*!
* ignore
*/
module.exports = function cleanModifiedSubpaths(doc, path, options) {
options = options || {};
const skipDocArrays = options.skipDocArrays;
let deleted = 0;
if (!doc) {
return deleted;
}
for (const modifiedPath of Object.keys(doc.$__.activePaths.states.modify)) {
if (skipDocArrays) {
const schemaType = doc.schema.path(modifiedPath);
if (schemaType && schemaType.$isMongooseDocumentArray) {
continue;
}
}
if (modifiedPath.indexOf(path + '.') === 0) {
delete doc.$__.activePaths.states.modify[modifiedPath];
++deleted;
}
}
return deleted;
};
+162
View File
@@ -0,0 +1,162 @@
'use strict';
const get = require('../../helpers/get');
const getSymbol = require('../../helpers/symbols').getSymbol;
const utils = require('../../utils');
let Document;
/*!
* exports
*/
exports.compile = compile;
exports.defineKey = defineKey;
/*!
* Compiles schemas.
*/
function compile(tree, proto, prefix, options) {
Document = Document || require('../../document');
const keys = Object.keys(tree);
const len = keys.length;
let limb;
let key;
for (let i = 0; i < len; ++i) {
key = keys[i];
limb = tree[key];
const hasSubprops = utils.isPOJO(limb) && Object.keys(limb).length &&
(!limb[options.typeKey] || (options.typeKey === 'type' && limb.type.type));
const subprops = hasSubprops ? limb : null;
defineKey(key, subprops, proto, prefix, keys, options);
}
}
/*!
* Defines the accessor named prop on the incoming prototype.
*/
function defineKey(prop, subprops, prototype, prefix, keys, options) {
Document = Document || require('../../document');
const path = (prefix ? prefix + '.' : '') + prop;
prefix = prefix || '';
if (subprops) {
Object.defineProperty(prototype, prop, {
enumerable: true,
configurable: true,
get: function() {
const _this = this;
if (!this.$__.getters) {
this.$__.getters = {};
}
if (!this.$__.getters[path]) {
const nested = Object.create(Document.prototype, getOwnPropertyDescriptors(this));
// save scope for nested getters/setters
if (!prefix) {
nested.$__.scope = this;
}
nested.$__.nestedPath = path;
Object.defineProperty(nested, 'schema', {
enumerable: false,
configurable: true,
writable: false,
value: prototype.schema
});
Object.defineProperty(nested, 'toObject', {
enumerable: false,
configurable: true,
writable: false,
value: function() {
return utils.clone(_this.get(path, null, {
virtuals: get(this, 'schema.options.toObject.virtuals', null)
}));
}
});
Object.defineProperty(nested, 'toJSON', {
enumerable: false,
configurable: true,
writable: false,
value: function() {
return _this.get(path, null, {
virtuals: get(_this, 'schema.options.toJSON.virtuals', null)
});
}
});
Object.defineProperty(nested, '$__isNested', {
enumerable: false,
configurable: true,
writable: false,
value: true
});
const _isEmptyOptions = Object.freeze({
minimize: true,
virtuals: false,
getters: false,
transform: false
});
Object.defineProperty(nested, '$isEmpty', {
enumerable: false,
configurable: true,
writable: false,
value: function() {
return Object.keys(this.get(path, null, _isEmptyOptions) || {}).length === 0;
}
});
compile(subprops, nested, path, options);
this.$__.getters[path] = nested;
}
return this.$__.getters[path];
},
set: function(v) {
if (v instanceof Document) {
v = v.toObject({ transform: false });
}
const doc = this.$__.scope || this;
return doc.$set(path, v);
}
});
} else {
Object.defineProperty(prototype, prop, {
enumerable: true,
configurable: true,
get: function() {
return this[getSymbol].call(this.$__.scope || this, path);
},
set: function(v) {
return this.$set.call(this.$__.scope || this, path, v);
}
});
}
}
// gets descriptors for all properties of `object`
// makes all properties non-enumerable to match previous behavior to #2211
function getOwnPropertyDescriptors(object) {
const result = {};
Object.getOwnPropertyNames(object).forEach(function(key) {
result[key] = Object.getOwnPropertyDescriptor(object, key);
// Assume these are schema paths, ignore them re: #5470
if (result[key].get) {
delete result[key];
return;
}
result[key].enumerable = ['isNew', '$__', 'errors', '_doc', '$locals'].indexOf(key) === -1;
});
return result;
}
@@ -0,0 +1,43 @@
'use strict';
const get = require('../get');
/*!
* Like `schema.path()`, except with a document, because impossible to
* determine path type without knowing the embedded discriminator key.
*/
module.exports = function getEmbeddedDiscriminatorPath(doc, path, options) {
options = options || {};
const typeOnly = options.typeOnly;
const parts = path.split('.');
let schema = null;
let type = 'adhocOrUndefined';
for (let i = 0; i < parts.length; ++i) {
const subpath = parts.slice(0, i + 1).join('.');
schema = doc.schema.path(subpath);
if (schema == null) {
type = 'adhocOrUndefined';
continue;
}
if (schema.instance === 'Mixed') {
return typeOnly ? 'real' : schema;
}
type = doc.schema.pathType(subpath);
if ((schema.$isSingleNested || schema.$isMongooseDocumentArrayElement) &&
schema.schema.discriminators != null) {
const discriminators = schema.schema.discriminators;
const discriminatorKey = doc.get(subpath + '.' +
get(schema, 'schema.options.discriminatorKey'));
if (discriminatorKey == null || discriminators[discriminatorKey] == null) {
continue;
}
const rest = parts.slice(i + 1).join('.');
return getEmbeddedDiscriminatorPath(doc.get(subpath), rest, options);
}
}
// Are we getting the whole schema or just the type, 'real', 'nested', etc.
return typeOnly ? type : schema;
};
+39
View File
@@ -0,0 +1,39 @@
'use strict';
/*!
* Simplified lodash.get to work around the annoying null quirk. See:
* https://github.com/lodash/lodash/issues/3659
*/
module.exports = function get(obj, path, def) {
const parts = path.split('.');
let rest = path;
let cur = obj;
for (const part of parts) {
if (cur == null) {
return def;
}
// `lib/cast.js` depends on being able to get dotted paths in updates,
// like `{ $set: { 'a.b': 42 } }`
if (cur[rest] != null) {
return cur[rest];
}
cur = getProperty(cur, part);
rest = rest.substr(part.length + 1);
}
return cur == null ? def : cur;
};
function getProperty(obj, prop) {
if (obj == null) {
return obj;
}
if (obj instanceof Map) {
return obj.get(prop);
}
return obj[prop];
}
+12
View File
@@ -0,0 +1,12 @@
/*!
* Centralize this so we can more easily work around issues with people
* stubbing out `process.nextTick()` in tests using sinon:
* https://github.com/sinonjs/lolex#automatically-incrementing-mocked-time
* See gh-6074
*/
'use strict';
module.exports = function immediate(cb) {
return process.nextTick(cb);
};
+17 -4
View File
@@ -14,6 +14,7 @@ module.exports = applyHooks;
*/
applyHooks.middlewareFunctions = [
'deleteOne',
'save',
'validate',
'remove',
@@ -40,12 +41,22 @@ function applyHooks(model, schema, options) {
const objToDecorate = options.decorateDoc ? model : model.prototype;
model.$appliedHooks = true;
for (let i = 0; i < schema.childSchemas.length; ++i) {
const childModel = schema.childSchemas[i].model;
for (const key of Object.keys(schema.paths)) {
const type = schema.paths[key];
let childModel = null;
if (type.$isSingleNested) {
childModel = type.caster;
} else if (type.$isMongooseDocumentArray) {
childModel = type.Constructor;
} else {
continue;
}
if (childModel.$appliedHooks) {
continue;
}
applyHooks(childModel, schema.childSchemas[i].schema, options);
applyHooks(childModel, type.schema, options);
if (childModel.discriminators != null) {
const keys = Object.keys(childModel.discriminators);
for (let j = 0; j < keys.length; ++j) {
@@ -61,7 +72,7 @@ function applyHooks(model, schema, options) {
const middleware = schema.s.hooks.
filter(hook => {
if (hook.name === 'updateOne') {
if (hook.name === 'updateOne' || hook.name === 'deleteOne') {
return !!hook['document'];
}
if (hook.name === 'remove') {
@@ -86,6 +97,8 @@ function applyHooks(model, schema, options) {
createWrapper('validate', objToDecorate.$__validate, null, kareemOptions);
objToDecorate.$__remove = middleware.
createWrapper('remove', objToDecorate.$__remove, null, kareemOptions);
objToDecorate.$__deleteOne = middleware.
createWrapper('deleteOne', objToDecorate.$__deleteOne, null, kareemOptions);
objToDecorate.$__init = middleware.
createWrapperSync('init', objToDecorate.$__init, null, kareemOptions);
+56
View File
@@ -0,0 +1,56 @@
'use strict';
const get = require('../get');
/*!
* Register methods for this model
*
* @param {Model} model
* @param {Schema} schema
*/
module.exports = function applyMethods(model, schema) {
function apply(method, schema) {
Object.defineProperty(model.prototype, method, {
get: function() {
const h = {};
for (const k in schema.methods[method]) {
h[k] = schema.methods[method][k].bind(this);
}
return h;
},
configurable: true
});
}
for (const method of Object.keys(schema.methods)) {
const fn = schema.methods[method];
if (schema.tree.hasOwnProperty(method)) {
throw new Error('You have a method and a property in your schema both ' +
'named "' + method + '"');
}
if (schema.reserved[method] &&
!get(schema, `methodOptions.${method}.suppressWarning`, false)) {
console.warn(`mongoose: the method name "${method}" is used by mongoose ` +
'internally, overwriting it may cause bugs. If you\'re sure you know ' +
'what you\'re doing, you can suppress this error by using ' +
`\`schema.method('${method}', fn, { suppressWarning: true })\`.`);
}
if (typeof fn === 'function') {
model.prototype[method] = fn;
} else {
apply(method, schema);
}
}
// Recursively call `applyMethods()` on child schemas
model.$appliedMethods = true;
for (const key of Object.keys(schema.paths)) {
const type = schema.paths[key];
if (type.$isSingleNested && !type.caster.$appliedMethods) {
applyMethods(type.caster, type.schema);
}
if (type.$isMongooseDocumentArray && !type.Constructor.$appliedMethods) {
applyMethods(type.Constructor, type.schema);
}
}
};
+61
View File
@@ -0,0 +1,61 @@
'use strict';
const utils = require('../../utils');
module.exports = function applyStaticHooks(model, hooks, statics) {
const kareemOptions = {
useErrorHandlers: true,
numCallbackParams: 1
};
hooks = hooks.filter(hook => hook.model !== false);
model.$__insertMany = hooks.createWrapper('insertMany',
model.$__insertMany, model, kareemOptions);
for (const key of Object.keys(statics)) {
if (hooks.hasHooks(key)) {
const original = model[key];
model[key] = function() {
const numArgs = arguments.length;
const lastArg = numArgs > 0 ? arguments[numArgs - 1] : null;
const cb = typeof lastArg === 'function' ? lastArg : null;
const args = Array.prototype.slice.
call(arguments, 0, cb == null ? numArgs : numArgs - 1);
// Special case: can't use `Kareem#wrap()` because it doesn't currently
// support wrapped functions that return a promise.
return utils.promiseOrCallback(cb, callback => {
hooks.execPre(key, model, args, function(err) {
if (err != null) {
return callback(err);
}
let postCalled = 0;
const ret = original.apply(model, args.concat(post));
if (ret != null && typeof ret.then === 'function') {
ret.then(res => post(null, res), err => post(err));
}
function post(error, res) {
if (postCalled++ > 0) {
return;
}
if (error != null) {
return callback(error);
}
hooks.execPost(key, model, [res], function(error) {
if (error != null) {
return callback(error);
}
callback(null, res);
});
}
});
}, model.events);
};
}
}
};
+12
View File
@@ -0,0 +1,12 @@
'use strict';
/*!
* Register statics for this model
* @param {Model} model
* @param {Schema} schema
*/
module.exports = function applyStatics(model, schema) {
for (const i in schema.statics) {
model[i] = schema.statics[i];
}
};
+140
View File
@@ -0,0 +1,140 @@
'use strict';
const applyTimestampsToChildren = require('../update/applyTimestampsToChildren');
const applyTimestampsToUpdate = require('../update/applyTimestampsToUpdate');
const cast = require('../../cast');
const castUpdate = require('../query/castUpdate');
const setDefaultsOnInsert = require('../setDefaultsOnInsert');
/*!
* Given a model and a bulkWrite op, return a thunk that handles casting and
* validating the individual op.
*/
module.exports = function castBulkWrite(model, op, options) {
const now = model.base.now();
if (op['insertOne']) {
return (callback) => {
const doc = new model(op['insertOne']['document']);
if (model.schema.options.timestamps != null) {
doc.initializeTimestamps();
}
if (options.session != null) {
doc.$session(options.session);
}
op['insertOne']['document'] = doc;
op['insertOne']['document'].validate({ __noPromise: true }, function(error) {
if (error) {
return callback(error, null);
}
callback(null);
});
};
} else if (op['updateOne']) {
op = op['updateOne'];
return (callback) => {
try {
op['filter'] = cast(model.schema, op['filter']);
op['update'] = castUpdate(model.schema, op['update'], {
strict: model.schema.options.strict,
overwrite: false
});
if (op.setDefaultsOnInsert) {
setDefaultsOnInsert(op['filter'], model.schema, op['update'], {
setDefaultsOnInsert: true,
upsert: op.upsert
});
}
if (model.schema.$timestamps != null) {
const createdAt = model.schema.$timestamps.createdAt;
const updatedAt = model.schema.$timestamps.updatedAt;
applyTimestampsToUpdate(now, createdAt, updatedAt, op['update'], {});
}
applyTimestampsToChildren(now, op['update'], model.schema);
} catch (error) {
return callback(error, null);
}
callback(null);
};
} else if (op['updateMany']) {
op = op['updateMany'];
return (callback) => {
try {
op['filter'] = cast(model.schema, op['filter']);
op['update'] = castUpdate(model.schema, op['update'], {
strict: model.schema.options.strict,
overwrite: false
});
if (op.setDefaultsOnInsert) {
setDefaultsOnInsert(op['filter'], model.schema, op['update'], {
setDefaultsOnInsert: true,
upsert: op.upsert
});
}
if (model.schema.$timestamps != null) {
const createdAt = model.schema.$timestamps.createdAt;
const updatedAt = model.schema.$timestamps.updatedAt;
applyTimestampsToUpdate(now, createdAt, updatedAt, op['update'], {});
}
applyTimestampsToChildren(now, op['update'], model.schema);
} catch (error) {
return callback(error, null);
}
callback(null);
};
} else if (op['replaceOne']) {
return (callback) => {
try {
op['replaceOne']['filter'] = cast(model.schema,
op['replaceOne']['filter']);
} catch (error) {
return callback(error, null);
}
// set `skipId`, otherwise we get "_id field cannot be changed"
const doc = new model(op['replaceOne']['replacement'], null, true);
if (model.schema.options.timestamps != null) {
doc.initializeTimestamps();
}
if (options.session != null) {
doc.$session(options.session);
}
op['replaceOne']['replacement'] = doc;
op['replaceOne']['replacement'].validate({ __noPromise: true }, function(error) {
if (error) {
return callback(error, null);
}
callback(null);
});
};
} else if (op['deleteOne']) {
return (callback) => {
try {
op['deleteOne']['filter'] = cast(model.schema,
op['deleteOne']['filter']);
} catch (error) {
return callback(error, null);
}
callback(null);
};
} else if (op['deleteMany']) {
return (callback) => {
try {
op['deleteMany']['filter'] = cast(model.schema,
op['deleteMany']['filter']);
} catch (error) {
return callback(error, null);
}
callback(null);
};
} else {
return (callback) => {
callback(new Error('Invalid op passed to `bulkWrite()`'), null);
};
}
};
+1 -1
View File
@@ -82,7 +82,7 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu
}
utils.merge(schema, baseSchema, {
omit: { discriminators: true, $parentSchema: true },
omit: { discriminators: true },
omitNested: conflictingPaths.reduce((cur, path) => {
cur['tree.' + path] = true;
return cur;
+12
View File
@@ -0,0 +1,12 @@
'use strict';
module.exports = function once(fn) {
let called = false;
return function() {
if (called) {
return;
}
called = true;
return fn.apply(null, arguments);
};
};
@@ -0,0 +1,85 @@
'use strict';
module.exports = assignRawDocsToIdStructure;
/*!
* Assign `vals` returned by mongo query to the `rawIds`
* structure returned from utils.getVals() honoring
* query sort order if specified by user.
*
* This can be optimized.
*
* Rules:
*
* if the value of the path is not an array, use findOne rules, else find.
* for findOne the results are assigned directly to doc path (including null results).
* for find, if user specified sort order, results are assigned directly
* else documents are put back in original order of array if found in results
*
* @param {Array} rawIds
* @param {Array} vals
* @param {Boolean} sort
* @api private
*/
function assignRawDocsToIdStructure(rawIds, resultDocs, resultOrder, options, recursed) {
// honor user specified sort order
const newOrder = [];
const sorting = options.sort && rawIds.length > 1;
let doc;
let sid;
let id;
for (let i = 0; i < rawIds.length; ++i) {
id = rawIds[i];
if (Array.isArray(id)) {
// handle [ [id0, id2], [id3] ]
assignRawDocsToIdStructure(id, resultDocs, resultOrder, options, true);
newOrder.push(id);
continue;
}
if (id === null && !sorting) {
// keep nulls for findOne unless sorting, which always
// removes them (backward compat)
newOrder.push(id);
continue;
}
sid = String(id);
doc = resultDocs[sid];
// If user wants separate copies of same doc, use this option
if (options.clone) {
doc = doc.constructor.hydrate(doc._doc);
}
if (recursed) {
if (doc) {
if (sorting) {
newOrder[resultOrder[sid]] = doc;
} else {
newOrder.push(doc);
}
} else {
newOrder.push(id);
}
} else {
// apply findOne behavior - if document in results, assign, else assign null
newOrder[i] = doc || null;
}
}
rawIds.length = 0;
if (newOrder.length) {
// reassign the documents based on corrected order
// forEach skips over sparse entries in arrays so we
// can safely use this to our advantage dealing with sorted
// result sets too.
newOrder.forEach(function(doc, i) {
rawIds[i] = doc;
});
}
}
+215
View File
@@ -0,0 +1,215 @@
'use strict';
const assignRawDocsToIdStructure = require('./assignRawDocsToIdStructure');
const get = require('../get');
const getVirtual = require('./getVirtual');
const leanPopulateMap = require('./leanPopulateMap');
const mpath = require('mpath');
const sift = require('sift').default;
const utils = require('../../utils');
module.exports = function assignVals(o) {
// Options that aren't explicitly listed in `populateOptions`
const userOptions = get(o, 'allOptions.options.options');
// `o.options` contains options explicitly listed in `populateOptions`, like
// `match` and `limit`.
const populateOptions = Object.assign({}, o.options, userOptions, {
justOne: o.justOne
});
const originalIds = [].concat(o.rawIds);
// replace the original ids in our intermediate _ids structure
// with the documents found by query
assignRawDocsToIdStructure(o.rawIds, o.rawDocs, o.rawOrder, populateOptions);
// now update the original documents being populated using the
// result structure that contains real documents.
const docs = o.docs;
const rawIds = o.rawIds;
const options = o.options;
const count = o.count && o.isVirtual;
function setValue(val) {
if (count) {
return val;
}
if (o.justOne === true && Array.isArray(val)) {
return valueFilter(val[0], options, populateOptions);
} else if (o.justOne === false && !Array.isArray(val)) {
return valueFilter([val], options, populateOptions);
}
return valueFilter(val, options, populateOptions);
}
for (let i = 0; i < docs.length; ++i) {
const existingVal = utils.getValue(o.path, docs[i]);
if (existingVal == null && !getVirtual(o.originalModel.schema, o.path)) {
continue;
}
let valueToSet;
if (count) {
valueToSet = numDocs(rawIds[i]);
} else if (Array.isArray(o.match)) {
valueToSet = Array.isArray(rawIds[i]) ?
sift(o.match[i], rawIds[i]) :
sift(o.match[i], [rawIds[i]])[0];
} else {
valueToSet = rawIds[i];
}
// If we're populating a map, the existing value will be an object, so
// we need to transform again
const originalSchema = o.originalModel.schema;
const isDoc = get(docs[i], '$__', null) != null;
let isMap = isDoc ?
existingVal instanceof Map :
utils.isPOJO(existingVal);
// If we pass the first check, also make sure the local field's schematype
// is map (re: gh-6460)
isMap = isMap && get(originalSchema._getSchema(o.path), '$isSchemaMap');
if (!o.isVirtual && isMap) {
const _keys = existingVal instanceof Map ?
Array.from(existingVal.keys()) :
Object.keys(existingVal);
valueToSet = valueToSet.reduce((cur, v, i) => {
cur.set(_keys[i], v);
return cur;
}, new Map());
}
if (o.isVirtual && isDoc) {
docs[i].populated(o.path, o.justOne ? originalIds[0] : originalIds, o.allOptions);
// If virtual populate and doc is already init-ed, need to walk through
// the actual doc to set rather than setting `_doc` directly
mpath.set(o.path, valueToSet, docs[i], setValue);
continue;
}
const parts = o.path.split('.');
let cur = docs[i];
for (let j = 0; j < parts.length - 1; ++j) {
if (cur[parts[j]] == null) {
cur[parts[j]] = {};
}
cur = cur[parts[j]];
// If the property in MongoDB is a primitive, we won't be able to populate
// the nested path, so skip it. See gh-7545
if (typeof cur !== 'object') {
return;
}
}
if (docs[i].$__) {
docs[i].populated(o.path, o.allIds[i], o.allOptions);
}
// If lean, need to check that each individual virtual respects
// `justOne`, because you may have a populated virtual with `justOne`
// underneath an array. See gh-6867
utils.setValue(o.path, valueToSet, docs[i], setValue, false);
}
};
function numDocs(v) {
if (Array.isArray(v)) {
return v.length;
}
return v == null ? 0 : 1;
}
/*!
* 1) Apply backwards compatible find/findOne behavior to sub documents
*
* find logic:
* a) filter out non-documents
* b) remove _id from sub docs when user specified
*
* findOne
* a) if no doc found, set to null
* b) remove _id from sub docs when user specified
*
* 2) Remove _ids when specified by users query.
*
* background:
* _ids are left in the query even when user excludes them so
* that population mapping can occur.
*/
function valueFilter(val, assignmentOpts, populateOptions) {
if (Array.isArray(val)) {
// find logic
const ret = [];
const numValues = val.length;
for (let i = 0; i < numValues; ++i) {
const subdoc = val[i];
if (!isPopulatedObject(subdoc) && (!populateOptions.retainNullValues || subdoc != null)) {
continue;
}
maybeRemoveId(subdoc, assignmentOpts);
ret.push(subdoc);
if (assignmentOpts.originalLimit &&
ret.length >= assignmentOpts.originalLimit) {
break;
}
}
// Since we don't want to have to create a new mongoosearray, make sure to
// modify the array in place
while (val.length > ret.length) {
Array.prototype.pop.apply(val, []);
}
for (let i = 0; i < ret.length; ++i) {
val[i] = ret[i];
}
return val;
}
// findOne
if (isPopulatedObject(val)) {
maybeRemoveId(val, assignmentOpts);
return val;
}
if (val instanceof Map) {
return val;
}
if (populateOptions.justOne === true) {
return (val == null ? val : null);
}
if (populateOptions.justOne === false) {
return [];
}
return val;
}
/*!
* Remove _id from `subdoc` if user specified "lean" query option
*/
function maybeRemoveId(subdoc, assignmentOpts) {
if (assignmentOpts.excludeId) {
if (typeof subdoc.setValue === 'function') {
delete subdoc._doc._id;
} else {
delete subdoc._id;
}
}
}
/*!
* Determine if `obj` is something we can set a populated path to. Can be a
* document, a lean document, or an array/map that contains docs.
*/
function isPopulatedObject(obj) {
if (obj == null) {
return false;
}
return Array.isArray(obj) ||
obj.$isMongooseMap ||
obj.$__ != null ||
leanPopulateMap.has(obj);
}
+196
View File
@@ -0,0 +1,196 @@
'use strict';
/*!
* ignore
*/
const Mixed = require('../../schema/mixed');
const get = require('../get');
const leanPopulateMap = require('./leanPopulateMap');
const mpath = require('mpath');
/*!
* @param {Schema} schema
* @param {Object} doc POJO
* @param {string} path
*/
module.exports = function getSchemaTypes(schema, doc, path) {
const pathschema = schema.path(path);
const topLevelDoc = doc;
if (pathschema) {
return pathschema;
}
function search(parts, schema, subdoc, nestedPath) {
let p = parts.length + 1;
let foundschema;
let trypath;
while (p--) {
trypath = parts.slice(0, p).join('.');
foundschema = schema.path(trypath);
if (foundschema == null) {
continue;
}
if (foundschema.caster) {
// array of Mixed?
if (foundschema.caster instanceof Mixed) {
return foundschema.caster;
}
let schemas = null;
if (doc != null && foundschema.schema != null && foundschema.schema.discriminators != null) {
const discriminators = foundschema.schema.discriminators;
const discriminatorKeyPath = trypath + '.' +
foundschema.schema.options.discriminatorKey;
const keys = subdoc ? mpath.get(discriminatorKeyPath, subdoc) || [] : [];
schemas = Object.keys(discriminators).
reduce(function(cur, discriminator) {
if (keys.indexOf(discriminator) !== -1) {
cur.push(discriminators[discriminator]);
}
return cur;
}, []);
}
// Now that we found the array, we need to check if there
// are remaining document paths to look up for casting.
// Also we need to handle array.$.path since schema.path
// doesn't work for that.
// If there is no foundschema.schema we are dealing with
// a path like array.$
if (p !== parts.length && foundschema.schema) {
let ret;
if (parts[p] === '$') {
if (p + 1 === parts.length) {
// comments.$
return foundschema;
}
// comments.$.comments.$.title
ret = search(
parts.slice(p + 1),
schema,
subdoc ? mpath.get(trypath, subdoc) : null,
nestedPath.concat(parts.slice(0, p))
);
if (ret) {
ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
!foundschema.schema.$isSingleNested;
}
return ret;
}
if (schemas != null && schemas.length > 0) {
ret = [];
for (let i = 0; i < schemas.length; ++i) {
const _ret = search(
parts.slice(p),
schemas[i],
subdoc ? mpath.get(trypath, subdoc) : null,
nestedPath.concat(parts.slice(0, p))
);
if (_ret != null) {
_ret.$isUnderneathDocArray = _ret.$isUnderneathDocArray ||
!foundschema.schema.$isSingleNested;
if (_ret.$isUnderneathDocArray) {
ret.$isUnderneathDocArray = true;
}
ret.push(_ret);
}
}
return ret;
} else {
ret = search(
parts.slice(p),
foundschema.schema,
subdoc ? mpath.get(trypath, subdoc) : null,
nestedPath.concat(parts.slice(0, p))
);
if (ret) {
ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
!foundschema.schema.$isSingleNested;
}
return ret;
}
} else if (p !== parts.length &&
foundschema.$isMongooseArray &&
foundschema.casterConstructor.$isMongooseArray) {
// Nested arrays. Drill down to the bottom of the nested array.
// Ignore discriminators.
let type = foundschema;
while (type.$isMongooseArray && !type.$isMongooseDocumentArray) {
type = type.casterConstructor;
}
return search(
parts.slice(p),
type.schema,
null,
nestedPath.concat(parts.slice(0, p))
);
}
}
const fullPath = nestedPath.concat([trypath]).join('.');
if (topLevelDoc.$__ && topLevelDoc.populated(fullPath) && p < parts.length) {
const schema = get(doc.$__.populated[fullPath], 'options.model.schema');
if (schema != null) {
const ret = search(
parts.slice(p),
schema,
subdoc ? mpath.get(trypath, subdoc) : null,
nestedPath.concat(parts.slice(0, p))
);
if (ret) {
ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
!schema.$isSingleNested;
}
return ret;
}
}
const _val = get(topLevelDoc, trypath);
if (_val != null) {
const model = Array.isArray(_val) && _val.length > 0 ?
leanPopulateMap.get(_val[0]) :
leanPopulateMap.get(_val);
// Populated using lean, `leanPopulateMap` value is the foreign model
const schema = model != null ? model.schema : null;
if (schema != null) {
const ret = search(
parts.slice(p),
schema,
subdoc ? mpath.get(trypath, subdoc) : null,
nestedPath.concat(parts.slice(0, p))
);
if (ret) {
ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
!schema.$isSingleNested;
}
return ret;
}
}
return foundschema;
}
}
// look for arrays
const parts = path.split('.');
for (let i = 0; i < parts.length; ++i) {
if (parts[i] === '$') {
// Re: gh-5628, because `schema.path()` doesn't take $ into account.
parts[i] = '0';
}
}
return search(parts, schema, doc, []);
};
+61
View File
@@ -0,0 +1,61 @@
'use strict';
module.exports = getVirtual;
/*!
* ignore
*/
function getVirtual(schema, name) {
if (schema.virtuals[name]) {
return schema.virtuals[name];
}
const parts = name.split('.');
let cur = '';
let nestedSchemaPath = '';
for (let i = 0; i < parts.length; ++i) {
cur += (cur.length > 0 ? '.' : '') + parts[i];
if (schema.virtuals[cur]) {
if (i === parts.length - 1) {
schema.virtuals[cur].$nestedSchemaPath = nestedSchemaPath;
return schema.virtuals[cur];
}
continue;
}
if (schema.nested[cur]) {
continue;
}
if (schema.paths[cur] && schema.paths[cur].schema) {
schema = schema.paths[cur].schema;
const rest = parts.slice(i + 1).join('.');
if (schema.virtuals[rest]) {
if (i === parts.length - 2) {
schema.virtuals[rest].$nestedSchemaPath =
[nestedSchemaPath, cur].filter(v => !!v).join('.');
return schema.virtuals[rest];
}
continue;
}
if (i + 1 < parts.length && schema.discriminators) {
for (const key of Object.keys(schema.discriminators)) {
const _virtual = getVirtual(schema.discriminators[key], rest);
if (_virtual != null) {
_virtual.$nestedSchemaPath = [nestedSchemaPath, cur].
filter(v => !!v).join('.');
return _virtual;
}
}
}
nestedSchemaPath += (nestedSchemaPath.length > 0 ? '.' : '') + cur;
cur = '';
continue;
}
return null;
}
}
+7
View File
@@ -0,0 +1,7 @@
'use strict';
/*!
* ignore
*/
module.exports = new WeakMap();
+45
View File
@@ -0,0 +1,45 @@
'use strict';
module.exports = function normalizeRefPath(refPath, doc, populatedPath) {
if (refPath == null) {
return refPath;
}
if (typeof refPath === 'function') {
refPath = refPath.call(doc, doc, populatedPath);
}
// If populated path has numerics, the end `refPath` should too. For example,
// if populating `a.0.b` instead of `a.b` and `b` has `refPath = a.c`, we
// should return `a.0.c` for the refPath.
const hasNumericProp = /(\.\d+$|\.\d+\.)/g;
if (hasNumericProp.test(populatedPath)) {
const chunks = populatedPath.split(hasNumericProp);
if (chunks[chunks.length - 1] === '') {
throw new Error('Can\'t populate individual element in an array');
}
let _refPath = '';
let _remaining = refPath;
// 2nd, 4th, etc. will be numeric props. For example: `[ 'a', '.0.', 'b' ]`
for (let i = 0; i < chunks.length; i += 2) {
const chunk = chunks[i];
if (_remaining.startsWith(chunk + '.')) {
_refPath += _remaining.substr(0, chunk.length) + chunks[i + 1];
_remaining = _remaining.substr(chunk.length + 1);
} else if (i === chunks.length - 1) {
_refPath += _remaining;
_remaining = '';
break;
} else {
throw new Error('Could not normalize ref path, chunk ' + chunk + ' not in populated path');
}
}
return _refPath;
}
return refPath;
};
+19
View File
@@ -0,0 +1,19 @@
'use strict';
const MongooseError = require('../../error/mongooseError');
const util = require('util');
module.exports = validateRef;
function validateRef(ref, path) {
if (typeof ref === 'string') {
return;
}
if (typeof ref === 'function') {
return;
}
throw new MongooseError('Invalid ref at path "' + path + '". Got ' +
util.inspect(ref, { depth: 0 }));
}
+8
View File
@@ -0,0 +1,8 @@
'use strict';
if (typeof jest !== 'undefined' && typeof window !== 'undefined') {
console.warn('Mongoose: looks like you\'re trying to test a Mongoose app ' +
'with Jest\'s default jsdom test environment. Please make sure you read ' +
'Mongoose\'s docs on configuring Jest to test Node.js apps: ' +
'http://mongoosejs.com/docs/jest.html');
}
+18
View File
@@ -0,0 +1,18 @@
'use strict';
/*!
* ignore
*/
module.exports = function isDefiningProjection(val) {
if (val == null) {
// `undefined` or `null` become exclusive projections
return true;
}
if (typeof val === 'object') {
// Only cases where a value does **not** define whether the whole projection
// is inclusive or exclusive are `$meta` and `$slice`.
return !('$meta' in val) && !('$slice' in val);
}
return true;
};
+28
View File
@@ -0,0 +1,28 @@
'use strict';
const isDefiningProjection = require('./isDefiningProjection');
/*!
* ignore
*/
module.exports = function isExclusive(projection) {
const keys = Object.keys(projection);
let ki = keys.length;
let exclude = null;
if (ki === 1 && keys[0] === '_id') {
exclude = !!projection[keys[ki]];
} else {
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]];
break;
}
}
}
return exclude;
};
+34
View File
@@ -0,0 +1,34 @@
'use strict';
const isDefiningProjection = require('./isDefiningProjection');
/*!
* ignore
*/
module.exports = function isInclusive(projection) {
if (projection == null) {
return false;
}
const props = Object.keys(projection);
const numProps = props.length;
if (numProps === 0) {
return false;
}
for (let i = 0; i < numProps; ++i) {
const prop = props[i];
// Plus paths can't define the projection (see gh-7050)
if (prop.charAt(0) === '+') {
continue;
}
// 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;
}
}
return false;
};
+35
View File
@@ -0,0 +1,35 @@
'use strict';
const isDefiningProjection = require('./isDefiningProjection');
/*!
* Determines if `path` is excluded by `projection`
*
* @param {Object} projection
* @param {string} path
* @return {Boolean}
*/
module.exports = function isPathExcluded(projection, path) {
if (path === '_id') {
return projection._id === 0;
}
const paths = Object.keys(projection);
let type = null;
for (const _path of paths) {
if (isDefiningProjection(projection[_path])) {
type = projection[path] === 1 ? 'inclusive' : 'exclusive';
break;
}
}
if (type === 'inclusive') {
return projection[path] !== 1;
}
if (type === 'exclusive') {
return projection[path] === 0;
}
return false;
};
@@ -0,0 +1,28 @@
'use strict';
/*!
* ignore
*/
module.exports = function isPathSelectedInclusive(fields, path) {
const chunks = path.split('.');
let cur = '';
let j;
let keys;
let numKeys;
for (let i = 0; i < chunks.length; ++i) {
cur += cur.length ? '.' : '' + chunks[i];
if (fields[cur]) {
keys = Object.keys(fields);
numKeys = keys.length;
for (j = 0; j < numKeys; ++j) {
if (keys[i].indexOf(cur + '.') === 0 && keys[i].indexOf(path) !== 0) {
continue;
}
}
return true;
}
}
return false;
};
+70
View File
@@ -0,0 +1,70 @@
'use strict';
/*!
* ignore
*/
module.exports = applyQueryMiddleware;
/*!
* ignore
*/
applyQueryMiddleware.middlewareFunctions = [
'count',
'countDocuments',
'deleteMany',
'deleteOne',
'distinct',
'estimatedDocumentCount',
'find',
'findOne',
'findOneAndDelete',
'findOneAndRemove',
'findOneAndReplace',
'findOneAndUpdate',
'remove',
'replaceOne',
'update',
'updateMany',
'updateOne'
];
/*!
* Apply query middleware
*
* @param {Query} query constructor
* @param {Model} model
*/
function applyQueryMiddleware(Query, model) {
const kareemOptions = {
useErrorHandlers: true,
numCallbackParams: 1,
nullResultByDefault: true
};
const middleware = model.hooks.filter(hook => {
if (hook.name === 'updateOne' || hook.name === 'deleteOne') {
return hook.query == null || !!hook.query;
}
if (hook.name === 'remove') {
return !!hook.query;
}
return true;
});
// `update()` thunk has a different name because `_update` was already taken
Query.prototype._execUpdate = middleware.createWrapper('update',
Query.prototype._execUpdate, null, kareemOptions);
// `distinct()` thunk has a different name because `_distinct` was already taken
Query.prototype.__distinct = middleware.createWrapper('distinct',
Query.prototype.__distinct, null, kareemOptions);
applyQueryMiddleware.middlewareFunctions.
filter(v => v !== 'update' && v !== 'distinct').
forEach(fn => {
Query.prototype[`_${fn}`] = middleware.createWrapper(fn,
Query.prototype[`_${fn}`], null, kareemOptions);
});
}
+1 -1
View File
@@ -52,4 +52,4 @@ module.exports = function castFilterPath(query, schematype, val) {
}
return val;
};
};
+442
View File
@@ -0,0 +1,442 @@
'use strict';
const CastError = require('../../error/cast');
const StrictModeError = require('../../error/strict');
const ValidationError = require('../../error/validation');
const castNumber = require('../../cast/number');
const getEmbeddedDiscriminatorPath = require('./getEmbeddedDiscriminatorPath');
const utils = require('../../utils');
/*!
* Casts an update op based on the given schema
*
* @param {Schema} schema
* @param {Object} obj
* @param {Object} options
* @param {Boolean} [options.overwrite] defaults to false
* @param {Boolean|String} [options.strict] defaults to true
* @param {Query} context passed to setters
* @return {Boolean} true iff the update is non-empty
*/
module.exports = function castUpdate(schema, obj, options, context, filter) {
if (!obj) {
return undefined;
}
const ops = Object.keys(obj);
let i = ops.length;
const ret = {};
let hasKeys;
let val;
let hasDollarKey = false;
const overwrite = options.overwrite;
filter = filter || {};
while (i--) {
const op = ops[i];
// if overwrite is set, don't do any of the special $set stuff
if (op[0] !== '$' && !overwrite) {
// fix up $set sugar
if (!ret.$set) {
if (obj.$set) {
ret.$set = obj.$set;
} else {
ret.$set = {};
}
}
ret.$set[op] = obj[op];
ops.splice(i, 1);
if (!~ops.indexOf('$set')) ops.push('$set');
} else if (op === '$set') {
if (!ret.$set) {
ret[op] = obj[op];
}
} else {
ret[op] = obj[op];
}
}
// cast each value
i = ops.length;
// if we get passed {} for the update, we still need to respect that when it
// is an overwrite scenario
if (overwrite) {
hasKeys = true;
}
while (i--) {
const op = ops[i];
val = ret[op];
hasDollarKey = hasDollarKey || op.charAt(0) === '$';
if (val &&
typeof val === 'object' &&
!Buffer.isBuffer(val) &&
(!overwrite || hasDollarKey)) {
hasKeys |= walkUpdatePath(schema, val, op, options, context, filter);
} else if (overwrite && ret && typeof ret === 'object') {
// if we are just using overwrite, cast the query and then we will
// *always* return the value, even if it is an empty object. We need to
// set hasKeys above because we need to account for the case where the
// user passes {} and wants to clobber the whole document
// Also, _walkUpdatePath expects an operation, so give it $set since that
// is basically what we're doing
walkUpdatePath(schema, ret, '$set', options, context, filter);
} else {
const msg = 'Invalid atomic update value for ' + op + '. '
+ 'Expected an object, received ' + typeof val;
throw new Error(msg);
}
}
return hasKeys && ret;
};
/*!
* Walk each path of obj and cast its values
* according to its schema.
*
* @param {Schema} schema
* @param {Object} obj - part of a query
* @param {String} op - the atomic operator ($pull, $set, etc)
* @param {Object} options
* @param {Boolean|String} [options.strict]
* @param {Boolean} [options.omitUndefined]
* @param {Query} context
* @param {String} pref - path prefix (internal only)
* @return {Bool} true if this path has keys to update
* @api private
*/
function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
const strict = options.strict;
const prefix = pref ? pref + '.' : '';
const keys = Object.keys(obj);
let i = keys.length;
let hasKeys = false;
let schematype;
let key;
let val;
let aggregatedError = null;
let useNestedStrict;
if (options.useNestedStrict === undefined) {
useNestedStrict = schema.options.useNestedStrict;
} else {
useNestedStrict = options.useNestedStrict;
}
while (i--) {
key = keys[i];
val = obj[key];
if (val && val.constructor.name === 'Object') {
// watch for embedded doc schemas
schematype = schema._getSchema(prefix + key);
if (schematype && schematype.caster && op in castOps) {
// embedded doc schema
if ('$each' in val) {
hasKeys = true;
try {
obj[key] = {
$each: castUpdateVal(schematype, val.$each, op, key, context, prefix + key)
};
} catch (error) {
aggregatedError = _handleCastError(error, context, key, aggregatedError);
}
if (val.$slice != null) {
obj[key].$slice = val.$slice | 0;
}
if (val.$sort) {
obj[key].$sort = val.$sort;
}
if (!!val.$position || val.$position === 0) {
obj[key].$position = val.$position;
}
} else {
try {
obj[key] = castUpdateVal(schematype, val, op, key, context, prefix + key);
} catch (error) {
aggregatedError = _handleCastError(error, context, key, aggregatedError);
}
if (options.omitUndefined && obj[key] === void 0) {
delete obj[key];
continue;
}
hasKeys = true;
}
} else if ((op === '$currentDate') || (op in castOps && schematype)) {
// $currentDate can take an object
try {
obj[key] = castUpdateVal(schematype, val, op, key, context, prefix + key);
} catch (error) {
aggregatedError = _handleCastError(error, context, key, aggregatedError);
}
if (options.omitUndefined && obj[key] === void 0) {
delete obj[key];
continue;
}
hasKeys = true;
} else {
const pathToCheck = (prefix + key);
const v = schema._getPathType(pathToCheck);
let _strict = strict;
if (useNestedStrict &&
v &&
v.schema &&
'strict' in v.schema.options) {
_strict = v.schema.options.strict;
}
if (v.pathType === 'undefined') {
if (_strict === 'throw') {
throw new StrictModeError(pathToCheck);
} else if (_strict) {
delete obj[key];
continue;
}
}
// gh-2314
// we should be able to set a schema-less field
// to an empty object literal
hasKeys |= walkUpdatePath(schema, val, op, options, context, filter, prefix + key) ||
(utils.isObject(val) && Object.keys(val).length === 0);
}
} else {
const checkPath = (key === '$each' || key === '$or' || key === '$and' || key === '$in') ?
pref : prefix + key;
schematype = schema._getSchema(checkPath);
let pathDetails = schema._getPathType(checkPath);
// If no schema type, check for embedded discriminators
if (schematype == null) {
const _res = getEmbeddedDiscriminatorPath(schema, obj, filter, checkPath);
if (_res.schematype != null) {
schematype = _res.schematype;
pathDetails = _res.type;
}
}
let isStrict = strict;
if (useNestedStrict &&
pathDetails &&
pathDetails.schema &&
'strict' in pathDetails.schema.options) {
isStrict = pathDetails.schema.options.strict;
}
const skip = isStrict &&
!schematype &&
!/real|nested/.test(pathDetails.pathType);
if (skip) {
// Even if strict is `throw`, avoid throwing an error because of
// virtuals because of #6731
if (isStrict === 'throw' && schema.virtuals[checkPath] == null) {
throw new StrictModeError(prefix + key);
} else {
delete obj[key];
}
} else {
// gh-1845 temporary fix: ignore $rename. See gh-3027 for tracking
// improving this.
if (op === '$rename') {
hasKeys = true;
continue;
}
try {
obj[key] = castUpdateVal(schematype, val, op, key, context, prefix + key);
} catch (error) {
aggregatedError = _handleCastError(error, context, key, aggregatedError);
}
if (Array.isArray(obj[key]) && (op === '$addToSet' || op === '$push') && key !== '$each') {
if (schematype && schematype.caster && !schematype.caster.$isMongooseArray) {
obj[key] = { $each: obj[key] };
}
}
if (options.omitUndefined && obj[key] === void 0) {
delete obj[key];
continue;
}
hasKeys = true;
}
}
}
if (aggregatedError != null) {
throw aggregatedError;
}
return hasKeys;
}
/*!
* ignore
*/
function _handleCastError(error, query, key, aggregatedError) {
if (typeof query !== 'object' || !query.options.multipleCastError) {
throw error;
}
aggregatedError = aggregatedError || new ValidationError();
aggregatedError.addError(key, error);
return aggregatedError;
}
/*!
* These operators should be cast to numbers instead
* of their path schema type.
*/
const numberOps = {
$pop: 1,
$inc: 1
};
/*!
* These ops require no casting because the RHS doesn't do anything.
*/
const noCastOps = {
$unset: 1
};
/*!
* These operators require casting docs
* to real Documents for Update operations.
*/
const castOps = {
$push: 1,
$addToSet: 1,
$set: 1,
$setOnInsert: 1
};
/*!
* ignore
*/
const overwriteOps = {
$set: 1,
$setOnInsert: 1
};
/*!
* Casts `val` according to `schema` and atomic `op`.
*
* @param {SchemaType} schema
* @param {Object} val
* @param {String} op - the atomic operator ($pull, $set, etc)
* @param {String} $conditional
* @param {Query} context
* @api private
*/
function castUpdateVal(schema, val, op, $conditional, context, path) {
if (!schema) {
// non-existing schema path
if (op in numberOps) {
try {
return castNumber(val);
} catch (err) {
throw new CastError('number', val, path);
}
}
return val;
}
const cond = schema.caster && op in castOps &&
(utils.isObject(val) || Array.isArray(val));
if (cond && !overwriteOps[op]) {
// Cast values for ops that add data to MongoDB.
// Ensures embedded documents get ObjectIds etc.
let schemaArrayDepth = 0;
let cur = schema;
while (cur.$isMongooseArray) {
++schemaArrayDepth;
cur = cur.caster;
}
let arrayDepth = 0;
let _val = val;
while (Array.isArray(_val)) {
++arrayDepth;
_val = _val[0];
}
const additionalNesting = schemaArrayDepth - arrayDepth;
while (arrayDepth < schemaArrayDepth) {
val = [val];
++arrayDepth;
}
let tmp = schema.cast(Array.isArray(val) ? val : [val]);
for (let i = 0; i < additionalNesting; ++i) {
tmp = tmp[0];
}
return tmp;
}
if (op in noCastOps) {
return val;
}
if (op in numberOps) {
// Null and undefined not allowed for $pop, $inc
if (val == null) {
throw new CastError('number', val, schema.path);
}
if (op === '$inc') {
// Support `$inc` with long, int32, etc. (gh-4283)
return schema.castForQueryWrapper({
val: val,
context: context
});
}
try {
return castNumber(val);
} catch (error) {
throw new CastError('number', val, schema.path);
}
}
if (op === '$currentDate') {
if (typeof val === 'object') {
return {$type: val.$type};
}
return Boolean(val);
}
if (/^\$/.test($conditional)) {
return schema.castForQueryWrapper({
$conditional: $conditional,
val: val,
context: context
});
}
if (overwriteOps[op]) {
return schema.castForQueryWrapper({
val: val,
context: context,
$skipQueryCastForUpdate: val != null && schema.$isMongooseArray && schema.$fullPath != null && !schema.$fullPath.match(/\d+$/)
});
}
return schema.castForQueryWrapper({ val: val, context: context });
}
+47
View File
@@ -0,0 +1,47 @@
'use strict';
const helpers = require('../../queryhelpers');
module.exports = completeMany;
/*!
* Given a model and an array of docs, hydrates all the docs to be instances
* of the model. Used to initialize docs returned from the db from `find()`
*
* @param {Model} model
* @param {Array} docs
* @param {Object} fields the projection used, including `select` from schemas
* @param {Object} userProvidedFields the user-specified projection
* @param {Object} opts
* @param {Array} [opts.populated]
* @param {ClientSession} [opts.session]
* @param {Function} callback
*/
function completeMany(model, docs, fields, userProvidedFields, opts, callback) {
const arr = [];
let count = docs.length;
const len = count;
let error = null;
function init(_error) {
if (_error != null) {
error = error || _error;
}
if (error != null) {
--count || process.nextTick(() => callback(error));
return;
}
--count || process.nextTick(() => callback(error, arr));
}
for (let i = 0; i < len; ++i) {
arr[i] = helpers.createModel(model, docs[i], fields, userProvidedFields);
try {
arr[i].init(docs[i], opts, init);
} catch (error) {
init(error);
}
arr[i].$session(opts.session);
}
}
@@ -0,0 +1,53 @@
'use strict';
const cleanPositionalOperators = require('../schema/cleanPositionalOperators');
const get = require('../get');
/*!
* Like `schema.path()`, except with a document, because impossible to
* determine path type without knowing the embedded discriminator key.
*/
module.exports = function getEmbeddedDiscriminatorPath(schema, update, filter, path) {
const parts = path.split('.');
let schematype = null;
let type = 'adhocOrUndefined';
filter = filter || {};
update = update || {};
for (let i = 0; i < parts.length; ++i) {
const subpath = cleanPositionalOperators(parts.slice(0, i + 1).join('.'));
schematype = schema.path(subpath);
if (schematype == null) {
continue;
}
type = schema.pathType(subpath);
if ((schematype.$isSingleNested || schematype.$isMongooseDocumentArrayElement) &&
schematype.schema.discriminators != null) {
const discriminators = schematype.schema.discriminators;
const discriminatorValuePath = subpath + '.' +
get(schematype, 'schema.options.discriminatorKey');
const discriminatorFilterPath =
discriminatorValuePath.replace(/\.\d+\./, '.');
let discriminatorKey = null;
if (discriminatorValuePath in filter) {
discriminatorKey = filter[discriminatorValuePath];
}
if (discriminatorFilterPath in filter) {
discriminatorKey = filter[discriminatorFilterPath];
}
if (discriminatorKey == null || discriminators[discriminatorKey] == null) {
continue;
}
const rest = parts.slice(i + 1).join('.');
schematype = discriminators[discriminatorKey].path(rest);
if (schematype != null) {
type = discriminators[discriminatorKey]._getPathType(rest);
break;
}
}
}
return { type: type, schematype: schematype };
};
+16
View File
@@ -0,0 +1,16 @@
'use strict';
/*!
* ignore
*/
module.exports = function(obj) {
const keys = Object.keys(obj);
const len = keys.length;
for (let i = 0; i < len; ++i) {
if (keys[i].charAt(0) === '$') {
return true;
}
}
return false;
};
+46
View File
@@ -0,0 +1,46 @@
'use strict';
/*!
* ignore
*/
module.exports = function selectPopulatedFields(query) {
const opts = query._mongooseOptions;
if (opts.populate != null) {
const paths = Object.keys(opts.populate);
const userProvidedFields = query._userProvidedFields || {};
if (query.selectedInclusively()) {
for (let i = 0; i < paths.length; ++i) {
if (!isPathInFields(userProvidedFields, paths[i])) {
query.select(paths[i]);
} else if (userProvidedFields[paths[i]] === 0) {
delete query._fields[paths[i]];
}
}
} else if (query.selectedExclusively()) {
for (let i = 0; i < paths.length; ++i) {
if (userProvidedFields[paths[i]] == null) {
delete query._fields[paths[i]];
}
}
}
}
};
/*!
* ignore
*/
function isPathInFields(userProvidedFields, path) {
const pieces = path.split('.');
const len = pieces.length;
let cur = pieces[0];
for (let i = 1; i < len; ++i) {
if (userProvidedFields[cur] != null) {
return true;
}
cur += '.' + pieces[i];
}
return userProvidedFields[cur] != null;
}
+18
View File
@@ -0,0 +1,18 @@
'use strict';
/*!
* A query thunk is the function responsible for sending the query to MongoDB,
* like `Query#_findOne()` or `Query#_execUpdate()`. The `Query#exec()` function
* calls a thunk. The term "thunk" here is the traditional Node.js definition:
* a function that takes exactly 1 parameter, a callback.
*
* This function defines common behavior for all query thunks.
*/
module.exports = function wrapThunk(fn) {
return function _wrappedThunk(cb) {
++this._executionCount;
fn.call(this, cb);
};
};
+43
View File
@@ -0,0 +1,43 @@
'use strict';
module.exports = function applyPlugins(schema, plugins, options, cacheKey) {
if (schema[cacheKey]) {
return;
}
schema[cacheKey] = true;
if (!options || !options.skipTopLevel) {
for (let i = 0; i < plugins.length; ++i) {
schema.plugin(plugins[i][0], plugins[i][1]);
}
}
options = Object.assign({}, options);
delete options.skipTopLevel;
for (const path of Object.keys(schema.paths)) {
const type = schema.paths[path];
if (type.schema != null) {
applyPlugins(type.schema, plugins, options, cacheKey);
// Recompile schema because plugins may have changed it, see gh-7572
type.caster.prototype.$__setSchema(type.schema);
}
}
const discriminators = schema.discriminators;
if (discriminators == null) {
return;
}
const applyPluginsToDiscriminators = options.applyPluginsToDiscriminators;
const keys = Object.keys(discriminators);
for (let i = 0; i < keys.length; ++i) {
const discriminatorKey = keys[i];
const discriminatorSchema = discriminators[discriminatorKey];
applyPlugins(discriminatorSchema, plugins,
{ skipTopLevel: !applyPluginsToDiscriminators }, cacheKey);
}
};
+16
View File
@@ -0,0 +1,16 @@
'use strict';
const get = require('../get');
module.exports = function applyWriteConcern(schema, options) {
const writeConcern = get(schema, 'options.writeConcern', {});
if (!('w' in options) && writeConcern.w != null) {
options.w = writeConcern.w;
}
if (!('j' in options) && writeConcern.j != null) {
options.j = writeConcern.j;
}
if (!('wtimeout' in options) && writeConcern.wtimeout != null) {
options.wtimeout = writeConcern.wtimeout;
}
};
+12
View File
@@ -0,0 +1,12 @@
'use strict';
/**
* For consistency's sake, we replace positional operator `$` and array filters
* `$[]` and `$[foo]` with `0` when looking up schema paths.
*/
module.exports = function cleanPositionalOperators(path) {
return path.
replace(/\.\$(\[[^\]]*\])?\./g, '.0.').
replace(/\.(\[[^\]]*\])?\$$/g, '.0');
};
+124
View File
@@ -0,0 +1,124 @@
'use strict';
const get = require('../get');
const utils = require('../../utils');
/*!
* Gather all indexes defined in the schema, including single nested,
* document arrays, and embedded discriminators.
*/
module.exports = function getIndexes(schema) {
let indexes = [];
const schemaStack = new WeakMap();
const indexTypes = schema.constructor.indexTypes;
const collectIndexes = function(schema, prefix) {
// Ignore infinitely nested schemas, if we've already seen this schema
// along this path there must be a cycle
if (schemaStack.has(schema)) {
return;
}
schemaStack.set(schema, true);
prefix = prefix || '';
const keys = Object.keys(schema.paths);
const length = keys.length;
for (let i = 0; i < length; ++i) {
const key = keys[i];
const path = schema.paths[key];
if (path.$isMongooseDocumentArray || path.$isSingleNested) {
if (get(path, 'options.excludeIndexes') !== true &&
get(path, 'schemaOptions.excludeIndexes') !== true) {
collectIndexes(path.schema, prefix + key + '.');
}
if (path.schema.discriminators != null) {
const discriminators = path.schema.discriminators;
const discriminatorKeys = Object.keys(discriminators);
for (const discriminatorKey of discriminatorKeys) {
collectIndexes(discriminators[discriminatorKey]._originalSchema,
prefix + key + '.');
}
}
// Retained to minimize risk of backwards breaking changes due to
// gh-6113
if (path.$isMongooseDocumentArray) {
continue;
}
}
const index = path._index || (path.caster && path.caster._index);
if (index !== false && index !== null && index !== undefined) {
const field = {};
const isObject = utils.isObject(index);
const options = isObject ? index : {};
const type = typeof index === 'string' ? index :
isObject ? index.type :
false;
if (type && indexTypes.indexOf(type) !== -1) {
field[prefix + key] = type;
} else if (options.text) {
field[prefix + key] = 'text';
delete options.text;
} else {
field[prefix + key] = 1;
}
delete options.type;
if (!('background' in options)) {
options.background = true;
}
indexes.push([field, options]);
}
}
schemaStack.delete(schema);
if (prefix) {
fixSubIndexPaths(schema, prefix);
} else {
schema._indexes.forEach(function(index) {
if (!('background' in index[1])) {
index[1].background = true;
}
});
indexes = indexes.concat(schema._indexes);
}
};
collectIndexes(schema);
return indexes;
/*!
* Checks for indexes added to subdocs using Schema.index().
* These indexes need their paths prefixed properly.
*
* schema._indexes = [ [indexObj, options], [indexObj, options] ..]
*/
function fixSubIndexPaths(schema, prefix) {
const subindexes = schema._indexes;
const len = subindexes.length;
for (let i = 0; i < len; ++i) {
const indexObj = subindexes[i][0];
const keys = Object.keys(indexObj);
const klen = keys.length;
const newindex = {};
// use forward iteration, order matters
for (let j = 0; j < klen; ++j) {
const key = keys[j];
newindex[prefix + key] = indexObj[key];
}
indexes.push([newindex, subindexes[i][1]]);
}
}
};
+35
View File
@@ -0,0 +1,35 @@
'use strict';
/*!
* Behaves like `Schema#path()`, except for it also digs into arrays without
* needing to put `.0.`, so `getPath(schema, 'docArr.elProp')` works.
*/
module.exports = function getPath(schema, path) {
let schematype = schema.path(path);
if (schematype != null) {
return schematype;
}
const pieces = path.split('.');
let cur = '';
let isArray = false;
for (const piece of pieces) {
if (/^\d+$/.test(piece) && isArray) {
continue;
}
cur = cur.length === 0 ? piece : cur + '.' + piece;
schematype = schema.path(cur);
if (schematype != null && schematype.schema) {
schema = schematype.schema;
cur = '';
if (schematype.$isMongooseDocumentArray) {
isArray = true;
}
}
}
return schematype;
};
+24
View File
@@ -0,0 +1,24 @@
'use strict';
module.exports = handleTimestampOption;
/*!
* ignore
*/
function handleTimestampOption(arg, prop) {
if (arg == null) {
return null;
}
if (typeof arg === 'boolean') {
return prop;
}
if (typeof arg[prop] === 'boolean') {
return arg[prop] ? prop : null;
}
if (!(prop in arg)) {
return prop;
}
return arg[prop];
}
+19
View File
@@ -0,0 +1,19 @@
'use strict';
module.exports = function merge(s1, s2) {
s1.add(s2.obj);
s1.callQueue = s1.callQueue.concat(s2.callQueue);
s1.method(s2.methods);
s1.static(s2.statics);
for (const query in s2.query) {
s1.query[query] = s2.query[query];
}
for (const virtual in s2.virtuals) {
s1.virtuals[virtual] = s2.virtuals[virtual].clone();
}
s1.s.hooks.merge(s2.s.hooks, false);
};
+117
View File
@@ -0,0 +1,117 @@
'use strict';
const modifiedPaths = require('./common').modifiedPaths;
/**
* Applies defaults to update and findOneAndUpdate operations.
*
* @param {Object} filter
* @param {Schema} schema
* @param {Object} castedDoc
* @param {Object} options
* @method setDefaultsOnInsert
* @api private
*/
module.exports = function(filter, schema, castedDoc, options) {
const keys = Object.keys(castedDoc || {});
const updatedKeys = {};
const updatedValues = {};
const numKeys = keys.length;
const modified = {};
let hasDollarUpdate = false;
options = options || {};
if (!options.upsert || !options.setDefaultsOnInsert) {
return castedDoc;
}
for (let i = 0; i < numKeys; ++i) {
if (keys[i].charAt(0) === '$') {
modifiedPaths(castedDoc[keys[i]], '', modified);
hasDollarUpdate = true;
}
}
if (!hasDollarUpdate) {
modifiedPaths(castedDoc, '', modified);
}
const paths = Object.keys(filter);
const numPaths = paths.length;
for (let i = 0; i < numPaths; ++i) {
const path = paths[i];
const condition = filter[path];
if (condition && typeof condition === 'object') {
const conditionKeys = Object.keys(condition);
const numConditionKeys = conditionKeys.length;
let hasDollarKey = false;
for (let j = 0; j < numConditionKeys; ++j) {
if (conditionKeys[j].charAt(0) === '$') {
hasDollarKey = true;
break;
}
}
if (hasDollarKey) {
continue;
}
}
updatedKeys[path] = true;
modified[path] = true;
}
if (options && options.overwrite && !hasDollarUpdate) {
// Defaults will be set later, since we're overwriting we'll cast
// the whole update to a document
return castedDoc;
}
schema.eachPath(function(path, schemaType) {
if (schemaType.$isSingleNested) {
// Only handle nested schemas 1-level deep to avoid infinite
// recursion re: https://github.com/mongodb-js/mongoose-autopopulate/issues/11
schemaType.schema.eachPath(function(_path, _schemaType) {
if (_path === '_id' && _schemaType.auto) {
// Ignore _id if auto id so we don't create subdocs
return;
}
const def = _schemaType.getDefault(null, true);
if (!isModified(modified, path + '.' + _path) &&
typeof def !== 'undefined') {
castedDoc = castedDoc || {};
castedDoc.$setOnInsert = castedDoc.$setOnInsert || {};
castedDoc.$setOnInsert[path + '.' + _path] = def;
updatedValues[path + '.' + _path] = def;
}
});
} else {
const def = schemaType.getDefault(null, true);
if (!isModified(modified, path) && typeof def !== 'undefined') {
castedDoc = castedDoc || {};
castedDoc.$setOnInsert = castedDoc.$setOnInsert || {};
castedDoc.$setOnInsert[path] = def;
updatedValues[path] = def;
}
}
});
return castedDoc;
};
function isModified(modified, path) {
if (modified[path]) {
return true;
}
const sp = path.split('.');
let cur = sp[0];
for (let i = 1; i < sp.length; ++i) {
if (modified[cur]) {
return true;
}
cur += '.' + sp[i];
}
return false;
}
+12
View File
@@ -0,0 +1,12 @@
'use strict';
exports.arrayAtomicsSymbol = Symbol('mongoose#Array#_atomics');
exports.arrayParentSymbol = Symbol('mongoose#Array#_parent');
exports.arrayPathSymbol = Symbol('mongoose#Array#_path');
exports.arraySchemaSymbol = Symbol('mongoose#Array#_schema');
exports.documentArrayParent = Symbol('mongoose:documentArrayParent');
exports.getSymbol = Symbol('mongoose#Document#get');
exports.modelSymbol = Symbol('mongoose#Model');
exports.objectIdSymbol = Symbol('mongoose#ObjectId');
exports.schemaTypeSymbol = Symbol('mongoose#schemaType');
exports.validatorErrorSymbol = Symbol('mongoose:validatorError');
+176
View File
@@ -0,0 +1,176 @@
'use strict';
const cleanPositionalOperators = require('../schema/cleanPositionalOperators');
const handleTimestampOption = require('../schema/handleTimestampOption');
module.exports = applyTimestampsToChildren;
/*!
* ignore
*/
function applyTimestampsToChildren(now, update, schema) {
if (update == null) {
return;
}
const keys = Object.keys(update);
let key;
let createdAt;
let updatedAt;
let timestamps;
let path;
const hasDollarKey = keys.length && keys[0].charAt(0) === '$';
if (hasDollarKey) {
if (update.$push) {
for (key in update.$push) {
const $path = schema.path(key);
if (update.$push[key] &&
$path &&
$path.$isMongooseDocumentArray &&
$path.schema.options.timestamps) {
timestamps = $path.schema.options.timestamps;
createdAt = handleTimestampOption(timestamps, 'createdAt');
updatedAt = handleTimestampOption(timestamps, 'updatedAt');
if (update.$push[key].$each) {
update.$push[key].$each.forEach(function(subdoc) {
if (updatedAt != null) {
subdoc[updatedAt] = now;
}
if (createdAt != null) {
subdoc[createdAt] = now;
}
});
} else {
if (updatedAt != null) {
update.$push[key][updatedAt] = now;
}
if (createdAt != null) {
update.$push[key][createdAt] = now;
}
}
}
}
}
if (update.$set != null) {
const keys = Object.keys(update.$set);
for (key of keys) {
// Replace positional operator `$` and array filters `$[]` and `$[.*]`
const keyToSearch = cleanPositionalOperators(key);
path = schema.path(keyToSearch);
if (!path) {
continue;
}
let parentSchemaType = null;
const pieces = keyToSearch.split('.');
for (let i = pieces.length - 1; i > 0; --i) {
const s = schema.path(pieces.slice(0, i).join('.'));
if (s != null &&
(s.$isMongooseDocumentArray || s.$isSingleNested)) {
parentSchemaType = s;
break;
}
}
if (Array.isArray(update.$set[key]) && path.$isMongooseDocumentArray) {
applyTimestampsToDocumentArray(update.$set[key], path, now);
} else if (update.$set[key] && path.$isSingleNested) {
applyTimestampsToSingleNested(update.$set[key], path, now);
} else if (parentSchemaType != null) {
timestamps = parentSchemaType.schema.options.timestamps;
createdAt = handleTimestampOption(timestamps, 'createdAt');
updatedAt = handleTimestampOption(timestamps, 'updatedAt');
if (updatedAt == null) {
continue;
}
if (parentSchemaType.$isSingleNested) {
// Single nested is easy
update.$set[parentSchemaType.path + '.' + updatedAt] = now;
continue;
}
let childPath = key.substr(parentSchemaType.path.length + 1);
if (/^\d+$/.test(childPath)) {
update.$set[parentSchemaType.path + '.' + childPath][updatedAt] = now;
continue;
}
const firstDot = childPath.indexOf('.');
childPath = firstDot !== -1 ? childPath.substr(0, firstDot) : childPath;
update.$set[parentSchemaType.path + '.' + childPath + '.' + updatedAt] = now;
} else if (path.schema != null && path.schema != schema && update.$set[key]) {
timestamps = path.schema.options.timestamps;
createdAt = handleTimestampOption(timestamps, 'createdAt');
updatedAt = handleTimestampOption(timestamps, 'updatedAt');
if (updatedAt != null) {
update.$set[key][updatedAt] = now;
}
if (createdAt != null) {
update.$set[key][createdAt] = now;
}
}
}
}
} else {
const keys = Object.keys(update).filter(key => !key.startsWith('$'));
for (key of keys) {
// Replace positional operator `$` and array filters `$[]` and `$[.*]`
const keyToSearch = cleanPositionalOperators(key);
path = schema.path(keyToSearch);
if (!path) {
continue;
}
if (Array.isArray(update[key]) && path.$isMongooseDocumentArray) {
applyTimestampsToDocumentArray(update[key], path, now);
} else if (update[key] != null && path.$isSingleNested) {
applyTimestampsToSingleNested(update[key], path, now);
}
}
}
}
function applyTimestampsToDocumentArray(arr, schematype, now) {
const timestamps = schematype.schema.options.timestamps;
if (!timestamps) {
return;
}
const len = arr.length;
const createdAt = handleTimestampOption(timestamps, 'createdAt');
const updatedAt = handleTimestampOption(timestamps, 'updatedAt');
for (let i = 0; i < len; ++i) {
if (updatedAt != null) {
arr[i][updatedAt] = now;
}
if (createdAt != null) {
arr[i][createdAt] = now;
}
}
}
function applyTimestampsToSingleNested(subdoc, schematype, now) {
const timestamps = schematype.schema.options.timestamps;
if (!timestamps) {
return;
}
const createdAt = handleTimestampOption(timestamps, 'createdAt');
const updatedAt = handleTimestampOption(timestamps, 'updatedAt');
if (updatedAt != null) {
subdoc[updatedAt] = now;
}
if (createdAt != null) {
subdoc[createdAt] = now;
}
}
+65
View File
@@ -0,0 +1,65 @@
'use strict';
/*!
* ignore
*/
const get = require('../get');
module.exports = applyTimestampsToUpdate;
/*!
* ignore
*/
function applyTimestampsToUpdate(now, createdAt, updatedAt, currentUpdate, options) {
const updates = currentUpdate;
let _updates = updates;
const overwrite = get(options, 'overwrite', false);
const timestamps = get(options, 'timestamps', true);
// Support skipping timestamps at the query level, see gh-6980
if (!timestamps || updates == null) {
return currentUpdate;
}
if (overwrite) {
if (currentUpdate && currentUpdate.$set) {
currentUpdate = currentUpdate.$set;
updates.$set = {};
_updates = updates.$set;
}
if (updatedAt && !currentUpdate[updatedAt]) {
_updates[updatedAt] = now;
}
if (createdAt && !currentUpdate[createdAt]) {
_updates[createdAt] = now;
}
return updates;
}
updates.$set = updates.$set || {};
currentUpdate = currentUpdate || {};
if (updatedAt &&
(!currentUpdate.$currentDate || !currentUpdate.$currentDate[updatedAt])) {
updates.$set[updatedAt] = now;
}
if (createdAt) {
if (currentUpdate[createdAt]) {
delete currentUpdate[createdAt];
}
if (currentUpdate.$set && currentUpdate.$set[createdAt]) {
delete currentUpdate.$set[createdAt];
}
updates.$setOnInsert = updates.$setOnInsert || {};
updates.$setOnInsert[createdAt] = now;
}
if (Object.keys(updates.$set).length === 0) {
delete updates.$set;
}
return updates;
}
+19 -4
View File
@@ -1,6 +1,8 @@
'use strict';
const castFilterPath = require('../query/castFilterPath');
const cleanPositionalOperators = require('../schema/cleanPositionalOperators');
const getPath = require('../schema/getPath');
const modifiedPaths = require('./modifiedPaths');
module.exports = function castArrayFilters(query) {
@@ -11,6 +13,7 @@ module.exports = function castArrayFilters(query) {
const update = query.getUpdate();
const schema = query.schema;
const strictQuery = schema.options.strictQuery;
const updatedPaths = modifiedPaths(update);
@@ -24,7 +27,9 @@ module.exports = function castArrayFilters(query) {
if (firstMatch !== path.lastIndexOf(match)) {
throw new Error(`Path '${path}' contains the same array filter multiple times`);
}
cur[match.substring(2, match.length - 1)] = path.substr(0, firstMatch - 1);
cur[match.substring(2, match.length - 1)] = path.
substr(0, firstMatch - 1).
replace(/\$\[[^\]]+\]/g, '0');
}
return cur;
}, {});
@@ -50,13 +55,23 @@ module.exports = function castArrayFilters(query) {
// If there are multiple array filters in the path being updated, make sure
// to replace them so we can get the schema path.
filterPath = filterPath.replace(/\$\[[^\]]+\]/g, '0');
filterPath = cleanPositionalOperators(filterPath);
const schematype = schema.path(filterPath);
const schematype = getPath(schema, filterPath);
if (schematype == null) {
if (!strictQuery) {
return;
}
// For now, treat `strictQuery = true` and `strictQuery = 'throw'` as
// equivalent for casting array filters. `strictQuery = true` doesn't
// quite work in this context because we never want to silently strip out
// array filters, even if the path isn't in the schema.
throw new Error(`Could not find path "${filterPath}" in schema`);
}
if (typeof filter[firstKey] === 'object') {
filter[firstKey] = castFilterPath(query, schematype, filter[firstKey]);
} else {
filter[firstKey] = schematype.castForQuery(filter[firstKey]);
}
}
};
};
+33
View File
@@ -0,0 +1,33 @@
'use strict';
const _modifiedPaths = require('../common').modifiedPaths;
/**
* Given an update document with potential update operators (`$set`, etc.)
* returns an object whose keys are the directly modified paths.
*
* If there are any top-level keys that don't start with `$`, we assume those
* will get wrapped in a `$set`. The Mongoose Query is responsible for wrapping
* top-level keys in `$set`.
*
* @param {Object} update
* @return {Object} modified
*/
module.exports = function modifiedPaths(update) {
const keys = Object.keys(update);
const res = {};
const withoutDollarKeys = {};
for (const key of keys) {
if (key.startsWith('$')) {
_modifiedPaths(update[key], '', res);
continue;
}
withoutDollarKeys[key] = update[key];
}
_modifiedPaths(withoutDollarKeys, '', res);
return res;
};
+228
View File
@@ -0,0 +1,228 @@
'use strict';
/*!
* Module dependencies.
*/
const Mixed = require('../schema/mixed');
const ValidationError = require('../error/validation');
const cleanPositionalOperators = require('./schema/cleanPositionalOperators');
const flatten = require('./common').flatten;
const modifiedPaths = require('./common').modifiedPaths;
const parallel = require('async/parallel');
/**
* Applies validators and defaults to update and findOneAndUpdate operations,
* specifically passing a null doc as `this` to validators and defaults
*
* @param {Query} query
* @param {Schema} schema
* @param {Object} castedDoc
* @param {Object} options
* @method runValidatorsOnUpdate
* @api private
*/
module.exports = function(query, schema, castedDoc, options) {
let _keys;
const keys = Object.keys(castedDoc || {});
let updatedKeys = {};
let updatedValues = {};
const isPull = {};
const arrayAtomicUpdates = {};
const numKeys = keys.length;
let hasDollarUpdate = false;
const modified = {};
let currentUpdate;
let key;
let i;
for (i = 0; i < numKeys; ++i) {
if (keys[i].charAt(0) === '$') {
hasDollarUpdate = true;
if (keys[i] === '$push' || keys[i] === '$addToSet') {
_keys = Object.keys(castedDoc[keys[i]]);
for (let ii = 0; ii < _keys.length; ++ii) {
currentUpdate = castedDoc[keys[i]][_keys[ii]];
if (currentUpdate && currentUpdate.$each) {
arrayAtomicUpdates[_keys[ii]] = (arrayAtomicUpdates[_keys[ii]] || []).
concat(currentUpdate.$each);
} else {
arrayAtomicUpdates[_keys[ii]] = (arrayAtomicUpdates[_keys[ii]] || []).
concat([currentUpdate]);
}
}
continue;
}
modifiedPaths(castedDoc[keys[i]], '', modified);
const flat = flatten(castedDoc[keys[i]]);
const paths = Object.keys(flat);
const numPaths = paths.length;
for (let j = 0; j < numPaths; ++j) {
const updatedPath = cleanPositionalOperators(paths[j]);
key = keys[i];
// With `$pull` we might flatten `$in`. Skip stuff nested under `$in`
// for the rest of the logic, it will get handled later.
if (updatedPath.indexOf('$') !== -1) {
continue;
}
if (key === '$set' || key === '$setOnInsert' ||
key === '$pull' || key === '$pullAll') {
updatedValues[updatedPath] = flat[paths[j]];
isPull[updatedPath] = key === '$pull' || key === '$pullAll';
} else if (key === '$unset') {
updatedValues[updatedPath] = undefined;
}
updatedKeys[updatedPath] = true;
}
}
}
if (!hasDollarUpdate) {
modifiedPaths(castedDoc, '', modified);
updatedValues = flatten(castedDoc);
updatedKeys = Object.keys(updatedValues);
}
const updates = Object.keys(updatedValues);
const numUpdates = updates.length;
const validatorsToExecute = [];
const validationErrors = [];
const alreadyValidated = [];
const context = options && options.context === 'query' ? query : null;
function iter(i, v) {
const schemaPath = schema._getSchema(updates[i]);
if (schemaPath == null) {
return;
}
// gh-4305: `_getSchema()` will report all sub-fields of a 'Mixed' path
// as 'Mixed', so avoid double validating them.
if (schemaPath instanceof Mixed && schemaPath.$fullPath !== updates[i]) {
return;
}
if (v && Array.isArray(v.$in)) {
v.$in.forEach((v, i) => {
validatorsToExecute.push(function(callback) {
schemaPath.doValidate(
v,
function(err) {
if (err) {
err.path = updates[i] + '.$in.' + i;
validationErrors.push(err);
}
callback(null);
},
context,
{updateValidator: true});
});
});
} else {
if (isPull[updates[i]] &&
schemaPath.$isMongooseArray) {
return;
}
if (schemaPath.$isMongooseDocumentArrayElement && v != null && v.$__ != null) {
alreadyValidated.push(updates[i]);
validatorsToExecute.push(function(callback) {
schemaPath.doValidate(v, function(err) {
if (err) {
err.path = updates[i];
validationErrors.push(err);
return callback(null);
}
v.validate(function(err) {
if (err) {
if (err.errors) {
for (const key of Object.keys(err.errors)) {
const _err = err.errors[key];
_err.path = updates[i] + '.' + key;
validationErrors.push(_err);
}
}
}
callback(null);
});
}, context, { updateValidator: true });
});
} else {
validatorsToExecute.push(function(callback) {
for (const path of alreadyValidated) {
if (updates[i].startsWith(path + '.')) {
return callback(null);
}
}
schemaPath.doValidate(v, function(err) {
if (err) {
err.path = updates[i];
validationErrors.push(err);
}
callback(null);
}, context, { updateValidator: true });
});
}
}
}
for (i = 0; i < numUpdates; ++i) {
iter(i, updatedValues[updates[i]]);
}
const arrayUpdates = Object.keys(arrayAtomicUpdates);
const numArrayUpdates = arrayUpdates.length;
for (i = 0; i < numArrayUpdates; ++i) {
(function(i) {
let schemaPath = schema._getSchema(arrayUpdates[i]);
if (schemaPath && schemaPath.$isMongooseDocumentArray) {
validatorsToExecute.push(function(callback) {
schemaPath.doValidate(
arrayAtomicUpdates[arrayUpdates[i]],
function(err) {
if (err) {
err.path = arrayUpdates[i];
validationErrors.push(err);
}
callback(null);
},
options && options.context === 'query' ? query : null);
});
} else {
schemaPath = schema._getSchema(arrayUpdates[i] + '.0');
for (let j = 0; j < arrayAtomicUpdates[arrayUpdates[i]].length; ++j) {
(function(j) {
validatorsToExecute.push(function(callback) {
schemaPath.doValidate(
arrayAtomicUpdates[arrayUpdates[i]][j],
function(err) {
if (err) {
err.path = arrayUpdates[i];
validationErrors.push(err);
}
callback(null);
},
options && options.context === 'query' ? query : null,
{ updateValidator: true });
});
})(j);
}
}
})(i);
}
return function(callback) {
parallel(validatorsToExecute, function() {
if (validationErrors.length) {
const err = new ValidationError(null);
for (let i = 0; i < validationErrors.length; ++i) {
err.addError(validationErrors[i].path, validationErrors[i]);
}
return callback(err);
}
callback(null);
});
};
};