Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Random error when using refPath & strictPopulate on deep subdocs #15026

Closed
2 tasks done
BobBatard opened this issue Nov 8, 2024 · 0 comments
Closed
2 tasks done

Random error when using refPath & strictPopulate on deep subdocs #15026

BobBatard opened this issue Nov 8, 2024 · 0 comments
Labels
has repro script There is a repro script, the Mongoose devs need to confirm that it reproduces the issue
Milestone

Comments

@BobBatard
Copy link

BobBatard commented Nov 8, 2024

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Mongoose version

8.7.1

Node.js version

20.7.0

MongoDB server version

8.0.1

Typescript version (if applicable)

No response

Description

Some context: I have a parent model, which contains a property child that stores an ObjectId pointing to either a childA or childB model, with the model being specified in a childType property. The value of childType is passed to the refPath of the child property.

childA has a property subChildA, and subChildA has a property grandChildA.
childB has no properties.

parent  > childA > subChildA > grandChildA
        > childB

When performing a find on parent and populating the entire chain of children, the populate function sometimes mixes up the models, resulting in an error indicating that child.subChildA.grandChildA is not in the schema.

/node_modules/mongoose/lib/helpers/populate/getModelsMapForPopulate.js:50
    return new StrictPopulate(options._fullPath || options.path);
           ^

StrictPopulateError: Cannot populate path `child.subChildA.grandChildA` because it is not in your schema. Set the `strictPopulate` option to false to override.
    at getModelsMapForPopulate (/Users/bob/Documents/work/inmemori/crm_api/node_modules/mongoose/lib/helpers/populate/getModelsMapForPopulate.js:50:12)
    at populate (/Users/bob/Documents/work/inmemori/crm_api/node_modules/mongoose/lib/model.js:4245:21)
    at _populate (/Users/bob/Documents/work/inmemori/crm_api/node_modules/mongoose/lib/model.js:4205:5)
    at /Users/bob/Documents/work/inmemori/crm_api/node_modules/mongoose/lib/model.js:4177:5
    at new Promise (<anonymous>)
    at Function.populate (/Users/bob/Documents/work/inmemori/crm_api/node_modules/mongoose/lib/model.js:4176:10)
    at populate (/Users/bob/Documents/work/inmemori/crm_api/node_modules/mongoose/lib/model.js:4335:13)
    at _populate (/Users/bob/Documents/work/inmemori/crm_api/node_modules/mongoose/lib/model.js:4205:5)
    at /Users/bob/Documents/work/inmemori/crm_api/node_modules/mongoose/lib/model.js:4177:5
    at new Promise (<anonymous>) {
  path: 'child.subChildA.grandChildA'

Here’s what I observed in the code of the getModelsMapForPopulate.js file:

In getModelsMapForPopulate, adding
console.log(model, options.path)

just before line 50:
return new StrictPopulate(options._fullPath || options.path)

outputs Model { childB } subChildA.grandChildA.

Adding options = _.cloneDeep(options) at the beginning of this function resolves the issue.

In the addModelNamesToMap function, replacing line 526
utils.merge(currentOptions, options)
with
utils.merge(currentOptions, _.cloneDeep(options))

also resolves the issue.

It appears, then, that the problem is caused by a mutation of the options object passed as a parameter to the function, associated with a concurrency issue, given the random nature of the bug.

Steps to Reproduce

import mongoose from 'mongoose'

(async () => {
	await mongoose.connect('mongodb://localhost:27017/test')
	await mongoose.connection.dropDatabase()

	const { ObjectId } = mongoose.Types

	const Parent = new mongoose.Schema({
		child: { type: ObjectId, refPath: 'childType' },
		childType: { type: String, enum: ['childA', 'childB'] }
	})

	const ChildA = new mongoose.Schema({
		subChildA: { type: ObjectId, ref: 'subChildA' }
	})

	const SubChildA = new mongoose.Schema({
		grandChildA: { type: ObjectId, ref: 'grandChildA' }
	})

	const GrandChildA = new mongoose.Schema()

	const ChildB = new mongoose.Schema()

	const parentModel = mongoose.model('parent', Parent)
	const childAModel = mongoose.model('childA', ChildA)
	const subChildAModel = mongoose.model('subChildA', SubChildA)
	const grandChildAModel = mongoose.model('grandChildA', GrandChildA)
	const childBModel = mongoose.model('childB', ChildB)

	const grandChildA = await grandChildAModel.create({})
	const subChildA = await subChildAModel.create({ grandChildA })
	const childA = await childAModel.create({ subChildA })
	const childB = await childBModel.create({})

	await parentModel.create([
		{ child: childA, childType: 'childA' },
		{ child: childB, childType: 'childB' }
	])

	const getParents = () => parentModel
		.find()
		.populate({
			path: 'child',
			populate: {
				path: 'subChildA',
				strictPopulate: false,
				populate: {
					path: 'grandChildA',
					strictPopulate: true
				}
			}
		})

	// you may have to duplicate these lines to trigger the bug
	// on my computer, the problem doesn't occur when disabling the second line
	// the problem doesn't occur either when increasing the size of the arrays to 1000 😮
	// as a last resort, rerunning the script several times should be enough to trigger the bug
	// for i in {1..100}; do node strictPopulate.mjs; done
	await Promise.all(new Array(100).fill().map(getParents))
	await Promise.all(new Array(100).fill().map(getParents))

	process.exit()
})()

Expected Behavior

well, it shouldn't throw :)
thanks for your help, and this wonderful project !

@BobBatard BobBatard changed the title Random crash when using refPath & strictPopulate on deep subdocs Random error when using refPath & strictPopulate on deep subdocs Nov 11, 2024
@vkarpov15 vkarpov15 added this to the 8.8.3 milestone Nov 20, 2024
@vkarpov15 vkarpov15 added the has repro script There is a repro script, the Mongoose devs need to confirm that it reproduces the issue label Nov 20, 2024
@vkarpov15 vkarpov15 modified the milestones: 8.8.3, 8.8.4 Nov 26, 2024
@vkarpov15 vkarpov15 modified the milestones: 8.8.4, 8.9.1 Dec 5, 2024
vkarpov15 added a commit that referenced this issue Dec 9, 2024
vkarpov15 added a commit that referenced this issue Dec 11, 2024
fix(query): clone PopulateOptions when setting _localModel to avoid state leaking between subpopulate instances
@vkarpov15 vkarpov15 modified the milestones: 8.9.1, 8.9 Dec 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
has repro script There is a repro script, the Mongoose devs need to confirm that it reproduces the issue
Projects
None yet
Development

No branches or pull requests

2 participants