Skip to content

Commit

Permalink
refactor(populate): move getModelsMapForPopulate() into separate fu…
Browse files Browse the repository at this point in the history
…nction

Fix #7962
  • Loading branch information
vkarpov15 committed Jul 5, 2019
1 parent 2b7622b commit e96b6bd
Show file tree
Hide file tree
Showing 3 changed files with 406 additions and 401 deletions.
398 changes: 398 additions & 0 deletions lib/helpers/populate/getModelsMapForPopulate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,398 @@
'use strict';

const MongooseError = require('../../error/index');
const get = require('../get');
const isPathExcluded = require('../projection/isPathExcluded');
const getSchemaTypes = require('./getSchemaTypes');
const getVirtual = require('./getVirtual');
const normalizeRefPath = require('./normalizeRefPath');
const util = require('util');
const utils = require('../../utils');

const modelSymbol = require('../symbols').modelSymbol;
const populateModelSymbol = require('../symbols').populateModelSymbol;
const schemaMixedSymbol = require('../../schema/symbols').schemaMixedSymbol;

module.exports = function getModelsMapForPopulate(model, docs, options) {
let i;
let doc;
const len = docs.length;
const available = {};
const map = [];
const modelNameFromQuery = options.model && options.model.modelName || options.model;
let schema;
let refPath;
let Model;
let currentOptions;
let modelNames;
let modelName;
let modelForFindSchema;

const originalModel = options.model;
let isVirtual = false;
const modelSchema = model.schema;

for (i = 0; i < len; i++) {
doc = docs[i];

schema = getSchemaTypes(modelSchema, doc, options.path);
const isUnderneathDocArray = schema && schema.$isUnderneathDocArray;
if (isUnderneathDocArray && get(options, 'options.sort') != null) {
return new MongooseError('Cannot populate with `sort` on path ' + options.path +
' because it is a subproperty of a document array');
}

modelNames = null;
let isRefPath = false;
if (Array.isArray(schema)) {
for (let j = 0; j < schema.length; ++j) {
let _modelNames;
try {
const res = _getModelNames(doc, schema[j]);
_modelNames = res.modelNames;
isRefPath = res.isRefPath;
} catch (error) {
return error;
}
if (!_modelNames) {
continue;
}
modelNames = modelNames || [];
for (let x = 0; x < _modelNames.length; ++x) {
if (modelNames.indexOf(_modelNames[x]) === -1) {
modelNames.push(_modelNames[x]);
}
}
}
} else {
try {
const res = _getModelNames(doc, schema);
modelNames = res.modelNames;
isRefPath = res.isRefPath;
} catch (error) {
return error;
}

if (!modelNames) {
continue;
}
}

const virtual = getVirtual(model.schema, options.path);
let localField;
let count = false;
if (virtual && virtual.options) {
const virtualPrefix = virtual.$nestedSchemaPath ?
virtual.$nestedSchemaPath + '.' : '';
if (typeof virtual.options.localField === 'function') {
localField = virtualPrefix + virtual.options.localField.call(doc, doc);
} else {
localField = virtualPrefix + virtual.options.localField;
}
count = virtual.options.count;
} else {
localField = options.path;
}
let foreignField = virtual && virtual.options ?
virtual.options.foreignField :
'_id';

// `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) {
justOne = options.justOne;
} else if (virtual && virtual.options && virtual.options.refPath) {
const normalizedRefPath =
normalizeRefPath(virtual.options.refPath, doc, options.path);
justOne = !!virtual.options.justOne;
isVirtual = true;
const refValue = utils.getValue(normalizedRefPath, doc);
modelNames = Array.isArray(refValue) ? refValue : [refValue];
} else if (virtual && virtual.options && virtual.options.ref) {
let normalizedRef;
if (typeof virtual.options.ref === 'function') {
normalizedRef = virtual.options.ref.call(doc, doc);
} else {
normalizedRef = virtual.options.ref;
}
justOne = !!virtual.options.justOne;
isVirtual = true;
if (!modelNames) {
modelNames = [].concat(normalizedRef);
}
} else if (schema && !schema[schemaMixedSymbol]) {
// Skip Mixed types because we explicitly don't do casting on those.
justOne = !schema.$isMongooseArray;
}

if (!modelNames) {
continue;
}

if (virtual && (!localField || !foreignField)) {
return new MongooseError('If you are populating a virtual, you must set the ' +
'localField and foreignField options');
}

options.isVirtual = isVirtual;
options.virtual = virtual;
if (typeof localField === 'function') {
localField = localField.call(doc, doc);
}
if (typeof foreignField === 'function') {
foreignField = foreignField.call(doc);
}

const localFieldPathType = modelSchema._getPathType(localField);
const localFieldPath = localFieldPathType === 'real' ? modelSchema.paths[localField] : localFieldPathType.schema;
const localFieldGetters = localFieldPath && localFieldPath.getters ? localFieldPath.getters : [];
let ret;

const _populateOptions = get(options, 'options', {});

const getters = 'getters' in _populateOptions ?
_populateOptions.getters :
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);
if (Array.isArray(localFieldValue)) {
const localFieldHydratedValue = utils.getValue(localField.split('.').slice(0, -1), hydratedDoc);
ret = localFieldValue.map((localFieldArrVal, localFieldArrIndex) =>
localFieldPath.applyGetters(localFieldArrVal, localFieldHydratedValue[localFieldArrIndex]));
} else {
ret = localFieldPath.applyGetters(localFieldValue, hydratedDoc);
}
} else {
ret = convertTo_id(utils.getValue(localField, doc));
}

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.options.match', null);

const hasMatchFunction = typeof match === 'function';
if (hasMatchFunction) {
match = match.call(doc, doc);
}

let k = modelNames.length;
while (k--) {
modelName = modelNames[k];
if (modelName == null) {
continue;
}

// `PopulateOptions#connection`: if the model is passed as a string, the
// connection matters because different connections have different models.
const connection = options.connection != null ? options.connection : model.db;

try {
Model = originalModel && originalModel[modelSymbol] ?
originalModel :
modelName[modelSymbol] ? modelName : connection.model(modelName);
} catch (error) {
return error;
}

let ids = ret;
const flat = Array.isArray(ret) ? utils.array.flatten(ret) : [];
if (isRefPath && Array.isArray(ret) && flat.length === modelNames.length) {
ids = flat.filter((val, i) => modelNames[i] === modelName);
}

if (!available[modelName]) {
currentOptions = {
model: Model
};

if (isVirtual && virtual.options && virtual.options.options) {
currentOptions.options = utils.clone(virtual.options.options);
}
utils.merge(currentOptions, options);

// Used internally for checking what model was used to populate this
// path.
options[populateModelSymbol] = Model;

available[modelName] = {
model: Model,
options: currentOptions,
match: hasMatchFunction ? [match] : match,
docs: [doc],
ids: [ids],
allIds: [ret],
localField: new Set([localField]),
foreignField: new Set([foreignField]),
justOne: justOne,
isVirtual: isVirtual,
virtual: virtual,
count: count,
[populateModelSymbol]: Model
};
map.push(available[modelName]);
} else {
available[modelName].localField.add(localField);
available[modelName].foreignField.add(foreignField);
available[modelName].docs.push(doc);
available[modelName].ids.push(ids);
available[modelName].allIds.push(ret);
if (hasMatchFunction) {
available[modelName].match.push(match);
}
}
}
}

function _getModelNames(doc, schema) {
let modelNames;
let discriminatorKey;
let isRefPath = false;

if (schema && schema.caster) {
schema = schema.caster;
}
if (schema && schema.$isSchemaMap) {
schema = schema.$__schemaType;
}

if (!schema && model.discriminators) {
discriminatorKey = model.schema.discriminatorMapping.key;
}

refPath = schema && schema.options && schema.options.refPath;

const normalizedRefPath = normalizeRefPath(refPath, doc, options.path);

if (modelNameFromQuery) {
modelNames = [modelNameFromQuery]; // query options
} else if (normalizedRefPath) {
if (options._queryProjection != null && isPathExcluded(options._queryProjection, normalizedRefPath)) {
throw new MongooseError('refPath `' + normalizedRefPath +
'` must not be excluded in projection, got ' +
util.inspect(options._queryProjection));
}
modelNames = utils.getValue(normalizedRefPath, doc);
if (Array.isArray(modelNames)) {
modelNames = utils.array.flatten(modelNames);
}

isRefPath = true;
} else {
let modelForCurrentDoc = model;
let schemaForCurrentDoc;

if (!schema && discriminatorKey) {
modelForFindSchema = utils.getValue(discriminatorKey, doc);

if (modelForFindSchema) {
try {
modelForCurrentDoc = model.db.model(modelForFindSchema);
} catch (error) {
return error;
}

schemaForCurrentDoc = modelForCurrentDoc.schema._getSchema(options.path);

if (schemaForCurrentDoc && schemaForCurrentDoc.caster) {
schemaForCurrentDoc = schemaForCurrentDoc.caster;
}
}
} else {
schemaForCurrentDoc = schema;
}
const virtual = getVirtual(modelForCurrentDoc.schema, options.path);

let ref;
if ((ref = get(schemaForCurrentDoc, 'options.ref')) != null) {
ref = handleRefFunction(ref, doc);
modelNames = [ref];
} else if ((ref = get(virtual, 'options.ref')) != null) {
ref = handleRefFunction(ref, doc);

// When referencing nested arrays, the ref should be an Array
// of modelNames.
if (Array.isArray(ref)) {
modelNames = ref;
} else {
modelNames = [ref];
}

isVirtual = true;
} else {
// We may have a discriminator, in which case we don't want to
// populate using the base model by default
modelNames = discriminatorKey ? null : [model.modelName];
}
}

if (!modelNames) {
return { modelNames: modelNames, isRefPath: isRefPath };
}

if (!Array.isArray(modelNames)) {
modelNames = [modelNames];
}

return { modelNames: modelNames, isRefPath: isRefPath };
}

return map;
};

/*!
* ignore
*/

function handleRefFunction(ref, doc) {
if (typeof ref === 'function' && !ref[modelSymbol]) {
return ref.call(doc, doc);
}
return ref;
}

/*!
* Retrieve the _id of `val` if a Document or Array of Documents.
*
* @param {Array|Document|Any} val
* @return {Array|Document|Any}
*/

function convertTo_id(val) {
if (val != null && val.$__ != null) return val._id;

if (Array.isArray(val)) {
for (let i = 0; i < val.length; ++i) {
if (val[i] != null && val[i].$__ != null) {
val[i] = val[i]._id;
}
}
if (val.isMongooseArray && val.$schema()) {
return val.$schema().cast(val, val.$parent());
}

return [].concat(val);
}

// `populate('map')` may be an object if populating on a doc that hasn't
// been hydrated yet
if (val != null && val.constructor.name === 'Object') {
const ret = [];
for (const key of Object.keys(val)) {
ret.push(val[key]);
}
return ret;
}
// If doc has already been hydrated, e.g. `doc.populate('map').execPopulate()`
// then `val` will already be a map
if (val instanceof Map) {
return Array.from(val.values());
}

return val;
}
Loading

0 comments on commit e96b6bd

Please sign in to comment.