diff options
Diffstat (limited to 'node_modules/mongoose/lib/helpers/populate')
11 files changed, 1302 insertions, 0 deletions
diff --git a/node_modules/mongoose/lib/helpers/populate/SkipPopulateValue.js b/node_modules/mongoose/lib/helpers/populate/SkipPopulateValue.js new file mode 100644 index 0000000..5d46cfd --- /dev/null +++ b/node_modules/mongoose/lib/helpers/populate/SkipPopulateValue.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = function SkipPopulateValue(val) { + if (!(this instanceof SkipPopulateValue)) { + return new SkipPopulateValue(val); + } + + this.val = val; + return this; +};
\ No newline at end of file diff --git a/node_modules/mongoose/lib/helpers/populate/assignRawDocsToIdStructure.js b/node_modules/mongoose/lib/helpers/populate/assignRawDocsToIdStructure.js new file mode 100644 index 0000000..843c148 --- /dev/null +++ b/node_modules/mongoose/lib/helpers/populate/assignRawDocsToIdStructure.js @@ -0,0 +1,98 @@ +'use strict'; + +const leanPopulateMap = require('./leanPopulateMap'); +const modelSymbol = require('../symbols').modelSymbol; +const utils = require('../../utils'); + +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; + const nullIfNotFound = options.$nullIfNotFound; + 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 != null) { + if (options.lean) { + const _model = leanPopulateMap.get(doc); + doc = utils.clone(doc); + leanPopulateMap.set(doc, _model); + } else { + doc = doc.constructor.hydrate(doc._doc); + } + } + + if (recursed) { + if (doc) { + if (sorting) { + newOrder[resultOrder[sid]] = doc; + } else { + newOrder.push(doc); + } + } else if (id != null && id[modelSymbol] != null) { + newOrder.push(id); + } else { + newOrder.push(options.retainNullValues || nullIfNotFound ? null : 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; + }); + } +}
\ No newline at end of file diff --git a/node_modules/mongoose/lib/helpers/populate/assignVals.js b/node_modules/mongoose/lib/helpers/populate/assignVals.js new file mode 100644 index 0000000..9fd51d8 --- /dev/null +++ b/node_modules/mongoose/lib/helpers/populate/assignVals.js @@ -0,0 +1,274 @@ +'use strict'; + +const SkipPopulateValue = require('./SkipPopulateValue'); +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'); + // `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 + 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 (val instanceof SkipPopulateValue) { + return val.val; + } + + 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, populatedModel); + } else if (o.justOne === false && !Array.isArray(val)) { + return valueFilter([val], options, populateOptions, populatedModel); + } + return valueFilter(val, options, populateOptions, populatedModel); + } + + for (let i = 0; i < docs.length; ++i) { + const existingVal = mpath.get(o.path, docs[i], lookupLocalFields); + 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 (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); + // 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]; + const curPath = parts[0]; + for (let j = 0; j < parts.length - 1; ++j) { + // If we get to an array with a dotted path, like `arr.foo`, don't set + // `foo` on the array. + if (Array.isArray(cur) && !utils.isArrayIndex(parts[j])) { + 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; + } + 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 + mpath.set(o.path, valueToSet, docs[i], lookupLocalFields, setValue, false); + } +}; + +function numDocs(v) { + if (Array.isArray(v)) { + // If setting underneath an array of populated subdocs, we may have an + // array of arrays. See gh-7573 + if (v.some(el => Array.isArray(el))) { + return v.map(el => numDocs(el)); + } + 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 (subdoc != null && 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); +}
\ No newline at end of file diff --git a/node_modules/mongoose/lib/helpers/populate/getModelsMapForPopulate.js b/node_modules/mongoose/lib/helpers/populate/getModelsMapForPopulate.js new file mode 100644 index 0000000..66baf19 --- /dev/null +++ b/node_modules/mongoose/lib/helpers/populate/getModelsMapForPopulate.js @@ -0,0 +1,522 @@ +'use strict'; + +const MongooseError = require('../../error/index'); +const SkipPopulateValue = require('./SkipPopulateValue'); +const get = require('../get'); +const getDiscriminatorByValue = require('../discriminator/getDiscriminatorByValue'); +const isPathExcluded = require('../projection/isPathExcluded'); +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'); + +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; + + let allSchemaTypes = getSchemaTypes(modelSchema, null, options.path); + allSchemaTypes = Array.isArray(allSchemaTypes) ? allSchemaTypes : [allSchemaTypes].filter(v => v != null); + const _firstWithRefPath = allSchemaTypes.find(schematype => get(schematype, 'options.refPath', null) != null); + + 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 + if (schema != null && + schema.$isMongooseDocumentArray && + schema.options.ref == null && + schema.options.refPath == null) { + continue; + } + // 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]) { + continue; + } + 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 = !!_firstWithRefPath; + let normalizedRefPath = _firstWithRefPath ? get(_firstWithRefPath, 'options.refPath', null) : null; + + if (Array.isArray(schema)) { + const schemasArray = schema; + for (const _schema of schemasArray) { + let _modelNames; + let res; + try { + res = _getModelNames(doc, _schema); + _modelNames = res.modelNames; + isRefPath = isRefPath || res.isRefPath; + normalizedRefPath = normalizeRefPath(normalizedRefPath, doc, options.path) || + res.refPath; + justOne = res.justOne; + } catch (error) { + return error; + } + + if (isRefPath && !res.isRefPath) { + continue; + } + if (!_modelNames) { + continue; + } + modelNames = modelNames || []; + for (const modelName of _modelNames) { + if (modelNames.indexOf(modelName) === -1) { + modelNames.push(modelName); + } + } + } + } else { + try { + const res = _getModelNames(doc, schema); + modelNames = res.modelNames; + isRefPath = res.isRefPath; + normalizedRefPath = res.refPath; + justOne = res.justOne; + } catch (error) { + return error; + } + + if (!modelNames) { + continue; + } + } + + const _virtualRes = getVirtual(model.schema, options.path); + const virtual = _virtualRes == null ? null : _virtualRes.virtual; + + let localField; + let count = false; + if (virtual && virtual.options) { + const virtualPrefix = _virtualRes.nestedSchemaPath ? + _virtualRes.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; + + if (virtual.options.skip != null && !options.hasOwnProperty('skip')) { + options.skip = virtual.options.skip; + } + if (virtual.options.limit != null && !options.hasOwnProperty('limit')) { + options.limit = virtual.options.limit; + } + if (virtual.options.perDocumentLimit != null && !options.hasOwnProperty('perDocumentLimit')) { + options.perDocumentLimit = virtual.options.perDocumentLimit; + } + } 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()` + if ('justOne' in options && options.justOne !== void 0) { + 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 = Array.isArray(schema) ? + schema.every(schema => !schema.$isMongooseArray) : + !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.path(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 = mpath.get(localField, doc, lookupLocalFields); + if (Array.isArray(localFieldValue)) { + 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(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. + if (isRefPath && normalizedRefPath != null) { + const pieces = normalizedRefPath.split('.'); + let cur = ''; + for (let j = 0; j < pieces.length; ++j) { + const piece = pieces[j]; + cur = cur + (cur.length === 0 ? '' : '.') + piece; + const schematype = modelSchema.path(cur); + if (schematype != null && + schematype.$isMongooseArray && + schematype.caster.discriminators != null && + Object.keys(schematype.caster.discriminators).length > 0) { + const subdocs = utils.getValue(cur, doc); + const remnant = options.path.substr(cur.length + 1); + const discriminatorKey = schematype.caster.schema.options.discriminatorKey; + modelNames = []; + for (const subdoc of subdocs) { + const discriminatorName = utils.getValue(discriminatorKey, subdoc); + const discriminator = schematype.caster.discriminators[discriminatorName]; + const discriminatorSchema = discriminator && discriminator.schema; + if (discriminatorSchema == null) { + continue; + } + const _path = discriminatorSchema.path(remnant); + if (_path == null || _path.options.refPath == null) { + const docValue = utils.getValue(localField.substr(cur.length + 1), subdoc); + ret = ret.map(v => v === docValue ? SkipPopulateValue(v) : v); + continue; + } + const modelName = utils.getValue(pieces.slice(j + 1).join('.'), subdoc); + modelNames.push(modelName); + } + } + } + } + + 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) { + // 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; + 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.perDocumentLimit != null || get(currentOptions, 'options.perDocumentLimit') != null) { + currentOptions = { + model: Model + }; + + if (isVirtual && get(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); + } + } + } + } + + return map; + + function _getModelNames(doc, schema) { + let modelNames; + let discriminatorKey; + let isRefPath = false; + let justOne = null; + + 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)); + } + + if (modelSchema.virtuals.hasOwnProperty(normalizedRefPath) && doc.$__ == null) { + modelNames = [modelSchema.virtuals[normalizedRefPath].applyGetters(void 0, doc)]; + } else { + 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) { + // `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; + } + } + + schemaForCurrentDoc = modelForCurrentDoc.schema._getSchema(options.path); + + if (schemaForCurrentDoc && schemaForCurrentDoc.caster) { + schemaForCurrentDoc = schemaForCurrentDoc.caster; + } + } + } else { + schemaForCurrentDoc = schema; + } + 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; + + 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 if ((refPath = get(schemaForCurrentDoc, 'options.refPath')) != null) { + isRefPath = true; + refPath = normalizeRefPath(refPath, doc, options.path); + modelNames = utils.getValue(refPath, doc); + if (Array.isArray(modelNames)) { + modelNames = utils.array.flatten(modelNames); + } + } 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, refPath: normalizedRefPath, justOne: justOne }; + } + + if (!Array.isArray(modelNames)) { + modelNames = [modelNames]; + } + + return { modelNames: modelNames, isRefPath: isRefPath, refPath: normalizedRefPath, justOne: justOne }; + } +}; + +/*! + * 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, schema) { + 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' && + // 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)) { + 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; +}
\ No newline at end of file diff --git a/node_modules/mongoose/lib/helpers/populate/getSchemaTypes.js b/node_modules/mongoose/lib/helpers/populate/getSchemaTypes.js new file mode 100644 index 0000000..15660df --- /dev/null +++ b/node_modules/mongoose/lib/helpers/populate/getSchemaTypes.js @@ -0,0 +1,198 @@ +'use strict'; + +/*! + * ignore + */ + +const Mixed = require('../../schema/mixed'); +const get = require('../get'); +const leanPopulateMap = require('./leanPopulateMap'); +const mpath = require('mpath'); + +const populateModelSymbol = require('../symbols').populateModelSymbol; + +/*! + * @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 (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 (doc == null || 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 (const schema of schemas) { + const _ret = search( + parts.slice(p), + schema, + 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 != null && topLevelDoc.$__ && topLevelDoc.populated(fullPath) && p < parts.length) { + const model = doc.$__.populated[fullPath].options[populateModelSymbol]; + if (model != null) { + const ret = search( + parts.slice(p), + model.schema, + subdoc ? mpath.get(trypath, subdoc) : null, + nestedPath.concat(parts.slice(0, p)) + ); + + if (ret) { + ret.$isUnderneathDocArray = ret.$isUnderneathDocArray || + !model.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, []); +}; diff --git a/node_modules/mongoose/lib/helpers/populate/getVirtual.js b/node_modules/mongoose/lib/helpers/populate/getVirtual.js new file mode 100644 index 0000000..fc1641d --- /dev/null +++ b/node_modules/mongoose/lib/helpers/populate/getVirtual.js @@ -0,0 +1,72 @@ +'use strict'; + +module.exports = getVirtual; + +/*! + * ignore + */ + +function getVirtual(schema, name) { + if (schema.virtuals[name]) { + return { virtual: schema.virtuals[name], path: void 0 }; + } + + 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) { + return { virtual: schema.virtuals[cur], path: nestedSchemaPath }; + } + 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) { + return { + virtual: schema.virtuals[rest], + nestedSchemaPath: [nestedSchemaPath, cur].filter(v => !!v).join('.') + }; + } + continue; + } + + if (i + 1 < parts.length && schema.discriminators) { + for (const key of Object.keys(schema.discriminators)) { + const res = getVirtual(schema.discriminators[key], rest); + if (res != null) { + const _path = [nestedSchemaPath, cur, res.nestedSchemaPath]. + filter(v => !!v).join('.'); + return { + virtual: res.virtual, + nestedSchemaPath: _path + }; + } + } + } + + nestedSchemaPath += (nestedSchemaPath.length > 0 ? '.' : '') + cur; + cur = ''; + continue; + } + + if (schema.discriminators) { + for (const discriminatorKey of Object.keys(schema.discriminators)) { + const virtualFromDiscriminator = getVirtual(schema.discriminators[discriminatorKey], name); + if (virtualFromDiscriminator) return virtualFromDiscriminator; + } + } + + return null; + } +} diff --git a/node_modules/mongoose/lib/helpers/populate/leanPopulateMap.js b/node_modules/mongoose/lib/helpers/populate/leanPopulateMap.js new file mode 100644 index 0000000..a333124 --- /dev/null +++ b/node_modules/mongoose/lib/helpers/populate/leanPopulateMap.js @@ -0,0 +1,7 @@ +'use strict'; + +/*! + * ignore + */ + +module.exports = new WeakMap();
\ No newline at end of file diff --git a/node_modules/mongoose/lib/helpers/populate/lookupLocalFields.js b/node_modules/mongoose/lib/helpers/populate/lookupLocalFields.js new file mode 100644 index 0000000..08ed763 --- /dev/null +++ b/node_modules/mongoose/lib/helpers/populate/lookupLocalFields.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = function lookupLocalFields(cur, path, val) { + if (cur == null) { + return cur; + } + + if (cur._doc != null) { + cur = cur._doc; + } + + if (arguments.length >= 3) { + cur[path] = val; + return val; + } + + + // Support populating paths under maps using `map.$*.subpath` + if (path === '$*') { + return cur instanceof Map ? + Array.from(cur.values()) : + Object.keys(cur).map(key => cur[key]); + } + + return cur[path]; +};
\ No newline at end of file diff --git a/node_modules/mongoose/lib/helpers/populate/normalizeRefPath.js b/node_modules/mongoose/lib/helpers/populate/normalizeRefPath.js new file mode 100644 index 0000000..233b741 --- /dev/null +++ b/node_modules/mongoose/lib/helpers/populate/normalizeRefPath.js @@ -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; +};
\ No newline at end of file diff --git a/node_modules/mongoose/lib/helpers/populate/removeDeselectedForeignField.js b/node_modules/mongoose/lib/helpers/populate/removeDeselectedForeignField.js new file mode 100644 index 0000000..39b893a --- /dev/null +++ b/node_modules/mongoose/lib/helpers/populate/removeDeselectedForeignField.js @@ -0,0 +1,31 @@ +'use strict'; + +const get = require('../get'); +const mpath = require('mpath'); +const parseProjection = require('../projection/parseProjection'); + +/*! + * ignore + */ + +module.exports = function removeDeselectedForeignField(foreignFields, options, docs) { + const projection = parseProjection(get(options, 'select', null), true) || + parseProjection(get(options, 'options.select', null), true); + + if (projection == null) { + return; + } + for (const foreignField of foreignFields) { + if (!projection.hasOwnProperty('-' + foreignField)) { + continue; + } + + for (const val of docs) { + if (val.$__ != null) { + mpath.unset(foreignField, val._doc); + } else { + mpath.unset(foreignField, val); + } + } + } +};
\ No newline at end of file diff --git a/node_modules/mongoose/lib/helpers/populate/validateRef.js b/node_modules/mongoose/lib/helpers/populate/validateRef.js new file mode 100644 index 0000000..9dc2b6f --- /dev/null +++ b/node_modules/mongoose/lib/helpers/populate/validateRef.js @@ -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 })); +}
\ No newline at end of file |