fix(populate): make refPath work as a function#16035
Conversation
…amic references in arrays re #16028
There was a problem hiding this comment.
Pull request overview
This PR fixes and clarifies support for function-style refPath across both document assignment/casting ($set / create) and populate(), ensuring consistent (doc, path) semantics (including indexed paths for array subdocs) and improving error handling when refPath returns invalid values.
Changes:
- Aligns
refPathfunction invocation semantics between assignment/casting and populate (correctthis,doc, and root-relative indexedpath). - Improves error reporting by throwing
MongooseErrorwhen a functionrefPathreturns a non-string, rather than leakingTypeError. - Adds extensive regression tests and updates populate documentation to codify the function
refPathcontract.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/model.populate.test.js | Adds comprehensive populate coverage for function refPath, including indexed paths in arrays and lean/doc.populate parity. |
| test/document.test.js | Adds assignment/casting ($set / create) coverage for function refPath, including non-string return error handling. |
| lib/helpers/populate/modelNamesFromRefPath.js | Ensures non-string refPath values throw a MongooseError early with a clear message. |
| lib/helpers/populate/getModelsMapForPopulate.js | Updates populate model resolution for function refPath to use indexed paths per subdoc and avoids discriminator traversal issues with numeric segments. |
| lib/document.js | Fixes assignment/casting flow to call function refPath correctly (with root-relative indexed path) and validates return type. |
| docs/populate.md | Documents the function refPath contract (root-relative path, indexed array paths, consistent behavior across assignment and populate). |
hasezoey
left a comment
There was a problem hiding this comment.
LGTM, some minor nitpicks.
| const StrictPopulate = require('../../error/strictPopulate'); | ||
| const numericPathSegmentPattern = '\\.\\d+(?=\\.|$)'; |
There was a problem hiding this comment.
Maybe there should be a empty line in between (as the first part are just imports)
| for (const key of Object.keys(source)) { | ||
| const value = source[key]; |
There was a problem hiding this comment.
Is this faster than just using Object.entries()?
| it('should work with string refPath when assigning a document (baseline)', async function() { | ||
| // Arrange | ||
| const { User, ActivityLog } = createTestContext({ refPath: 'targetModel' }); | ||
| const user = await User.create({ name: 'Hafez' }); | ||
|
|
||
| // Act | ||
| const activityLog = await ActivityLog.create({ targetModel: 'User', targetUser: user }); | ||
| const activityLogFromDb = await ActivityLog.findById(activityLog._id).populate('targetUser'); | ||
|
|
||
| // Assert | ||
| assert.equal(activityLogFromDb.targetUser.name, 'Hafez'); | ||
| }); |
There was a problem hiding this comment.
If this is the baseline, shouldnt it be ordered before the other tests?
…ray and use faster string comparison
vkarpov15
left a comment
There was a problem hiding this comment.
I made a couple of minor improvements, mostly perf related, but LGTM overall thanks.
Reading through this PR made me just want to get rid of refPath() function support though - we added that in #6683 but I find the refPath() function API to be inferior to ref() function, which we added even earlier (at least #6554) . WDYT?
| } | ||
| const modelName = val.get(refPath); | ||
|
|
||
| if (typeof refPath === 'function' && !refPath[modelSymbol]) { |
There was a problem hiding this comment.
What's the reason for this refPath[modelSymbol] check here?
| let normalizedRefPath = null; | ||
| const schemaPath = schema?.path; | ||
|
|
||
| if (schemaPath != null && populatePath.endsWith('.' + schemaPath)) { |
| * | ||
| * @param {Function} refPath | ||
| * @param {Document|Object} doc | ||
| * @param {SchemaType} schema |
There was a problem hiding this comment.
Going forward, we should make sure we do not name SchemaType variables schema, that is confusing since a Schema is a distinct class. I know this is far from the only place where we use schema to refer to a SchemaType, but I would like to change that going forward.

Fixes #16028
Re #6669 #8452 #8469 #8731
While working on
refPathas a function, I found two behavior gaps:$set/create), functionrefPathwas not handled correctly for the "assign a document to an ObjectId ref" flow, leading to confusing cast errors (for example,path.split is not a functionwrapped inCastError).refPathwas called withoptions.path(for examplenotifications.target) instead of an indexed path per subdoc (for examplenotifications.0.target).That mismatch meant array subdocs could not reliably resolve per-element model paths, and
$setvs populate could disagree.What this PR changes:
refPathcall semantics across assignment/casting and populate.refPathandreffunctions on subdocument being populated rather than top level doc #8469 (comment):thisis the subdocument/document currently being processed.refPathis interpreted as root-relative.pathto functionrefPath(for examplenotifications.0.target,channels.1.notifications.0.target).pathformat in both$setand populate flows.MongooseError(instead of leakingTypeError) when functionrefPathreturns a non-string.pathpassed torefPath).I chose to always use indexed paths because assignment/casting needs a concrete element path to resolve the correct model for that exact subdocument.
If populate uses an unindexed path while
$setuses indexed resolution, behavior diverges.Using indexed root-relative paths everywhere gives one consistent contract and ensures function
refPathcomputes the same result in both flows.This passes all the current tests since the existing tests relied on regex replacements like
path.replace(/\.item$/, '.kind');Given how broken
refPathcurrently is, I think it's safe to introduce this change in a minor versionI tested this feature extensively with different scenarios:
Additionally, I updated
populatedocs to clarify functionrefPathcontract, root-relative indexedpathbehavior, and the array-subdocument pattern.In particular, for array subdocs, static string
refPathlikeitems.targetModelis insufficient when per-element resolution is needed. The recommended pattern is functionrefPath, for example: