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

Unable to populate subdocument's virtual array with dynamic ref #8277

Closed
yeegr opened this issue Oct 24, 2019 · 6 comments
Closed

Unable to populate subdocument's virtual array with dynamic ref #8277

yeegr opened this issue Oct 24, 2019 · 6 comments
Labels
help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary

Comments

@yeegr
Copy link

yeegr commented Oct 24, 2019

Can I populate a dynamically referenced (with "refPath") virtual field in a subdocument array in Mongoose?

The data structure goes like

Group
 - Members -> User

Code: models/schema

let MemberSchema = new Schema({
  userId: {
    type: Schema.Types.ObjectId,
    refPath: 'userRef',
    required: true
  },
  userRef: {
    type: String,
    required: true,
    enum: ['Admin', 'User'],
    default: 'Admin'
  },
  isCreator: {
    type: Boolean,
    required: true,
    default: false
  },
  isManager: {
    type: Boolean,
    required: true,
    default: false
  },
  alias: {
    type: String,
    required: true
  },
  joined: {
    type: Number,
    required: true,
    default: UTIL.getTimestamp()
  }
}, {
  toObject: {
    virtuals: true
  },
  toJSON: {
    virtuals: true
  }
})

MemberSchema.virtual('user', {
  ref: (doc) => doc.userRef,
  localField: 'userId',
  foreignField: '_id',
  justOne: true
})

let GroupSchema = new Schema({
  title: {
    type: String,
    required: true
  },
  slug: {
    type: String,
    required: true,
    default: randomstring.generate({
      length: CONFIG.GROUP_NAME_LENGTH,
      charset: CONFIG.GROUP_NAME_CHARSET
    })
  },
  updated: {
    type: Number,
    required: true,
    default: UTIL.getTimestamp()
  },
  members: [MemberSchema]
}, {
  toObject: {
    virtuals: true
  },
  toJSON: {
    virtuals: true
  }
})

export default model('Group', GroupSchema)

Code: controller

export function get(req, res) {
  GroupModel
  .findOne({
    slug: req.params.slug
  })
  .populate({
    path: 'members.user'
  })
  .exec()
  .then((data) => {
    if (data) {
      res.status(200).json(data)        
    } else {
      res.status(404).send()
    }
  })
  .catch((err) => {
    res.status(res.statusCode).send()
    console.log(err)
  })
}

I will get a populated user if I add {model: 'Admin'} to the populate options, but that completely defeats the purpose.

@VictorGaiva
Copy link

VictorGaiva commented Oct 26, 2019

I'm having the same issue. The execution throws TypeError: Cannot read property '0' of undefined . If I change my model's refPath to a static reference, the same populate script works as intended. Here it is the trace for the exepction:

TypeError: Cannot read property '0' of undefined
    at markArraySubdocsPopulated (/home/victor/Dev/MyProject/node_modules/mongoose/lib/document.js:555:48)
    at model.Document.$__init (/home/victor/Dev/MyProject/node_modules/mongoose/lib/document.js:515:3)
    at model.syncWrapper [as $__init] (/home/victor/Dev/MyProject/node_modules/kareem/index.js:234:23)
    at model.Document.init (/home/victor/Dev/MyProject/node_modules/mongoose/lib/document.js:481:8)
    at completeOne (/home/victor/Dev/MyProject/node_modules/mongoose/lib/query.js:2828:12)
    at /home/victor/Dev/MyProject/node_modules/mongoose/lib/query.js:2083:7
    at /home/victor/Dev/MyProject/node_modules/mongoose/lib/model.js:4598:16
    at /home/victor/Dev/MyProject/node_modules/mongoose/lib/utils.js:264:16
    at next (/home/victor/Dev/MyProject/node_modules/mongoose/lib/model.js:4071:5)
    at populate (/home/victor/Dev/MyProject/node_modules/mongoose/lib/model.js:4179:12)
    at _populate (/home/victor/Dev/MyProject/node_modules/mongoose/lib/model.js:4061:5)
    at /home/victor/Dev/MyProject/node_modules/mongoose/lib/model.js:4036:5
    at Object.promiseOrCallback (/home/victor/Dev/MyProject/node_modules/mongoose/lib/utils.js:249:12)
    at Function.Model.populate (/home/victor/Dev/MyProject/node_modules/mongoose/lib/model.js:4035:16)
    at model.Query.Query._completeOne (/home/victor/Dev/MyProject/node_modules/mongoose/lib/query.js:2077:9)
    at Immediate.<anonymous> (/home/victor/Dev/MyProject/node_modules/mongoose/lib/query.js:2116:10)

On function markArraySubdocsPopulated(doc, populated), populated is running the following data:

[
  PopulateOptions {
    _docs: {},
    path: 'supportChat.emiterId',
    _queryProjection: {}
  }
]

@VictorGaiva
Copy link

VictorGaiva commented Oct 26, 2019

On query.js line 2052:

Query.prototype._completeOne = function(doc, res, callback) {
  if (!doc && !this.options.rawResult) {
    return callback(null, null);
  }

  const model = this.model;
  const projection = utils.clone(this._fields);
  const userProvidedFields = this._userProvidedFields || {};
  // `populate`, `lean`
  const mongooseOptions = this._mongooseOptions;
  // `rawResult`
  const options = this.options;

  if (options.explain) {
    return callback(null, doc);
  }

  if (!mongooseOptions.populate) {
    return mongooseOptions.lean ?
      _completeOneLean(doc, res, options, callback) :
      completeOne(model, doc, res, options, projection, userProvidedFields,
        null, callback);
  }
  console.log(this._mongooseOptions);
  const pop = helpers.preparePopulationOptionsMQ(this, this._mongooseOptions);
  console.log(this._mongooseOptions);
  console.log(pop);
  model.populate(doc, pop, (err, doc) => {
    if (err) {
      return callback(err);
    }
    console.log(this._mongooseOptions);
    console.log(pop);
    
    return mongooseOptions.lean ?
      _completeOneLean(doc, res, options, callback) :
      completeOne(model, doc, res, options, projection, userProvidedFields,
        pop, callback);
  });
};

gives the following data on a successful run:

{
  populate: {
    'supportChat.emiterId': PopulateOptions { _docs: {}, path: 'supportChat.emiterId' }
  }
}
{
  populate: {
    'supportChat.emiterId': PopulateOptions {
      _docs: {},
      path: 'supportChat.emiterId',
      _queryProjection: {}
    }
  }
}
[
  PopulateOptions {
    _docs: {},
    path: 'supportChat.emiterId',
    _queryProjection: {}
  }
]
{
  populate: {
    'supportChat.emiterId': PopulateOptions {
      _docs: [Object],
      path: 'supportChat.emiterId',
      _queryProjection: {},
      isVirtual: false,
      virtual: null,
      [Symbol(mongoose.PopulateOptions#Model)]: Model { Client }
    }
  }
}
[
  PopulateOptions {
    _docs: { '5db2fb1193e1ed2878b97291': [Array] },
    path: 'supportChat.emiterId',
    _queryProjection: {},
    isVirtual: false,
    virtual: null,
    [Symbol(mongoose.PopulateOptions#Model)]: Model { Client }
  }
]

and the following on a failed one:

{
  populate: {
    'supportChat.emiterId': PopulateOptions { _docs: {}, path: 'supportChat.emiterId' }
  }
}
{
  populate: {
    'supportChat.emiterId': PopulateOptions {
      _docs: {},
      path: 'supportChat.emiterId',
      _queryProjection: {}
    }
  }
}
[
  PopulateOptions {
    _docs: {},
    path: 'supportChat.emiterId',
    _queryProjection: {}
  }
]
{
  populate: {
    'supportChat.emiterId': PopulateOptions {
      _docs: {},
      path: 'supportChat.emiterId',
      _queryProjection: {}
    }
  }
}
[
  PopulateOptions {
    _docs: {},
    path: 'supportChat.emiterId',
    _queryProjection: {}
  }
]

I'm not able to understand how the last two ones have different values on the successful one when being used inside the callback.

@VictorGaiva
Copy link

VictorGaiva commented Oct 26, 2019

On model.js line 4091, const modelsMap = getModelsMapForPopulate(model, docs, options); has an empty list when the crash happens, but has the following when it doesn't:

[
  {
    model: Model { Client },
    options: {
      model: Model { Client },
      _docs: [Object],
      path: 'supportChat.emiterId',
      _queryProjection: {},
      isVirtual: false,
      virtual: null
    },
    match: null,
    docs: [ [Object] ],
    ids: [ [Array] ],
    allIds: [ [Array] ],
    localField: Set { 'supportChat.emiterId' },
    foreignField: Set { '_id' },
    justOne: true,
    isVirtual: false,
    virtual: null,
    count: false,
    [Symbol(mongoose.PopulateOptions#Model)]: Model { Client }
  }
]

I'm pretty sure the bug can be isolated to mongoose/lib/helpers/populate/getModelsMapForPopulate.js.

@VictorGaiva
Copy link

VictorGaiva commented Oct 28, 2019

The issue seems to be here:

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

Given the following data:

normalizeRefPath(
  "emiterType",
  {
    _id: "5db2fb1193e1ed2878b97291",
    activeAgents: [],
    isActive: true,
    hasAgent: false,
    creationTimeStamp: "2019-10-25T13:29:41.344Z",
    clientRef: "5daf1403d5412851d02be2dc",
    supportChat: [
      {
        _id: "5db2fb1193e1ed2878b97290",
        data: 'Teste',
        emiterId: "5daf1403d5412851d02be2dc",
        emiterType: 'Client',
        originalMessage: "5db2fb1193e1ed2878b9728e'
      }
    ],
    __v: 1
  },
  "supportChat.emiterId"
);

it should've returned ['supportChat', 'emiterType'] but it returns 'emiterType' instead.

@VictorGaiva
Copy link

VictorGaiva commented Oct 28, 2019

Oh I see now. When there are nested schemes and the child scheme has a refPath, it transverses the document starting at the parent. So refPath should contain the full path to do so, but it doesn't by default.

A temporary workaround is to set the full path on child's refPath. Like this:

emiterId: { type: Schema.Types.ObjectId, refPath: 'supportChat.emiterType', required: true }

This issue should be fixed as soon as possible since the child document doesn't always knows how it is contained inside its parent component.

@vkarpov15
Copy link
Collaborator

@yeegr yes you should be able to do this:

MemberSchema.virtual('user', {
  ref: (doc) => doc.userRef,
  localField: 'userId',
  foreignField: '_id',
  justOne: true
})

@VictorGaiva glad you found your issue. You could also just do ref: doc => doc.emiterType

@vkarpov15 vkarpov15 added the help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary label Nov 3, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary
Projects
None yet
Development

No branches or pull requests

3 participants