Mongoose populate ObjectID from multiple possible collections - node.js

I have a mongoose model that looks something like this
var LogSchema = new Schema({
item: {
type: ObjectId,
ref: 'article',
index:true,
},
});
But 'item' could be referenced from multiple collections. Is it possible to do something like this?
var LogSchema = new Schema({
item: {
type: ObjectId,
ref: ['article','image'],
index:true,
},
});
The idea being that 'item' could be a document from the 'article' collection OR the 'image' collection.
Is this possible or do i need to manually populate?

Question is old, but maybe someone else still looks for similar issues :)
I found in Mongoose Github issues this:
mongoose 4.x supports using refPath instead of ref:
var schema = new Schema({
name:String,
others: [{ value: {type:mongoose.Types.ObjectId, refPath: 'others.kind' } }, kind: String }]
})
In #CadeEmbery case it would be:
var logSchema = new Schema({
item: {type: mongoose.Types.ObjectId, refPath: 'kind' } },
kind: String
})
But I did't try it yet...

First of all some basics
The ref option says mongoose which collection to get data for when you use populate().
The ref option is not mandatory, when you do not set it up, populate() require you to give dynamically a ref to him using the model option.
#example
populate({ path: 'conversation', model: Conversation }).
Here you say to mongoose that the collection behind the ObjectId is Conversation.
It is not possible to gives populate or Schema an array of refs.
Some others Stackoverflow people asked about it.
Soluce 1: Populate both (Manual)
Try to populate one, if you have no data, populate the second.
Soluce 2: Change your schema
Create two link, and set one of them.
var LogSchema = new Schema({
itemLink1: {
type: ObjectId,
ref: 'image',
index: true,
},
itemLink2: {
type: ObjectId,
ref: 'article',
index: true,
},
});
LogSchema.find({})
.populate('itemLink1')
.populate('itemLink2')
.exec()

Dynamic References via refPath
Mongoose can also populate from multiple collections based on the value of a property in the document. Let's say you're building a schema for storing comments. A user may comment on either a blog post or a product.
body: { type: String, required: true },
on: {
type: Schema.Types.ObjectId,
required: true,
// Instead of a hardcoded model name in `ref`, `refPath` means Mongoose
// will look at the `onModel` property to find the right model.
refPath: 'onModel'
},
onModel: {
type: String,
required: true,
enum: ['BlogPost', 'Product']
}
});
const Product = mongoose.model('Product', new Schema({ name: String }));
const BlogPost = mongoose.model('BlogPost', new Schema({ title: String }));
const Comment = mongoose.model('Comment', commentSchema);

Related

How can I populate an array field without ref in MongoDB (mongoose)?

I have a Schema that contains an array field. In this array field I'll insert objects that contains the author's _id and an author's comment.
I want populate this field, but my Schema don't have ref.
that's my Schemas
const Book = new Schema({
name: {
type:String,
required: true
}
authors: [{
type: Array
}]
})
const Author = new Schema({
name: {
type:String,
required: true
}
(... author's data)
})
I will insert objects in authors field of Book collection:
insertAuthor = {
_id:id,
comment: 'comment'
}
It's working fine, but I can't populate the authors field.
Can anybody help me with this?
Thank you!!
You can specify the model in the populate if the ref is not defined in the Schema. So from what I understand you need to query the Book by populating Author.
const books = await Book.find().populate({path: 'authors._id', model: 'Author'}).exec();
Also in your Book schema if you are inserting a JSON object as mentioned in your question then don't need to define type: Array inside the JSON. You can update it as below.
const Book = new Schema({
name: {
type:String,
required: true
}
authors: [{}] //or [{_id: {type: Schema.Types.ObjectId}, comment: {type: String}}]
})

Is there a way to query a document based on a subdocument object ref?

Suppose we have a schema that looks like this:
const RandomSchema = new Schema({
name: String,
randomField: String,
subDoc: {
name: String,
refDoc: {
type: Schema.Types.ObjectId,
ref: 'OtherModel',
required: true,
},
},
}, options);
Our OtherModel has a schema that looks like this:
const OtherModel = new Schema({
name: String,
funFact: String,
}, options);
From the front end of my application I'd like to query the RandomSchema model and return all instances of this model where subDoc.refDoc.funFact === someValue.
Is this possible? I know we have ways to populate those subdocs when return them but it happens only after matching docs have been returned, when in this case we'd need to know more than just the objectId of refDoc.
If multiple collections are involved, this task requires use of aggregation pipeline.

How to dynamically populate array of objects in mongoose?

I am trying to dynamically populate array of objects in mongoose. On my user model I want an array that contains all posts that user made. The problem is that I have multiple types of posts.
Different post models:
const ImagePost = mongoose.model('ImagePost', new Schema({ url: String }))
const TextPost = mongoose.model('TextPost', new Schema({ text: String }))
My user model looks like this:
const userSchema = new Schema({
userName: {
type: String,
required: true
},
posts: [{
postId: {
type: Schema.Types.ObjectId,
required: true,
refPath: "postModel"
},
postModel: {
type: String,
required: true,
enum: ['ImagePost', 'TextPost']
}
}]
})
const User = mongoose.model('User', userSchema)
How can I get the user from my database and automatically populate the posts the user made?
The whey I think it should work is this but for some reason it doesn't do anything:
User.findById('5d302c7caf1b8906ccb611b6').populate('posts.postId')
Changing your refPath from postModel to posts.postModel may solve your problem.

How to populate a document inside an array inside another document?

Sorry if title looks complicated... I couldn't think of a better way to describing it.
My real case situation matches the following Schemes:
Collection1:
const mongoose = require('mongoose');
const itemSchema = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
name: { type: String, required: [true, 'Name is required.'] },
quantity: { type: Number, required: [true, 'Quantity is required.'] },
collection2: { type: mongoose.Schema.Types.ObjectId, ref: 'Collection2' }
}, { _id : false });
const collection1Schema = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
name: { type: String, required: [true, 'Name is required.'] },
imagePath: { type: String, required: [true, 'Image is required.'] },
items: [itemSchema]
});
module.exports = mongoose.model('Collection1', collection1Schema);
Note: itemsSchema is inside the collection1 file (and having no declared _id's) because they only exist for the Collection1 model (considering "quantity" and other fields I removed for simplification). This itemsScheme is not needed elsewhere as another collection.
Collection2:
const mongoose = require('mongoose');
const collection2Schema = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
name: { type: String, required: [true, 'Name is required.'], unique: true }
});
module.exports = mongoose.model('Collection2', collection2Schema );
Note: Other properties (such as 'imagePath') were removed for simplification.
Now, this is the query I am trying to run:
Collection1.find()
.populate({
path: 'items',
populate: {
path: 'collection2', model: 'Collection2'
}
})
.then(...)
.catch(...);
And this is the error message I am getting when I run it:
Error fetching collection1: Cast to ObjectId failed for value "{
name: 'an item name',
quantity: 750
}" at path "_id" for model "Collection1"
The exact same error happens if I just run:
Collection1.find()
.populate('items')
.then(...)
.catch(...);
Maybe I cannot run .populate('items') because it has no declared model. If this is the case, how can I populate collection2 while querying collection1? Again, I cannot consider storing items in a separated collection.
But if I run:
Collection1.find()
.populate('collection2')
.then(...)
.catch(...);
I get the items, no errors, but it doesn't populate collection2. Well, it makes sense for the items because they're just an array of a block of properties inside collection1. But what about populating collection2?
Collection2 already has a few documents added, all with their _ids and other fields well filled. In the controller, I set _id: new mongoose.Types.ObjectId(), while creating a new document for both cases, Collection1 and Collection2.
In the front-end, I create a new document for Collection1, I add items, each item with a document from Collection2, and I save everything with no errors. I also confirmed everything is been properly saved (collection1 has list of items and each item an _id reference to collection2). The only problem is populating collection2 inside this array.
I have already tried restructuring everything with _ids (including itemScheme) and dropping all collections to test it again but no success.
I have been stuck with this problem for about three days now.
Is there any special property I should be setting for populate to make it work for this specific structure?
Thanks in advance...
populate('items')
This will not work as item is not a model.
What you want is following:
Collection1.find()
.populate('items.collection2')
.then(...)
.catch(...);
This will populate collection2 in all the array elements

Using UUIDs in mongoose for ObjectID references

I'm building a CRUD-style REST service with Node.js, Express and MongoDB using mongoose. This service is going to allow users of an already existing android application to upload/sync the contents of their individual databases online.
The data model for the already-existing application uses UUIDs (generated in Java) which clashes with the shorter, monotonic MongoDB style _id fields. Because the data model already exists and is populated with data from many users, I cannot convert the source data over to monotonic MongoDB-style _ids. This has left me with 2 options that I can think of: either 1) Make Mongo/Mongoose (or some other ODM) play nicely with full UUIDs instead of the monotonic _ids or 2) add a uuid field to the mongoose model in addition to the _id field and fight the pitfalls of this approach. I'm attempting to choose option #1 and running into issues with ObjectID references.
I originally stumbled upon mongoose-uuid, but unfortunately this isn't working for my use-case properly because it was overwriting my explicitly-set _id value when creating new Mongoose objects. Diving into the plugin code, it assumes that an object is new (by calling checking Mongoose's .isNew value) and thus overwrites the _id with a new uuid. Since I need to retain the original uuid when creating new documents in Mongo, this plugin isn't working for me.
Next, I found a post by Aaron Heckmann, creator of mongoose, on a similar topic. This has been helpful, however I am now encountering the problem where I cannot have my mongoose schemas reference each other by ObjectID, since they technically they are now referencing each other using String `_ids.
Schema example:
var mongoose = require('mongoose');
var uuid = require('node-uuid');
var Schema = mongoose.Schema;
var trackPassSchema = new Schema({
_id: { type: String, default: function genUUID() {
uuid.v1()
}},
//Omitting other fields in snippet for simplicity
vehicle: [
{type: Schema.Types.ObjectId, required: true, ref: 'Vehicle'}
]
});
module.exports = mongoose.model('TrackPass', trackPassSchema);
Referencing schema:
var mongoose = require('mongoose');
var uuid = require('node-uuid');
var Schema = mongoose.Schema;
var vehicleSchema = new Schema({
_id: { type: String, default: function genUUID() {
uuid.v1()
}},
//Omitting other fields in snippet for simplicity
description: {type: String},
year: {type: Number}
});
module.exports = mongoose.model('Vehicle', vehicleSchema);
When I attempt to call save() a trackPass that has been passed in from my application:
var trackPass = new TrackPass(req.body);
//Force the ID to match what was put into the request
trackPass._id = req.params.id;
trackPass.save(function (err) { ... }
I get the following error:
{ [CastError: Cast to ObjectId failed for value "b205ac4d-fd96-4b1e-892a-d4fab818ea2a" at path "vehicle"]
message: 'Cast to ObjectId failed for value "b205ac4d-fd96-4b1e-892a-d4fab818ea2a" at path "vehicle"',
name: 'CastError',
type: 'ObjectId',
value: ["b205ac4d-fd96-4b1e-892a-d4fab818ea2a"],
path: 'vehicle' }
I believe this error makes sense as I'm now using Strings which are longer than typical Mongo ObjectIDs. Without having the ObjectID reference, I don't believe I will be able to populate() referenced objects from other collections. I suppose I could simply not reference the other nested objects in my schema definitions, however I don't like this approach as I feel I will be losing a lot of the benefit of utilizing the ODM. Any other thoughts?
You can still use populate() with _id values of types besides ObjectID, but you do need to use the same type in the reference definition.
So your trackPassSchema would need to change to:
var trackPassSchema = new Schema({
_id: { type: String, default: function genUUID() {
return uuid.v1()
}},
vehicle: [
{type: String, required: true, ref: 'Vehicle'}
]
});
As Adam notes in the comments, you could simplify your default value to:
var trackPassSchema = new Schema({
_id: { type: String, default: uuid.v1 },
vehicle: [
{type: String, required: true, ref: 'Vehicle'}
]
});
Both JohnnyHK and Adam C answers are correct. But if you're using uuid in schema for an array of objects, it is good to use it like this
var trackPassSchema = new Schema({
_id: { type: String, default: () => uuid.v1 },
vehicle: [
{type: String, required: true, ref: 'Vehicle'}
]
});
Because, in one such scenario when i tried using like this _id: { type: String, default: () => uuid.v1 } multiple objects of the array had the same id.
It is not possible in this case as _id is unique field, but it can happen when you are using with fields that aren't unique.

Resources