Routing based on GET parameters for Express/Mongoose REST API - node.js

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.

Related

Mongoose TypeError: User is not a constructor

I'm trying to add a subdocument to a parent schema with Mongoose and MongoDB however I'm being thrown the following error:
TypeError: User is not a constructor
This is based off Mongoose's documentation on subdocuments and I think everything is the same. How can I debug this further?
Router
// Add a destination to the DB
router.post('/add', function(req, res, next) {
let airport = req.body.destination
let month = req.body.month
let id = (req.user.id)
User.findById(id , function (err, User) {
if (err) return handleError(err)
function addToCart (airport, month, id) {
var user = new User ({
destinations: [(
airport = '',
month = ''
)]
})
dog.destinations[0].airport = airport
dog.destinations[0].month = month
dog.save(callback)
res.status(200).send('added')
}
addToCart()
})
console.log(airport)
})
Schema
var destinationSchema = new Schema({
airport: String,
month: String
})
// Define the scheme
var User = new Schema ({
firstName: {
type: String,
index: true
},
lastName: {
type: String,
index: true
},
email: {
type: String,
index: true
},
homeAirport: {
type: String,
index: true
},
destinations: [destinationSchema]
})
User.plugin(passportLocalMongoose)
module.exports = mongoose.model('User', User)
JavaScript is case sensitive about the variable names. You have User model and the User result with the same name.
Your code will work with the following change :
User.findById(id , function (err, user) {
/* ^ use small `u` */
if (err) return handleError(err)
/* rest of your code */
Also keep in mind that further in your code you are declaring another variable named user. You will need to change that to something different.

Mongoose query nested document returns empty array

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) { ... });

Mongoose - persistent `virtual` field?

Ok, I have node as backend, it has the following Mongoose model:
var SomeSchema = new Schema({
name: {type: String, required: true},
iplanned: {type: String, default:60}
});
SomeSchema.virtual('planned')
.get(function () {
return parseInt(this.iplanned / 60, 10) + ' mins';
})
.set(function (val) {
this.iplanned = parseInt(val, 10) * 60;
});
someModel = mongoose.model('Some', SomeSchema);
So far it is good, from node.js side of things I can work with the records and access this planned field as I like.
The following is a simple responder to serve this list through http:
exports.list = function (req, res) {
someModel.find(function (err, deeds) {
return res.send(deeds);
});
});
And here's the problem - the virtual field planned is not included in each record (well, it is understandable, sort of). Is there a way to inject somehow my virtual field to each record? Or I have to do the virtual conversion on a front end as well? (Yeah, I know that there's Meteor.js out there, trying go without it here).
In your schema you should redefine toJSON() method:
SomeSchema.methods.toJSON = function () {
var obj = this.toObject();
obj.planned = this.planned;
return obj;
};

Create unique autoincrement field with mongoose [duplicate]

This question already has answers here:
Mongoose auto increment
(15 answers)
Closed 2 years ago.
Given a Schema:
var EventSchema = new Schema({
id: {
// ...
},
name: {
type: String
},
});
I want to make id unique and autoincrement. I try to realize mongodb implementation but have problems of understanding how to do it right in mongoose.
My question is: what is the right way to implement autoincrement field in mongoose without using any plugins and so on?
const ModelIncrementSchema = new Schema({
model: { type: String, required: true, index: { unique: true } },
idx: { type: Number, default: 0 }
});
ModelIncrementSchema.statics.getNextId = async function(modelName, callback) {
let incr = await this.findOne({ model: modelName });
if (!incr) incr = await new this({ model: modelName }).save();
incr.idx++;
incr.save();
return incr.idx;
};
const PageSchema = new Schema({
id: { type: Number , default: 0},
title: { type: String },
description: { type: String }
});
PageSchema.pre('save', async function(next) {
if (this.isNew) {
const id = await ModelIncrement.getNextId('Page');
this.id = id; // Incremented
next();
} else {
next();
}
});
Yes, here's the "skinny" on that feature.
You need to have that collection in your mongo database. It acts as concurrent key allocation single record of truth if you want. Mongo's example shows you how to perform an "atomic" operation to get the next key and ensure that even there are concurrent requests you will be guaranteed to have the unique key returned without collisions.
But, mongodb doesn't implement that mechanism natively, they show you how to do it. They only provide for the _id to be used as unique document key. I hope this clarifies your approach.
To expand on the idea, go ahead and add that mongo suggested implementation to your defined Mongoose model and as you already guessed, use it in Pre-save or better yet pre-init event to ensure you always generate an id if you work with a collection server side before you save it to mongo.
You can use this.
This package every time generate unique value for this.
Package Name : uniqid
Link : https://www.npmjs.com/package/uniqid
Ignore all the above. Here is the solution
YourModelname.find().count(function(err, count){
req["body"].yourID= count + 1;
YourModelname.create(req.body, function (err, post) {
if (err) return next(err);
res.json(req.body);
});
});

Hide embedded document in mongoose/node REST server

I'm trying to hide certain fields on my GET output for my REST server. I have 2 schema's, both have a field to embed related data from eachother into the GET, so getting /people would return a list of locations they work at and getting a list of locations returns who works there. Doing that, however, will add a person.locations.employees field and will then list out the employees again, which obviously I don't want. So how do I remove that field from the output before displaying it? Thanks all, let me know if you need any more information.
/********************
/ GET :endpoint
********************/
app.get('/:endpoint', function (req, res) {
var endpoint = req.params.endpoint;
// Select model based on endpoint, otherwise throw err
if( endpoint == 'people' ){
model = PeopleModel.find().populate('locations');
} else if( endpoint == 'locations' ){
model = LocationsModel.find().populate('employees');
} else {
return res.send(404, { erorr: "That resource doesn't exist" });
}
// Display the results
return model.exec(function (err, obj) {
if (!err) {
return res.send(obj);
} else {
return res.send(err);
}
});
});
Here is my GET logic. So I've been trying to use the query functions in mongoose after the populate function to try and filter out those references. Here are my two schema's.
peopleSchema.js
return new Schema({
first_name: String,
last_name: String,
address: {},
image: String,
job_title: String,
created_at: { type: Date, default: Date.now },
active_until: { type: Date, default: null },
hourly_wage: Number,
locations: [{ type: Schema.ObjectId, ref: 'Locations' }],
employee_number: Number
}, { collection: 'people' });
locationsSchema.js
return new Schema({
title: String,
address: {},
current_manager: String, // Inherit person details
alternate_contact: String, // Inherit person details
hours: {},
employees: [{ type: Schema.ObjectId, ref: 'People' }], // mixin employees that work at this location
created_at: { type: Date, default: Date.now },
active_until: { type: Date, default: null }
}, { collection: 'locations' });
You should specify the fields you want to fetch by using the select() method. You can do so by doing something like:
if( endpoint == 'people' ){
model = PeopleModel.find().select('locations').populate('locations');
} else if( endpoint == 'locations' ){
model = LocationsModel.find().select('employees').populate('employees');
} // ...
You can select more fields by separating them with spaces, for example:
PeopleModel.find().select('first_name last_name locations') ...
Select is the right answer but it also may help to specify it in your schema so that you maintain consistency in your API and I've found it helps me to not remember to do it everywhere I perform a query on the object.
You can set certain fields in your schema to never return by using the select: true|false attribute on the schema field.
More details can be found here: http://mongoosejs.com/docs/api.html#schematype_SchemaType-select
SOLUTION!
Because this was so hard for me to find i'm going to leave this here for anybody else. In order to "deselect" a populated item, just prefix the field with "-" in your select. Example:
PeopleModel.find().populate({path: 'locations', select: '-employees'});
And now locations.employee's will be hidden.
If you remember from you SQL days, SELECT does a restriction on the table(s) being queried. Restrict is one of the primitive operations from the relational model and continues to be a useful feature as the relational model has evolved. blah blah blah.
In mongoose, the Query.select() method allows you to perform this operation with some extra features. Particularly, not only can you specify what attributes (columns) to return, but you can also specify what attributes you want to exclude.
So here's the example:
function getPeople(req,res, next) {
var query = PeopleModel.find().populate({path: 'locations', select: '-employees'});
query.exec(function(err, people) {
// error handling stuff
// process and return response stuff
});
}
function getLocations(req,res, next) {
var query = LocationModel.find().populate({path: 'employees', select: '-locations'});
query.exec(function(err, people) {
// error handling stuff
// processing and returning response stuff
});
}
app.get('people', getPeople);
app.get('locations', getLocations);
Directly from the Mongoose Docs:
Go to http://mongoosejs.com/docs/populate.html and search for "Query conditions and other options"
Query conditions and other options
What if we wanted to populate our fans array based on their age,
select just their names, and return at most, any 5 of them?
Story
.find(...)
.populate({
path: 'fans',
match: { age: { $gte: 21 }},
select: 'name -_id',
options: { limit: 5 }
})
.exec()
I just wanted to remark, for the simplicity of the endpoint you may be able to get away with this way to define the endpoints. However, in general this kind of dispacher pattern is not necessary and may pose problems later in development when developing with Express.

Resources