I have these schemas:
var Store = mongoose.model('Store', new Schema({
name: String
}));
var Client = mongoose.model('Cllient', new Schema({
name: String,
store: { type: Schema.ObjectId, ref: 'Store' }
}));
var Order = mongoose.model('Order', new Schema({
number: String,
client: { type: Schema.ObjectId, ref: 'Client' }
}));
I'm trying to code the Url handler of the API that returns the order details, which looks like this:
app.get('/api/store/:storeId/order/:orderId', function (...));
I'm passing the store id in the Url to quickly check if the logged user has permissions on the store. If not, it returns a 403 status. That said, I think this storeId and the orderId are enough data to get the order, so I'm trying to do a query on a nested document, but it just doesn't work.
Order.findOne(
{ 'client.store': req.params.storeId, _id: req.params.orderId },
function (err, order) { ... });
But the order object is null;
Even when I perform a find, it returns an empty array:
Order.find(
{ 'client.store': req.params.storeId },
function (err, results) { ... });
I know that I could as well pass the cliendId to the Url and check first if the client belongs to the store, and then retrieve the order from the client, but I think the client part is redundant, don't you think? I should be able to get the order in a secure way by using only these two fields.
What am I doing wrong here?
Ok, I found it. The secret was in the match option of populate. The final code looks like this:
Order
.findOne({ _id: req.params.orderId })
.populate({ path: 'client', match: { store: req.params.storeId } })
.exec(function (err, order) { ... });
Related
Hello I am new to nodejs and mongodb.
I have 3 models:
"user" with fields "name phone"
"Shop" with fields "name, address"
"Member" with fields "shop user status". (shop and user hold the "id" of respective collections).
Now when I create "shops" api to fetch all shop, then I need to add extra field "isShopJoined" which is not part of the model. This extra field will true if user who see that shop is joined it otherwise it will be false.
The problem happens when I share my model with frontend developers like Android/iOS and others, They will not aware of that extra field until they see the API response.
So is it ok if I add extra field in shops listing which is not part of the model? Or do I need to add that extra field in model?
Important note
All the code below has NOT been tested (yet, I'll do it when I can setup a minimal environment) and should be adapted to your project. Keep in mind that I'm no expert when it comes to aggregation with MongoDB, let alone with Mongoose, the code is only here to grasp the general idea and algorithm.
If I understood correctly, you don't have to do anything since the info is stored in the Member collection. But it forces the front-end to do an extra-request (or many extra-requests) to have both the list of Shops and to check (one by one) if the current logged user is a Member of the shop.
Keep in mind that the front-end in general is driven by the data (and so, the API/back-end), not the contrary. The front-end will have to adapt to what you give it.
If you're happy with what you have, you can just keep it that way and it will work, but that might not be very effective.
Assuming this:
import mongoose from "mongoose";
const MemberSchema = new mongoose.Schema({
shopId: {
type: ObjectId,
ref: 'ShopSchema',
required: true
},
userId: {
type: ObjectId,
ref: 'UserSchema',
required: true
},
status: {
type: String,
required: true
}
});
const ShopSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
address: {
//your address model
}
});
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
phone: {
type: String,
required: true,
},
// Add something like this
shopsJoined: {
type: Array,
default: [],
required: true
}
});
You could tackle this problem via 2 ways:
MongoDB Aggregates
When retrieving (back-end side) the list of shops, if you know the user that made the request, instead of simply returning the list of Shops, you could return an aggregate of Shops and Members resulting in an hybrid document containing both the info of Shops and Models. That way, the front-end have all the info it needs with one back-end request.
Important note
The following code might not work as-is and you'll have to adapt it, I currently have nothing to test it against. Keep in mind I'm not very familiar with aggregates, let alone with Mongoose, but you'll get the general idea by looking the code and comments.
const aggregateShops = async (req, res, next) => {
try {
// $lookup will merge the "Model" and "Shop" documents into one
// $match will return only the results matching the condition
const aggreg = await Model.aggregate({$lookup: {
from: 'members', //the name of the mongodb collection
localField: '_id', //the "Shop" field to match with foreign collection
foreignField: 'shopId', //the "Member" field to match with local collection
as: 'memberInfo' //the field name in which to store the "Member" fields;
}, {
$match: {memberInfo: {userId: myUserId}}
}});
// the result should be an array of object looking like this:
/*{
_id: SHOP_OBJECT_ID,
name: SHOP_NAME,
address: SHOP_ADDRESS,
memberInfo: {
shopId: SHOP_OBJECT_ID,
userId: USER_OBJECT_ID,
status: STATUS_JOINED_OR_NOT
}
}*/
// send back the aggregated result to front-end
} catch (e) {
return next(e);
}
}
Drop the Members collection and store the info elsewhere
Instinctively, I would've gone this way. The idea is to either store an array field shopsJoined in the User model, or a membersJoined array field in the Shops model. That way, the info is retrieved no matter what, since you still have to retrieve the Shops and you already have your User.
// Your PATCH route should look like this
const patchUser = async (req, res, next) => {
try {
// How you chose to proceed here is up to you
// I tend to facilitate front-end work, so get them to send you (via req.body) the shopId to join OR "un-join"
// They should already know what shops are joined or not as they have the User
// For example, req.body.shopId = "+ID" if it's a join, or req.body.shopId = "-ID" if it's an un-join
if (req.body.shopId.startsWith("+")) {
await User.findOneAndUpdate(
{ _id: my_user_id },
{ $push: { shopsJoined: req.body.shopId } }
);
} else if (req.body.shopId.startsWith("-")) {
await User.findOneAndUpdate(
{ _id: my_user_id },
{ $pull: { shopsJoined: req.body.shopId } }
);
} else {
// not formatted correctly, return error
}
// return OK here depending on the framework you use
} catch (e) {
return next(e);
}
};
Of course, the above code is for the User model, but you can do the same thing for the Shop model.
Useful links:
MongoDB aggregation pipelines
Mongoose aggregates
MongoDB $push operator
MongoDB $pull operator
Yes you have to add the field to the model because adding it to the response will be only be a temporary display of the key but what if you need that in the future or in some list filters, so its good to add it to the model.
If you are thinking that front-end will have to be informed so just go it, and also you can set some default values to the "isShopJoined" key let it be flase for the time.
Users are able to post items which other users can request. So, a user creates one item and many users can request it. So, I thought the best way would be to put an array of users into the product schema for who has requested it. And for now I just want to store that users ID and first name. Here is the schema:
const Schema = mongoose.Schema;
const productSchema = new Schema({
title: {
type: String,
required: true
},
category: {
type: String,
required: true
},
description: {
type: String,
required: true
},
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
requests: [
{
userId: {type: Object},
firstName: {type: String}
}
],
});
module.exports = mongoose.model('Product', productSchema);
In my controller I am first finding the item and then calling save().
exports.postRequest = (req, res, next) => {
const productId = req.body.productId;
const userId = req.body.userId;
const firstName = req.body.firstName;
const data = {userId: userId, firstName: firstName};
Product.findById(productId).then(product => {
product.requests.push(data);
return product
.save()
.then(() => {
res.status(200).json({ message: "success" });
})
.catch(err => {
res.status(500).json({message: 'Something went wrong'});
});
});
};
Firstly, is it okay to do it like this? I found a few posts about this but they don't find and call save, they use findByIdAndUpdate() and $push. Is it 'wrong' to do it how I have done it? This is the second way I tried it and I get the same result in the database:
exports.postRequest = (req, res, next) => {
const productId = req.body.productId;
const userId = req.body.userId;
const firstName = req.body.firstName;
const data = {userId: userId, firstName: firstName};
Product.findByIdAndUpdate(productId, {
$push: {requests: data}
})
.then(() => {
console.log('succes');
})
.catch(err => {
console.log(err);
})
};
And secondly, if you look at the screen shot is the data in the correct format and structure? I don't know why there is _id in there as well instead of just the user ID and first name.
Normally, Developers will save only the reference of other collection(users) in the collection(product). In addition, you had saved username also. Thats fine.
Both of your methods work. But, second method has been added in MongoDB exactly for your specific need. So, no harm in using second method.
There is nothing wrong doing it the way you have done it. using save after querying gives you the chance to validate some things in the data as well for one.
and you can add additional fields as well (if included in the Schema). for an example if your current json return doesn't have a field called last_name then you can add that and save the doc as well so that's a benefit..
When using findById() you don't actually have the power to make a change other than what you program it to do
One thing I noticed.. In your Schema, after you compile it using mongoose.modal()
export the compiled model so that you can use it everywhere it's required using import. like this..
const Product = module.exports = mongoose.model('Product', productSchema);
I have three models: User, Post and Comment
var User = new Schema({
name: String,
email: String,
password: String // obviously encrypted
});
var Post = new Schema({
title: String,
author: { type: Schema.ObjectId, ref: 'User' }
});
var Comment = new Schema({
text: String,
post: { type: Schema.ObjectId, ref: 'Post' },
author: { type: Schema.ObjectId, ref: 'User' }
});
I need to get all posts in which the user has commented.
I know it should be a very simple and common use case, but right now I can't figure a way to make the query without multiple calls and manually iterating the results.
I've been thinking of adding a comments field to the Post schema (which I'd prefer to avoid) and make something like:
Post.find()
.populate({ path: 'comments', match: { author: user } })
.exec(function (err, posts) {
console.log(posts);
});
Any clues without modifying my original schemas?
Thanks
You have basically a couple of approaches to solving this.
1) Without populating. This uses promises with multiple calls. First query the Comment model for the particular user, then in the callback returned use the post ids in the comments to get the posts. You can use the promises like this:
var promise = Comment.find({ "author": userId }).select("post").exec();
promise.then(function (comments) {
var postIds = comments.map(function (c) {
return c.post;
});
return Post.find({ "_id": { "$in": postIds }).exec();
}).then(function (posts) {
// do something with the posts here
console.log(posts);
}).then(null, function (err) {
// handle error here
});
2) Using populate. Query the Comment model for a particular user using the given userId, select just the post field you want and populate it:
var query = Comment.find({ "author": userId });
query.select("post").populate("post");
query.exec(function(err, results){
console.log(results);
var posts = results.map(function (r) { return r.post; });
console.log(posts);
});
We have a requirement to store a copy of a Mongo document, as an embedded subdocument in another document. It should have a reference to the original document. The copied document needs to be a deep copy, like a snapshot of the original.
The original document's schema (defined with Mongoose) is not fixed -
it currently uses a type of inheritance to allow different additions to the Schema depending on "type".
Is there a way to such a flexible embedded schema within a Mongoose model?
Is it something that needs to be injected at runtime, when we can know
the schema?
The models / schemas we have currently look like this:
///UserList Schema: - this should contain a deep copy of a List
user: {
type: ObjectId,
ref: 'User'
},
list: {
/* Not sure if this is a how we should store the reference
type: ObjectId,
ref: 'List'
*/
listId: ObjectId,
name: {
type: String,
required: true
},
items: [{
type: ObjectId,
ref: 'Item'
}]
}
///List Schema:
name: {
type: String,
required: true
},
items: [{
type: ObjectId,
ref: 'Item'
}],
createdBy: {
type: ObjectId,
ref: 'User'
}
The code we currently have uses inheritance to allow different item types. I realise this technique may not be the best way to achieve the flexibility we require and is not the focus of my question.
///Item Model + Schema
var mongoose = require('mongoose'),
nodeutils = require('util'),
Schema = mongoose.Schema,
ObjectId = Schema.Types.ObjectId;
function ItemSchema() {
var self = this;
Schema.apply(this, arguments);
self.add({
question: {
type: String,
required: true
}
});
self.methods.toDiscriminator = function(type) {
var Item = mongoose.model('Item');
this.__proto__ = new Item.discriminators[type](this);
return this;
};
}
nodeutils.inherits(ItemSchema, Schema);
module.exports = ItemSchema;
I think you just need to create an empty {} object for the document in your parent mongoose schema. This way you´ll be able to store any object with a hardcopy of all it´s data.
parentobj : {
name: Sring,
nestedObj: {}
}
I think at this point, what you´ll need is to mark your nested objet as modified before you save it. Here is an example of my mongoose code.
exports.update = function(req, res) {
User.findById(req.params.id, function (err, eluser) {
if (err) { return handleError(res, err); }
if(!eluser) { return res.send(404); }
var updated = _.merge(eluser, req.body);
//This makes NESTEDDATA OBJECT to be saved
updated.markModified('nestedData');
updated.save(function (err) {
if (err) { return handleError(res, err); }
return res.json(200, eluser);
});
});
};
In addition, if you need an array of different documents in nestedDocument, the right way is this one:
parentobj : {
name: Sring,
nestedObjs: [Schema.Types.Mixed]
}
Please check Mongoose Schema Types carefully
EDIT
As you said, I´ll add you final solution as including ItemSchema in the nestedObj array definition to clarifythe type of the object to a determined one..
var ItemSchema = new Schema({
item1: String,
item2: String
});
var parentobj = new Schema({
name: Sring,
nestedObj: [ItemSchema]
});
EDIT 2:
Remember adding new Items to the nestedArray, must be done with nestedArray.push(item)
regards!!
I'm trying to wrap my head around how to handle more complicated queries when designing a REST API using Mongo + Express. Unfortunately, all of the examples I've been able to find have been too simple. Here's a simple example for the purpose of this question. I have Groups and I have Users. Within each Group, there are members and 1 leader. For the sake of simplicity, I am going to exclude middleware and post/put/delete functionality.
The routes would look something like this:
app.get('/groups', groups.all);
app.get('/groups/:groupId', groups.show);
app.param('groupId', groups.group);
The controller would look something like this:
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Group = mongoose.model('Group'),
_ = require('lodash');
/**
* Find group by id
*/
exports.group = function(req, res, next, id) {
Group.load(id, function(err, group) {
if (err) return next(err);
if (!group) return next(new Error('Failed to load group ' + id));
req.group = group;
next();
});
};
/**
* Show a group
*/
exports.show = function(req, res) {
res.jsonp(req.group);
};
/**
* List of Groups
*/
exports.all = function(req, res) {
Group.find().sort('-created').populate('user', 'name username').exec(function(err, groups) {
if (err) {
res.render('error', {
status: 500
});
} else {
res.jsonp(groups);
}
});
};
And then the model would look something like this:
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
/**
* Group Schema
*/
var GroupSchema = new Schema({
created: {
type: Date,
default: Date.now
},
updated: {
type: Date,
default: Date.now
},
enableScores: {
type: Boolean,
default: false
},
name: {
type: String,
default: '',
trim: true
},
creator: {
type: Schema.ObjectId,
ref: 'User'
},
commissioner: {
type: Schema.ObjectId,
ref: 'User'
}
});
/**
* Validations
*/
GroupSchema.path('name').validate(function(name) {
return name.length;
}, 'Name cannot be blank');
/**
* Statics
*/
GroupSchema.statics.load = function(id, cb) {
this.findOne({
_id: id
})
.populate('creator', 'name username')
.populate('commissioner', 'name username')
.exec(cb);
};
mongoose.model('Group', GroupSchema);
What if I want to query the REST API based off the commissioner field? Or the creator, name, or created field? Is this possible using Express's routing and if so, is there a best practice?
It seems like instead of handling each of these unique situations, it would be better to have something generic that returns all groups that match based off of req.params because if the model changes later down the line, I won't need to update the controller. If this is the way to do it, then perhaps modifying the all() function to find based off of the req.params is the solution? So if nothing is provided, then it returns everything, but as you provide more get parameters, it drills down on what you are looking for.
I would recommend using req.query for matching fields in your schema.
If you send a request like /groups?name=someGrp&enableScores=1 the req.query will look like this...
{name: "someGrp", enableScores: 1}
You can pass this object to the find method like this...
Group.find(req.query, callback);
This approach will work for simple property matching queries but for other things like comparisons and array properties you will have to write additional code.