This commit is contained in:
Jonasz Bigda
2023-03-25 21:51:42 +01:00
parent 0db1d5117e
commit b332e9ceb0
1044 changed files with 37502 additions and 63938 deletions

View File

@@ -69,7 +69,12 @@ function assignRawDocsToIdStructure(rawIds, resultDocs, resultOrder, options, re
if (recursed) {
if (doc) {
if (sorting) {
newOrder[resultOrder[sid]] = doc;
const _resultOrder = resultOrder[sid];
if (Array.isArray(_resultOrder) && Array.isArray(doc) && _resultOrder.length === doc.length) {
newOrder.push(doc);
} else {
newOrder[_resultOrder] = doc;
}
} else {
newOrder.push(doc);
}

View File

@@ -5,24 +5,27 @@ const assignRawDocsToIdStructure = require('./assignRawDocsToIdStructure');
const get = require('../get');
const getVirtual = require('./getVirtual');
const leanPopulateMap = require('./leanPopulateMap');
const lookupLocalFields = require('./lookupLocalFields');
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');
const userOptions = Object.assign({}, get(o, 'allOptions.options.options'), get(o, 'allOptions.options'));
// `o.options` contains options explicitly listed in `populateOptions`, like
// `match` and `limit`.
const populateOptions = Object.assign({}, o.options, userOptions, {
justOne: o.justOne
});
populateOptions.$nullIfNotFound = o.isVirtual;
const populatedModel = o.populatedModel;
const originalIds = [].concat(o.rawIds);
// replace the original ids in our intermediate _ids structure
// with the documents found by query
o.allIds = [].concat(o.allIds);
assignRawDocsToIdStructure(o.rawIds, o.rawDocs, o.rawOrder, populateOptions);
// now update the original documents being populated using the
@@ -31,6 +34,7 @@ module.exports = function assignVals(o) {
const rawIds = o.rawIds;
const options = o.options;
const count = o.count && o.isVirtual;
let i;
function setValue(val) {
if (count) {
@@ -39,17 +43,42 @@ module.exports = function assignVals(o) {
if (val instanceof SkipPopulateValue) {
return val.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);
if (val === void 0) {
return val;
}
return valueFilter(val, options, populateOptions);
const _allIds = o.allIds[i];
if (o.justOne === true && Array.isArray(val)) {
// Might be an embedded discriminator (re: gh-9244) with multiple models, so make sure to pick the right
// model before assigning.
const ret = [];
for (const doc of val) {
const _docPopulatedModel = leanPopulateMap.get(doc);
if (_docPopulatedModel == null || _docPopulatedModel === populatedModel) {
ret.push(doc);
}
}
// 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 valueFilter(val[0], options, populateOptions, _allIds);
} else if (o.justOne === false && !Array.isArray(val)) {
return valueFilter([val], options, populateOptions, _allIds);
}
return valueFilter(val, options, populateOptions, _allIds);
}
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)) {
for (i = 0; i < docs.length; ++i) {
const _path = o.path.endsWith('.$*') ? o.path.slice(0, -3) : o.path;
const existingVal = mpath.get(_path, docs[i], lookupLocalFields);
if (existingVal == null && !getVirtual(o.originalModel.schema, _path)) {
continue;
}
@@ -58,8 +87,8 @@ module.exports = function assignVals(o) {
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];
rawIds[i].filter(sift(o.match[i])) :
[rawIds[i]].filter(sift(o.match[i]))[0];
} else {
valueToSet = rawIds[i];
}
@@ -73,7 +102,7 @@ module.exports = function assignVals(o) {
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');
isMap = isMap && get(originalSchema._getSchema(_path), '$isSchemaMap');
if (!o.isVirtual && isMap) {
const _keys = existingVal instanceof Map ?
Array.from(existingVal.keys()) :
@@ -84,15 +113,25 @@ module.exports = function assignVals(o) {
}, new Map());
}
if (isDoc && Array.isArray(valueToSet)) {
for (const val of valueToSet) {
if (val != null && val.$__ != null) {
val.$__.parent = docs[i];
}
}
} else if (isDoc && valueToSet != null && valueToSet.$__ != null) {
valueToSet.$__.parent = docs[i];
}
if (o.isVirtual && isDoc) {
docs[i].populated(o.path, o.justOne ? originalIds[0] : originalIds, o.allOptions);
docs[i].populated(_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);
mpath.set(_path, valueToSet, docs[i], setValue);
continue;
}
const parts = o.path.split('.');
const parts = _path.split('.');
let cur = docs[i];
const curPath = parts[0];
for (let j = 0; j < parts.length - 1; ++j) {
@@ -102,13 +141,17 @@ module.exports = function assignVals(o) {
break;
}
if (parts[j] === '$*') {
break;
}
if (cur[parts[j]] == null) {
// If nothing to set, avoid creating an unnecessary array. Otherwise
// we'll end up with a single doc in the array with only defaults.
// See gh-8342, gh-8455
const schematype = originalSchema._getSchema(curPath);
if (valueToSet == null && schematype != null && schematype.$isMongooseArray) {
return;
break;
}
cur[parts[j]] = {};
}
@@ -116,17 +159,17 @@ module.exports = function assignVals(o) {
// 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;
break;
}
}
if (docs[i].$__) {
docs[i].populated(o.path, o.allIds[i], o.allOptions);
docs[i].populated(_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);
mpath.set(_path, valueToSet, docs[i], lookupLocalFields, setValue, false);
}
};
@@ -160,15 +203,20 @@ function numDocs(v) {
* that population mapping can occur.
*/
function valueFilter(val, assignmentOpts, populateOptions) {
function valueFilter(val, assignmentOpts, populateOptions, allIds) {
const userSpecifiedTransform = typeof populateOptions.transform === 'function';
const transform = userSpecifiedTransform ? populateOptions.transform : noop;
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)) {
let subdoc = val[i];
const _allIds = Array.isArray(allIds) ? allIds[i] : allIds;
if (!isPopulatedObject(subdoc) && (!populateOptions.retainNullValues || subdoc != null) && !userSpecifiedTransform) {
continue;
} else if (userSpecifiedTransform) {
subdoc = transform(isPopulatedObject(subdoc) ? subdoc : null, _allIds);
}
maybeRemoveId(subdoc, assignmentOpts);
ret.push(subdoc);
@@ -190,22 +238,19 @@ function valueFilter(val, assignmentOpts, populateOptions) {
}
// findOne
if (isPopulatedObject(val)) {
if (isPopulatedObject(val) || utils.isPOJO(val)) {
maybeRemoveId(val, assignmentOpts);
return val;
return transform(val, allIds);
}
if (val instanceof Map) {
return val;
}
if (populateOptions.justOne === true) {
return (val == null ? val : null);
}
if (populateOptions.justOne === false) {
return [];
}
return val;
return val == null ? transform(val, allIds) : transform(null, allIds);
}
/*!
@@ -213,7 +258,7 @@ function valueFilter(val, assignmentOpts, populateOptions) {
*/
function maybeRemoveId(subdoc, assignmentOpts) {
if (assignmentOpts.excludeId) {
if (subdoc != null && assignmentOpts.excludeId) {
if (typeof subdoc.$__setValue === 'function') {
delete subdoc._doc._id;
} else {
@@ -236,4 +281,8 @@ function isPopulatedObject(obj) {
obj.$isMongooseMap ||
obj.$__ != null ||
leanPopulateMap.has(obj);
}
function noop(v) {
return v;
}

View File

@@ -5,8 +5,11 @@ const SkipPopulateValue = require('./SkipPopulateValue');
const get = require('../get');
const getDiscriminatorByValue = require('../discriminator/getDiscriminatorByValue');
const isPathExcluded = require('../projection/isPathExcluded');
const getConstructorName = require('../getConstructorName');
const getSchemaTypes = require('./getSchemaTypes');
const getVirtual = require('./getVirtual');
const lookupLocalFields = require('./lookupLocalFields');
const mpath = require('mpath');
const normalizeRefPath = require('./normalizeRefPath');
const util = require('util');
const utils = require('../../utils');
@@ -28,7 +31,6 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
let currentOptions;
let modelNames;
let modelName;
let modelForFindSchema;
const originalModel = options.model;
let isVirtual = false;
@@ -40,7 +42,7 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
for (i = 0; i < len; i++) {
doc = docs[i];
let justOne = null;
schema = getSchemaTypes(modelSchema, doc, options.path);
// Special case: populating a path that's a DocumentArray unless
// there's an explicit `ref` or `refPath` re: gh-8946
@@ -52,7 +54,7 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
}
// Populating a nested path should always be a no-op re: #9073.
// People shouldn't do this, but apparently they do.
if (modelSchema.nested[options.path]) {
if (options._localModel != null && options._localModel.schema.nested[options.path]) {
continue;
}
const isUnderneathDocArray = schema && schema.$isUnderneathDocArray;
@@ -64,6 +66,7 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
modelNames = null;
let isRefPath = !!_firstWithRefPath;
let normalizedRefPath = _firstWithRefPath ? get(_firstWithRefPath, 'options.refPath', null) : null;
let schemaOptions = null;
if (Array.isArray(schema)) {
const schemasArray = schema;
@@ -76,6 +79,7 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
isRefPath = isRefPath || res.isRefPath;
normalizedRefPath = normalizeRefPath(normalizedRefPath, doc, options.path) ||
res.refPath;
justOne = res.justOne;
} catch (error) {
return error;
}
@@ -99,6 +103,8 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
modelNames = res.modelNames;
isRefPath = res.isRefPath;
normalizedRefPath = res.refPath;
justOne = res.justOne;
schemaOptions = get(schema, 'options.populate', null);
} catch (error) {
return error;
}
@@ -118,6 +124,8 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
_virtualRes.nestedSchemaPath + '.' : '';
if (typeof virtual.options.localField === 'function') {
localField = virtualPrefix + virtual.options.localField.call(doc, doc);
} else if (Array.isArray(virtual.options.localField)) {
localField = virtual.options.localField.map(field => virtualPrefix + field);
} else {
localField = virtualPrefix + virtual.options.localField;
}
@@ -142,7 +150,6 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
// `justOne = null` means we don't know from the schema whether the end
// result should be an array or a single doc. This can result from
// populating a POJO using `Model.populate()`
let justOne = null;
if ('justOne' in options && options.justOne !== void 0) {
justOne = options.justOne;
} else if (virtual && virtual.options && virtual.options.refPath) {
@@ -166,9 +173,11 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
}
} else if (schema && !schema[schemaMixedSymbol]) {
// Skip Mixed types because we explicitly don't do casting on those.
justOne = Array.isArray(schema) ?
schema.every(schema => !schema.$isMongooseArray) :
!schema.$isMongooseArray;
if (options.path.endsWith('.' + schema.path)) {
justOne = Array.isArray(schema) ?
schema.every(schema => !schema.$isMongooseArray) :
!schema.$isMongooseArray;
}
}
if (!modelNames) {
@@ -189,6 +198,27 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
foreignField = foreignField.call(doc);
}
let match = get(options, 'match', null) ||
get(currentOptions, 'match', null) ||
get(options, 'virtual.options.match', null) ||
get(options, 'virtual.options.options.match', null);
let hasMatchFunction = typeof match === 'function';
if (hasMatchFunction) {
match = match.call(doc, doc);
}
if (Array.isArray(localField) && Array.isArray(foreignField) && localField.length === foreignField.length) {
match = Object.assign({}, match);
for (let i = 1; i < localField.length; ++i) {
match[foreignField[i]] = convertTo_id(mpath.get(localField[i], doc, lookupLocalFields), schema);
hasMatchFunction = true;
}
localField = localField[0];
foreignField = foreignField[0];
}
const localFieldPathType = modelSchema._getPathType(localField);
const localFieldPath = localFieldPathType === 'real' ? modelSchema.path(localField) : localFieldPathType.schema;
const localFieldGetters = localFieldPath && localFieldPath.getters ? localFieldPath.getters : [];
@@ -201,31 +231,21 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
options.isVirtual && get(virtual, 'options.getters', false);
if (localFieldGetters.length > 0 && getters) {
const hydratedDoc = (doc.$__ != null) ? doc : model.hydrate(doc);
const localFieldValue = utils.getValue(localField, doc);
const localFieldValue = mpath.get(localField, doc, lookupLocalFields);
if (Array.isArray(localFieldValue)) {
const localFieldHydratedValue = utils.getValue(localField.split('.').slice(0, -1), hydratedDoc);
const localFieldHydratedValue = mpath.get(localField.split('.').slice(0, -1), hydratedDoc, lookupLocalFields);
ret = localFieldValue.map((localFieldArrVal, localFieldArrIndex) =>
localFieldPath.applyGetters(localFieldArrVal, localFieldHydratedValue[localFieldArrIndex]));
} else {
ret = localFieldPath.applyGetters(localFieldValue, hydratedDoc);
}
} else {
ret = convertTo_id(utils.getValue(localField, doc), schema);
ret = convertTo_id(mpath.get(localField, doc, lookupLocalFields), schema);
}
const id = String(utils.getValue(foreignField, doc));
options._docs[id] = Array.isArray(ret) ? ret.slice() : ret;
let match = get(options, 'match', null) ||
get(currentOptions, 'match', null) ||
get(options, 'virtual.options.match', null) ||
get(options, 'virtual.options.options.match', null);
const hasMatchFunction = typeof match === 'function';
if (hasMatchFunction) {
match = match.call(doc, doc);
}
// Re: gh-8452. Embedded discriminators may not have `refPath`, so clear
// out embedded discriminator docs that don't have a `refPath` on the
// populated path.
@@ -280,7 +300,12 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
originalModel :
modelName[modelSymbol] ? modelName : connection.model(modelName);
} catch (error) {
return error;
// If `ret` is undefined, we'll add an empty entry to modelsMap. We shouldn't
// execute a query, but it is necessary to make sure `justOne` gets handled
// correctly for setting an empty array (see gh-8455)
if (ret !== undefined) {
return error;
}
}
let ids = ret;
@@ -290,13 +315,15 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
ids = flat.filter((val, i) => modelNames[i] === modelName);
}
if (!available[modelName] || currentOptions.perDocumentLimit != null) {
if (!available[modelName] || currentOptions.perDocumentLimit != null || get(currentOptions, 'options.perDocumentLimit') != null) {
currentOptions = {
model: Model
};
if (isVirtual && get(virtual, 'options.options')) {
currentOptions.options = utils.clone(virtual.options.options);
} else if (schemaOptions != null) {
currentOptions.options = Object.assign({}, schemaOptions);
}
utils.merge(currentOptions, options);
@@ -332,13 +359,13 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
}
}
}
return map;
function _getModelNames(doc, schema) {
let modelNames;
let discriminatorKey;
let isRefPath = false;
let justOne = null;
if (schema && schema.caster) {
schema = schema.caster;
@@ -378,28 +405,26 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
} else {
let modelForCurrentDoc = model;
let schemaForCurrentDoc;
let discriminatorValue;
if (!schema && discriminatorKey) {
modelForFindSchema = utils.getValue(discriminatorKey, doc);
if (modelForFindSchema) {
// `modelForFindSchema` is the discriminator value, so we might need
// find the discriminated model name
const discriminatorModel = getDiscriminatorByValue(model, modelForFindSchema);
if (discriminatorModel != null) {
modelForCurrentDoc = discriminatorModel;
} else {
try {
modelForCurrentDoc = model.db.model(modelForFindSchema);
} catch (error) {
return error;
}
if (!schema && discriminatorKey && (discriminatorValue = utils.getValue(discriminatorKey, doc))) {
// `modelNameForFind` is the discriminator value, so we might need
// find the discriminated model name
const discriminatorModel = getDiscriminatorByValue(model.discriminators, discriminatorValue) || model;
if (discriminatorModel != null) {
modelForCurrentDoc = discriminatorModel;
} else {
try {
modelForCurrentDoc = model.db.model(discriminatorValue);
} catch (error) {
return error;
}
}
schemaForCurrentDoc = modelForCurrentDoc.schema._getSchema(options.path);
schemaForCurrentDoc = modelForCurrentDoc.schema._getSchema(options.path);
if (schemaForCurrentDoc && schemaForCurrentDoc.caster) {
schemaForCurrentDoc = schemaForCurrentDoc.caster;
}
if (schemaForCurrentDoc && schemaForCurrentDoc.caster) {
schemaForCurrentDoc = schemaForCurrentDoc.caster;
}
} else {
schemaForCurrentDoc = schema;
@@ -407,6 +432,10 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
const _virtualRes = getVirtual(modelForCurrentDoc.schema, options.path);
const virtual = _virtualRes == null ? null : _virtualRes.virtual;
if (schemaForCurrentDoc != null) {
justOne = !schemaForCurrentDoc.$isMongooseArray && !schemaForCurrentDoc._arrayPath;
}
let ref;
let refPath;
@@ -440,14 +469,14 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
}
if (!modelNames) {
return { modelNames: modelNames, isRefPath: isRefPath, refPath: normalizedRefPath };
return { modelNames: modelNames, isRefPath: isRefPath, refPath: normalizedRefPath, justOne: justOne };
}
if (!Array.isArray(modelNames)) {
modelNames = [modelNames];
}
return { modelNames: modelNames, isRefPath: isRefPath, refPath: normalizedRefPath };
return { modelNames: modelNames, isRefPath: isRefPath, refPath: normalizedRefPath, justOne: justOne };
}
};
@@ -470,7 +499,12 @@ function handleRefFunction(ref, doc) {
*/
function convertTo_id(val, schema) {
if (val != null && val.$__ != null) return val._id;
if (val != null && val.$__ != null) {
return val._id;
}
if (val != null && val._id != null && (schema == null || !schema.$isSchemaMap)) {
return val._id;
}
if (Array.isArray(val)) {
for (let i = 0; i < val.length; ++i) {
@@ -479,7 +513,7 @@ function convertTo_id(val, schema) {
}
}
if (val.isMongooseArray && val.$schema()) {
return val.$schema().cast(val, val.$parent());
return val.$schema()._castForPopulate(val, val.$parent());
}
return [].concat(val);
@@ -487,8 +521,7 @@ function convertTo_id(val, schema) {
// `populate('map')` may be an object if populating on a doc that hasn't
// been hydrated yet
if (val != null &&
val.constructor.name === 'Object' &&
if (getConstructorName(val) === 'Object' &&
// The intent here is we should only flatten the object if we expect
// to get a Map in the end. Avoid doing this for mixed types.
(schema == null || schema[schemaMixedSymbol] == null)) {

View File

@@ -20,7 +20,6 @@ const populateModelSymbol = require('../symbols').populateModelSymbol;
module.exports = function getSchemaTypes(schema, doc, path) {
const pathschema = schema.path(path);
const topLevelDoc = doc;
if (pathschema) {
return pathschema;
}
@@ -33,7 +32,6 @@ module.exports = function getSchemaTypes(schema, doc, path) {
while (p--) {
trypath = parts.slice(0, p).join('.');
foundschema = schema.path(trypath);
if (foundschema == null) {
continue;
}
@@ -52,7 +50,8 @@ module.exports = function getSchemaTypes(schema, doc, path) {
const keys = subdoc ? mpath.get(discriminatorKeyPath, subdoc) || [] : [];
schemas = Object.keys(discriminators).
reduce(function(cur, discriminator) {
if (doc == null || keys.indexOf(discriminator) !== -1) {
const tiedValue = discriminators[discriminator].discriminatorMapping.value;
if (doc == null || keys.indexOf(discriminator) !== -1 || keys.indexOf(tiedValue) !== -1) {
cur.push(discriminators[discriminator]);
}
return cur;
@@ -117,24 +116,40 @@ module.exports = function getSchemaTypes(schema, doc, path) {
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(
const ret = search(
parts.slice(p),
type.schema,
null,
nestedPath.concat(parts.slice(0, p))
);
if (ret != null) {
return ret;
}
if (type.schema.discriminators) {
const discriminatorPaths = [];
for (const discriminatorName of Object.keys(type.schema.discriminators)) {
const _schema = type.schema.discriminators[discriminatorName] || type.schema;
const ret = search(parts.slice(p), _schema, null, nestedPath.concat(parts.slice(0, p)));
if (ret != null) {
discriminatorPaths.push(ret);
}
}
if (discriminatorPaths.length > 0) {
return discriminatorPaths;
}
}
}
}
@@ -153,7 +168,6 @@ module.exports = function getSchemaTypes(schema, doc, path) {
ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
!model.schema.$isSingleNested;
}
return ret;
}
}
@@ -173,19 +187,16 @@ module.exports = function getSchemaTypes(schema, doc, path) {
nestedPath.concat(parts.slice(0, p))
);
if (ret) {
if (ret != null) {
ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
!schema.$isSingleNested;
return ret;
}
return ret;
}
}
return foundschema;
}
}
// look for arrays
const parts = path.split('.');
for (let i = 0; i < parts.length; ++i) {