mongoose relation between models - javascript

I am using mongoose and I have two models: Item and Hashtag.
Hashtag model should contain only name and Item model should contain a list of hashtags (represented by ids).
This is what I've done:
var ItemSchema = new Schema({
hashtags: [ { type: Schema.ObjectId, 'default': null, ref: 'Hashtag' } ],
});
var HashtagSchema = new Schema({
name: { type: String, 'default': '', trim: true },
items: [{ type: Schema.ObjectId, ref: 'Page' }]
});
This is how I try to create an Item:
var item = new Item({
hashtags: ['a', 'b', 'c']
});
item.save(function (err, item) {
if (err) return res.json({ error: err });
res.json(item);
});
Unfortunately I get this error:
CastError: Cast to ObjectId failed for value "a,b,c" at path "hashtags"
How can I solve this?

Since you are using references instead of subdocuments, you need to create the hashtag objects first:
var tagnames = ['a','b','c'];
var hashtags = {}; //for referencing quickly by name later
for (var h in tagnames){
var tag = new Hashtag({
name: tagnames[h]
});
tag.save(function (err, item) {
if (err) console.log('error:',err);
hashtags[item.name] = item;
});
}
Once you have the hashtags created, you can reference them::
var item = new Item({
hashtags: [hashtags.a._id,hashtags.b._id,hashtags.c._id]
});
item.save(function (err, item) {
if (err) return res.json({ error: err });
res.json(item);
});
Then you can use populate to automatically turn the object ids into documents:
Item.find({})
.populate('hashtags')
.exec(function (err, items) {
if (err) return handleError(err);
//items are populated with hashtags
});
If you are just doing simple tagging, then subdocuments may be a better fit. They allow you to declare and save child documents all in one step. The trade-off is that subdocuments belong exclusively to their parent documents. They are not references so any aggregations on them must be done manually.

Related

How to remove multiple references when removing many documents?

I'm removing a document from collection 'Component' to which there are references in collection 'Space'.
Upon removal of document from Component, I remove the reference to that document from Space.
There is another collection 'ListItem', to which there are references in 'Space'. As I delete the Component, I want to delete an unknown amount of documents from 'ListItem' and then delete all these references from 'Space'. I was able to delete the documents inside 'ListItem' but not the references in Space.
I couldn't figure out how to use hooks with this. I now understand this would only work for removing a single reference. If I could somehow, per instance of ListItem that gets removed, pull ListItem.ListId from Space.listItems, it might work? $pullAll seems to be the most promising potential solution I've found thus far. Then I'd have to query Space and find all matching ids from Space.listItems and store them in an array?
ListItemsSchema.pre('deleteMany', function(next) {
var list = this;
var id = this.ListId;
var spaceId = this.SpaceId;
list.model('Space').update(
{ _id: spaceId},
{$pull: {listItems: id}},
{multi: true},
next
);
// i'm removing ListItem.ListId from Space.listItems
})
I couldn't figure out how to solve it as I remove the ListItems. Same issue that the pull method is wrong.
router.delete('/DeleteComponent/:id', function(req, res) {
let id = req.params.id;
let spaceID = req.body.CurrentSpaceId;
let listArray = [];
Component.findOneAndRemove({ _id: id}).exec(function(err, removed) {
Space.findOneAndUpdate(
{ _id: spaceID },
{ $pull: {components: id} },
{ new: true },
function(err, removedFromSpace) {
if (err) { console.log(err) }
res.status(200).send(removedFromSpace);
})
});
// delete all items where id == component/list id
ListItem.deleteMany({ ListId: id}).exec(function(err, results) {
if (err) {
return console.log(err);
} else {
console.log("ListItems successfully removed.", results);
}
Space.update(
{ _id: spaceID},
{$pull: {listItems: id}},
{multi: true},
function(err, removedFromSpace) {
if (err) { console.log(err) }
res.status(200).send(removedFromSpace);
}
);
});
});
I've tried a few more things but these have been my best attempts so far.
Space's schema:
let spaceSchema = new mongoose.Schema({
components: [{type: Schema.Types.ObjectId, ref: 'Component'}],
listItems: [{type: Schema.Types.ObjectId, ref: 'ListItem'}],
});
Component's schema:
let componentSchema = new mongoose.Schema({
...
})
ListItem's schema:
let ListItemsSchema = new mongoose.Schema({
ListItem: String,
// ListId is equivalent to Component's _id
ListId: String,
SpaceId: String
});
Expected result is that when a component is deleted, then also ListItems are deleted along with references to them in Space. Currently I'm unable to do the last step of deleting the references for ListItems in Space.
I ended up solving the problem through making structural changes to the database schemas, ending up with a generally cleaner and more elegant solution.
I moved the references listItems: [{type: Schema.Types.ObjectId, ref: 'ListItem'}] to the componentSchema, so when a component is deleted, all the references to associated ListItem documents are also deleted, without having to worry about accessing Space or iterating over values to dynamically create arrays. After that, all ListItem documents where ListItem.ComponentId matches the id of the recently deleted component are deleted.
// Delete component and remove from Spaces
router.delete('/DeleteComponent/:id', function(req, res) {
let id = req.params.id;
let spaceID = req.body.CurrentSpaceId;
Component.findOneAndRemove({ _id: id}).exec(function(err, removed) {
Space.findOneAndUpdate(
{ _id: spaceID },
{ $pull: {components: id} },
{ new: true },
function(err, removedFromSpace) {
if (err) { console.log(err) }
res.status(200).send(removedFromSpace);
})
});
// delete all items where id == component id
ListItem.deleteMany({ ComponentId: id}).exec(function(err, results) {
if (err) {
return console.log(err);
} else {
console.log("ListItems successfully removed.", results);
}
});
});
I did make progress towards solving the original technical challenge as well. My suggestion would be to find a way to gather all the IDs you want to remove from a reference and put them in an array, and then you can use $pullAll to remove them from references:
let myIdsArray = [id1, id2, id3];
Collection.update( {_id: id}, { $pullAll: {field: myIdsArray } } );
However, as far as I can tell, unless you have a very good reason for your references to exist in such a disconnected way, it makes more sense to use the much simpler solution, especially when using MongoDB or any other non-relational database.

Mongoose: updating array in document not working

I'm trying to update an array in document by adding object if it doesn't exist, and replacing the object in array otherwise. But nothing ($push, $addToSet) except the $set parameter does anything, and $set works as expected - overwrites the whole array.
My mongoose schema:
var cartSchema = mongoose.Schema({
mail: String,
items: Array
});
The post request handler:
app.post('/addToCart', function(req, res) {
var request = req.body;
Cart.findOneAndUpdate({
"mail": request.mail
}, {
$addToSet: {
"items": request.item
}
}, {
upsert: true
},
function(err, result) {
console.log(result);
}
);
res.send(true);
});
The data that I'm sending from the client:
{
"mail":"test#gmail.com",
"item":{
"_id":"59da78db7e9e0433280578ec",
"manufacturer":"Schecter",
"referenceNo":"Daemon-412",
"type":"Gitare",
"image":"images/ba9727909d6c3c26412341907e7e12041507489988265.jpeg",
"__v":0,
"subcategories":[
"Elektricne"
]
}
}
EDIT:
I also get this log when I trigger 'addToCart' request:
{ MongoError: The field 'items' must be an array but is of type object in
document {_id: ObjectId('5a19ae2884d236048c8c91e2')}
The comparison in $addToSet would succeeded only if the existing document has the exact same fields and values, and the fields are in the same order. Otherwise the operator will fail.
So in your case, request.item always need to be exactly the same.
I would recommend creating a model of "item". Then, your cart schema would be like:
var cartSchema = mongoose.Schema({
mail: String,
items: [{
type: ObjectId,
ref: 'item',
}],
});
And let MongoDB determine if the item exist.
this should work you just need to implement objectExits function that test if the item is that one you're looking for :
Cart.findOne({ "mail": request.mail })
.exec()
.then(cart => {
var replaced = cart.items.some((item, i) => {
if (item._id == request.item._id)) {
cart.items[i] = request.item;
return true;
}
})
if (!replaced) {
cart.items.push(request.item);
}
cart.save();
return cart;
})
.catch(err => {
console.log(err)
});

How can I query a nested document and return attributes from it?

I have a Product collection and every document looks something like this:
"_id" : ObjectId("574393c59afcfdd3763d91b1"),
"name" : "dummy product name",
"price" : 200,
"category" : ObjectId("574393c59afcfdd3763d91ad"),
"images" : [ ],
"ratings" : [
2
],
"reviews" : [ ],
"purchase_count" : 0,
"tags" : [
ObjectId("574393c59afcfdd3763d91a9"),
ObjectId("574393c59afcfdd3763d91ab")
]
Where category and tags are references to their respective collections and they each have a name attribute. Here are all 3 schemas:
var ProductSchema = new mongoose.Schema({
name : {type: String, required: true},
price : { type: Number,required: true },
tags : [{ type: Schema.Types.ObjectId, ref: 'Tag' }],
purchase_count : {type: Number, default: 0},
reviews : [ReviewSchema],
ratings : [{type: Number, min: 0, max: 10}],
category : {type: Schema.Types.ObjectId, ref: 'ProductCategory'},
images : [{type: String}]
});
var ProductCategorySchema = new mongoose.Schema({
name : { type: String, required: true, unique: true, dropDups: true }});
var TagSchema = new mongoose.Schema({
name : { type: String, required: true, unique: true, dropDups: true, lowercase: true }
});
I'm trying to query the product collection and return data of each product like this:
app.models.Product.find({}, function(err, products){
if (err) throw err;
res.json(products);
}).populate('category').populate('tags').exec();
The only the difference is that I want the actual name of the category instead of a document representing the collection with it's ID. I also want the same for the tags , I just want a simple array of strings which represent the tag names. How can I do this in the query?
I tried implicitly defining what I wanted to select by using a select() after the second populate() and writing 'category.name tags.name (and then all the other attributes)' but that didn't work.
This is what the result of the query looks like:
This is how I got it to work. Credit goes to Neta Meta for giving me the idea of using pure JS.
app.models.Product.find({})
.populate('category' , 'name')
.populate('tags', 'name').populate('seller', 'name').exec(function(err, products){
if (err) throw err;
products = products.map(function(a){
var productToSend = {};
productToSend.name = a.name;
productToSend.price = a.price;
productToSend.description = a.description;
if(a.seller != undefined){
productToSend.seller = a.seller.name;
}
if(a.tags != undefined){
productToSend.tags = a.tags.map(function(b){
return b.name;
});
}
productToSend.purchase_count = a.purchase_count;
productToSend.reviews = a.reviews;
productToSend.ratings = a.ratings;
if(a.category != undefined){
productToSend.category = a.category.name;
}
productToSend.images = a.images;
return productToSend;
})
res.json(products);
});
To select specific field you would
app.models.Product.find({}, function(err, products){
if (err) throw err;
res.json(products);
}).populate('category', 'fieldname1 fieldname2').populate('tags', 'fieldname1 fieldname2').exec();
http://mongoosejs.com/docs/populate.html
However i dont see in your schema definition any reference to anything.
Also if you have / need any kind of relation (which is what you seem to need) dont use mongo.
Actually dont use mongo ever unless you absolutely must - try to stay a way from mongo.
update
Something like that.
var res = app.models.Product.find({}, function(err, products){
if (err) throw err;
res.json(products);
}).populate('category', 'fieldname1 fieldname2').populate('tags', 'fieldname1 fieldname2').exec();
res.category = categoty.fieldname;
res.tags = res.tags.map(function(tag){
return tag.fieldname
})
Another option might be - to add getter - not sure if this will work because populate might happened after so ..
but :
ProductSchema.virtual('categoryString').get(function () {
return this.category.fieldname
});
ProductSchema.virtual('tagsArr').get(function () {
return this.tags.map(function(tag){
if(tag && tag.name) {
return tag.fieldname;
}
return '';
});
});
I don't know why you send response before populate query. Using lodash package to modified your products before response.
var _ = require('lodash');
app.models.Product.find({}).populate('category tags').exec(function(err, products){
if (err) throw err;
var modified = _.map(products, function (product) {
product.tags = _.map(product.tags, function (tag) { return _.pick(tag, ['name']) });
product.category = _.pick(product.category, ['name']);
return product;
});
res.json(modified);
});

Why is Mongoose replacing key/value pair in an object with _id?

I am creating my first backend project with Node.js, Express.js and Mongoose. I have a user, with a list of stocks objects [{symbol: amount}].
When the user wants to buy a stock, they send a POST request with stock, an amount, and a verb in this case 'buy'. In the Post, I take the stock and amount from the request body and add it to the User's stock list.
A request with
{stock: 'F', amount: '2', verb: 'buy'}
should add
{'F': '2'}
to the user's stocks. The problem is when I create and push the stock with
stockObject[stock] = amount;
user.stocks.push(stockObject);
user.stocks becomes [{ _id: 54be8739dd63f94c0e000004 }] instead of [{'F': '2'}], but when I make
stockObject={'symbol':stock, 'amount': amount}
and push that I will get
[{'symbol': 'F', 'amount': '2', _id: 54be8739dd63f94c0e000004}]
Why will Mongoose replace my data in the first case, but keep it in the second?
var UserSchema = new Schema({
id: String,
stocks: [{
symbol: String,
amount: Number
}]
});
router.route('/user/:user_id/action')
.post(function(req, res) {
User.findOne({
id: req.params.user_id
}, function(err, user) {
if (err) res.send(err);
var stock = req.body.stock;
var amount = req.body.amount;
var stockObject = {};
if (req.body.verb === 'buy') {
stockObject[stock] = amount;
}
user.stocks.push(stockObject);
user.save(function(err) {
res.json({
stocks: user.stocks
});
});
});
})
The issue is that the first object you're trying to save:
console.log(stockObject);
// { 'F': '2' }
Doesn't match the Schema you've defined for it:
{
symbol: String,
amount: Number
}
Mongoose normalizes objects it saves based on the Schema, removing excess properties like 'F' when it's expecting only 'symbol' and 'amount'.
if(req.body.verb === 'buy') {
stockObject.symbol = stock;
stockObject.amount = amount;
}
To get the output as [{"F": "2"}], you could .map() the collection before sending it to the client:
res.json({
stocks: user.stocks.map(function (stock) {
// in ES6
// return { [stock.symbol]: stock.amount };
var out = {};
out[stock.symbol] = stock.amount;
return out;
});
});
Or, use the Mixed type, as mentioned in "How do you use Mongoose without defining a schema?," that would allow you to store { 'F': '2' }.
var UserSchema = new Schema({
id: String,
stocks: [{
type: Schema.Types.Mixed
}]
});

MongooseJS -- $pullAll from embedded document by value

Currently I have the following scheme model:
var userModel = mongoose.schema({
images: {
public: [{url: {type: String}}]}
});
I want to remove items from "images.public" from their url value, rather than their ID.
var to_remove = [{url: "a"}, {url: "b"}];
// db entry with id "ID" contains entries with these properties.
userModel.findByIdAndUpdate(ID,
{$pullAll: {"images.public": to_remove}},
function(err, doc) {
if (err) {
throw(err);
}
}
);
However, this does not show any errors and do not remove the items I want to remove from the database. How would I appropriately target this?
// Notice the change of structure in to_remove
var to_remove = ["a", "b"];
userModel.findByIdAndUpdate(ID,
{$pull: {"images.public": {url: {$in: to_remove}}}},
function(err, doc) {
if (err) {
throw(err);
}
}
);
Issue here being that the structure of variable to_remove changed, luckily that was not a problem in my case.

Categories