-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(populate): move
getModelsMapForPopulate()
into separate fu…
…nction Fix #7962
- Loading branch information
Showing
3 changed files
with
406 additions
and
401 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.