Mongoose: query one to many relationship, but inverse - node.js

I am doing an app with MongoDB/Mongoose/Node.js RESTapi.
I have two models: Books and Authors.
An Author can write many books. Only a book can be written by an author.
The models are defined like this.
author model
var authorSchema = new Schema({
name: {type: String, required: true},
surname: {type: String, required: true},
book model
var bookSchema = new Schema({
isbn: {type: String, required: true},
title: {type: String, required: true},
author: {type: Schema.Types.ObjectId, ref: 'Author'},
description: {type: String}
What I am trying to do, is get the books that were written by an author. This is my API endpoint. IdAuthor is the id that mongodb gives.
on books-routes.js
localhost:3000/api/book/author/:idAuthor
router.get('/author/:idAuthor', (req, res, next) => {
let idAuthor = req.params.idAuthor;
Book.find({ author: idAuthor })
.select('isbn title author description')
.exec()
.then(bookCollection => {
const response = {
count: bookCollection.length,
books: bookCollection
}
if (response.count) {
res.status(200).json(bookCollection);
} else {
res.status(200).json({
message: 'This author does not have any book'
});
}
})
.catch(err => {
res.status(500).json({
error: err
});
});
});
Sadly, it is allways returning an empty array of books. I am sure it has something to do about "author" attribute referencing to the authors model.
I have also tried doing this:
Book.find({ author: ObjectId(idAuthor) })
without luck.
Thank you for your help!

Related

Can I send an array of objects to the view, in the controller, associated with a mongodb object in node/ express/ mongoose?

I have been following a tutorial on the MDN site for Node/ express/ mongoose. It may be familiar to many people but I will put the code down anyway. What I want to do is create a view that is similar to the book_list page, however, I wish to have the ability to send the book instances with each book (details will follow). In other words I wish to be able to have the BookInstances for each book as part of the book object on the list page - it is mainly for the count (or length) but I may wish to also use it in other ways.
The book model
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var BookSchema = new Schema({
title: {type: String, required: true},
author: { type: Schema.ObjectId, ref: 'Author', required: true },
summary: {type: String, required: true},
isbn: {type: String, required: true},
genre: [{ type: Schema.ObjectId, ref: 'Genre' }]
});
// Virtual for this book instance URL.
BookSchema
.virtual('url')
.get(function () {
return '/catalog/book/'+this._id;
});
// Export model.
module.exports = mongoose.model('Book', BookSchema);
BookInstance Schema part of the Model:
var BookInstanceSchema = new Schema(
{
book: { type: Schema.Types.ObjectId, ref: 'Book', required: true },//reference to the associated book
imprint: { type: String, required: true },
status: { type: String, required: true, enum: ['Available', 'Maintenance', 'Loaned', 'Reserved'], default: 'Maintenance' },
due_back: { type: Date, default: Date.now }
}
);
The book_list controller:
// Display list of all Books.
exports.book_list = function(req, res, next) {
Book.find({}, 'title author')
.populate('author')
.exec(function (err, list_books) {
if (err) { return next(err); }
//Successful, so render
res.render('book_list', { title: 'Book List', book_list: list_books });
});
};
The book detail controller:
// Display detail page for a specific book.
exports.book_detail = function(req, res, next) {
async.parallel({
book: function(callback) {
Book.findById(req.params.id)
.populate('author')
.populate('genre')
.exec(callback);
},
book_instance: function(callback) {
BookInstance.find({ 'book': req.params.id })
.exec(callback);
},
}, function(err, results) {
if (err) { return next(err); }
if (results.book==null) { // No results.
var err = new Error('Book not found');
err.status = 404;
return next(err);
}
// Successful, so render.
res.render('book_detail', { title: 'Book Detail', book: results.book,
book_instances: results.book_instance } );
});
};
I have a feeling it must be something that can maybe be done with populate but I have not got that to work. The only way I have managed to get the book instance object to appear in the list for each book item is to send all book instances to the view. From there I use a foreach loop and then IF statement to get the book instances for each book. It looks really ugly and I am sure there must be some other way to do this. I am used to asp.net mvc - in that you use a virtual object. I am not sure if I am supposed to modify the model here or the controller. I may also want to pass in a much more complex model with lists within lists.
I have noted the genre is actually saved into the book document unlike bookinstances - hence the lines in the book detail controller:
book_instance: function(callback) {
BookInstance.find({ 'book': req.params.id })
.exec(callback);
},
Below I have shown what I have done. I could also have done this as objects in the controller but this is what I have now:
Book Controller:
exports.book_list = function (req, res, next) {
async.parallel({
books: function (callback) {
Book.find()
.exec(callback)
},
bookinstances: function (callback) {
BookInstance.find()
.exec(callback)
},
}, function (err, results) {
if (err) { return next(err); } // Error in API usage.
// Successful, so render.
res.render('book_list', { title: 'Book Detail', books: results.books,
bookinstances: results.bookinstances });
});
};
book_list.pug code:
extends layout
block content
h1= title
table.table
th Book
th BookInstance Count
th
//- above th is for buttons only (no title)
each book in books
- var instCount = 0
each bookinstance in bookinstances
if book._id.toString() === bookinstance.book.toString()
- instCount++
tr
td
a(href=book.url) #{book.title}
td #{instCount}
td
a.btn.btn-sm.btn-primary(href=book.url+'/update') Update
if !instCount
a.btn.btn-sm.btn-danger(href=book.url+'/delete') Delete
else
li There are no books.
What the page comes out as:
The problem was identified as me trying to use MongoDb like a relational database and not a document type. The solution to this problem is to use an array of the BookInstances in the Book document in the same way as genre:
var BookSchema = new Schema({
title: {type: String, required: true},
author: { type: Schema.ObjectId, ref: 'Author', required: true },
summary: {type: String, required: true},
isbn: {type: String, required: true},
genre: [{ type: Schema.ObjectId, ref: 'Genre' }],
bookInstances: [{ type: Schema.ObjectId, ref: 'BookInstance' }]
});
All the details can be kept in the BookInstance document still because the _id is all that is required in the Book document. Whenever a BookInstance is added it can be pushed onto the Book/ BookInstances array (this post helps: Push items into mongo array via mongoose). This does also mean that the BookInstance will need to be deleted (pulled) from the array as well as the document that contains its details.
Now the mongoose populate() can be used in the normal way.

How to insert data in to related mongoose Schemas?

I am trying to create an api endpoint in nodejs to insert data in mongodb. I have got two mongoose schemas which are related to each other that`s why i am relating these two schemas like below:
Posts schema:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const PostSchema = mongoose.Schema({
title: { type: String, trim: true, required: true},
description: { type:String, required: true },
created_at: { type: Date, default: Date.now },
author: {type: Schema.ObjectId, ref: 'Author', required: true},
});
const Post = module.exports = mongoose.model('Post', PostSchema);
Authors Schema:
const mongoose = require('mongoose');
const AuthorSchema = mongoose.Schema({
fullname: { type: String, trim: true, required: true},
address: { type: String, required: true },
phone: { type: Number, required: true },
email: { type: String, required: true },
created_at: { type: Date, default: Date.now }
});
const Author = module.exports = mongoose.model('Author', AuthorSchema);
Now i can easily insert data for authors schema
Authors.js:
router.post('/', (req, res, next) => {
let newAuthor = new Authors({
fullname: req.body.fullname,
address: req.body.address,
phone: req.body.phone,
email: req.body.email
});
newAuthor.save((err, user) => {
if(err) {
res.json({
success: false,
msg: "Failed to add author"
});
} else {
res.json({
success: true,
msg: "Author added successfully"
});
}
});
});
But for posts i am stuck in here
posts.js:
router.post('/', (req, res) => {
var newPost = new Posts({
title: req.body.title,
description: req.body.description,
author:
})
})
main problem is how to get author??
You can set author id in author field.
// you can set authorId in req.body and use that
router.post('/', (req, res) => {
var newPost = new Posts({
title: req.body.title,
description: req.body.description,
author: req.body.authorId
})
});
OR you can set author id in route path and use req.params.authorId
// for that route from ui call should be like
// var userId = 4654654545454
// $http.post('/'+userId)....
router.post('/:authorId', (req, res) => {
var newPost = new Posts({
title: req.body.title,
description: req.body.description,
author: req.params.authorId
})
});
On the page where you allow users to create posts, you need to pass the author's id along with the rest of the post details. Then you can simply refer to the author's id by whatever you chose to send it as (i.e. authorId).
If you are using a serializer that takes all the values from your form and nicely packages them, then insert a hidden input field that stores the author's id, so you can capture that as well. For example:
<input type="hidden" name="authorId" value={user._id} />
Otherwise, if you are manually packaging the form field values, then just add the author's id as another property in the response object. Not sure what you're doing to send the request but say you were using the axios library to send an ajax post to your endpoint you could do this to easily add the author to the response:
const title = document.getElementByNames("title")[0].value
const description = document.getElementByNames("description")[0].value
const author = document.getElementByNames("authorId")[0].value
axios.post("/posts", {title: title, description: description, author: authorId}).then(res => console.log(res))

Mongoose nested schemas

I'm creating an app where you log workouts and I'm having some problems with Mongoose.
I have two schemas, one for workouts and one for exercises. When the user adds a new exercise, I want it to be stored inside the workout, and I've been trying this in a bunch of ways.
For now, the exercises are saved in a different collection in my MongoDB (don't know if this is the best way to do it), and I thought that it should save the exercise inside the workout.exercises, but there is only the objectID. How do I resolve this? Have looked at the populate function, but can't figure out how to get it to work.
addExercises
export function addExercise(req, res) {
if (!req.body.exercise.title) {
res.status(403).end();
}
const newExercise = new Exercise(req.body.exercise);
// Let's sanitize inputs
newExercise.title = sanitizeHtml(newExercise.title);
newExercise.cuid = cuid();
newExercise.sets = [];
newExercise.save((err, saved) => {
if (err) res.status(500).send(err);
});
Workout
.findOneAndUpdate(
{cuid: req.body.exercise.workoutCUID},
{$push: {exercises: newExercise}},
{upsert: true, new: true},
function (err, data) {
if (err) console.log(err);
});
}
getExercises
export function getExercises(req, res) {
Workout.findOne({cuid: req.params.cuid}).exec((err, workout) => {
if (err) {
res.status(500).send(err);
}
console.log(workout);
let exercises = workout.exercises;
res.json({exercises});
});
}
Workout
import mongoose from "mongoose";
const Schema = mongoose.Schema;
var Exercise = require('./exercise');
const workoutSchema = new Schema({
title: {type: 'String', required: true},
cuid: {type: 'String', required: true},
slug: {type: 'String', required: true},
userID: {type: 'String', required: true},
exercises: [{ type: Schema.Types.ObjectId, ref: 'Exercise' }],
date: {type: 'Date', default: Date.now, required: true},
});
export default mongoose.model('Workout', workoutSchema);
Exercise
import mongoose from "mongoose";
const Schema = mongoose.Schema;
var Workout = require('./workout');
const exerciseSchema = new Schema({
title: {type: 'String', required: true},
cuid: {type: 'String', required: true},
workoutCUID: {type: 'String', required: true},
sets: {type: 'Array', "default": [], required: true}
});
export default mongoose.model('Exercise', exerciseSchema);
Based on your Workout schema, you declare the type of the Exercises field to be [{ type: Schema.Types.ObjectId, ref: 'Exercise' }]. This means that this field should be an array of Mongoose ObjectId's.
It appears that you are attempting to add the whole exercise object to the workout's exercises field, rather than just the ObjectId. Try modifying it this way:
const newExercise = new Exercise(req.body.exercise);
// Let's sanitize inputs
newExercise.title = sanitizeHtml(newExercise.title);
newExercise.cuid = cuid();
newExercise.sets = [];
newExercise.save((err, saved) => {
if (err) res.status(500).send(err);
// Nest the Workout update in here to ensure that the new exercise saved correctly before proceeding
Workout
.findOneAndUpdate(
{cuid: req.body.exercise.workoutCUID},
// push just the _id, not the whole object
{$push: {exercises: newExercise._id}},
{upsert: true, new: true},
function (err, data) {
if (err) console.log(err);
});
});
Now that you correctly have the ObjectId saved in the exercises field, .populate should work when you query the workout:
Workout.findById(id).populate("exercises").exec((err, workout) => {
// handle error and do stuff with the workout
})
Workout.findById(req.params.id).populate("exercises").exec((err,workout) =>{
res.status(200).json(workout);
})
It should work this way

How to implement partial document embedding in Mongoose?

I have a simple relation between topics and categories when topic belongs to a category.
So schema looks like this:
const CategorySchema = new mongoose.Schema({
name: String,
slug: String,
description: String
});
And topic
const TopicSchema = new mongoose.Schema({
category: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Category'
},
title: String,
slug: String,
body: String,
created: {type: Date, default: Date.now}
});
I want to implement particular embedding of category into topic
{
category: {
_id: ObjectId('abc'),
slug: 'catslug'
},
title: "Title",
slug: "topictitle",
...
}
It will help me avoid unnecessary population and obtain performance bonuses.
I don't want to embed whole document because I want to changes categories sometimes (it is a rare operation) and maintain references.
Hope this helps, done it in my own project to save some RTTs in common use cases. Make sure you're taking care of both copies on update.
parent.model.js:
const mongoose = require('mongoose');
const childEmbeddedSchema = new mongoose.Schema({
_id: {type: mongoose.Schema.Types.ObjectId, ref: 'Child', auto: false, required: true, index: true},
someFieldIWantEmbedded: {type: String}
});
const parentSchema = new mongoose.Schema({
child: { type: childEmbeddedSchema },
moreChildren: { type: [{type: childEmbeddedSchema }] }
});
module.exports = mongoose.model('Parent', parentSchema);
child.model.js:
const mongoose = require('mongoose');
const childSchema = new mongoose.Schema({
someFieldIWantEmbedded: {type: String},
someFieldIDontWantEmbedded: {type: Number},
anotherFieldIDontWantEmbedded: {type: Date}
});
module.exports = mongoose.model('Child', childSchema);
parent.controller.js:
const mongoose = require('mongoose');
const Parent = require('path/to/parent.model');
exports.getAll = (req, res, next) => {
const query = Parent.find();
// only populate if requested! if true, will replace entire sub-document with fetched one.
if (req.headers.populate === 'true') {
query.populate({
path: 'child._id',
select: `someFieldIWantEmbedded ${req.headers.select}`
});
query.populate({
path: 'moreChildren._id',
select: `someFieldIWantEmbedded ${req.headers.select}`
});
}
query.exec((err, results) => {
if (err) {
next(err);
} else {
res.status(200).json(results);
}
});
};

Inheriting Mongoose schemas

I wanted to make a base 'Entity Schema', and other model entities would inherit from it.
I did it, kinda, but then strange thing happened.
Those are my schemas:
AbstractEntitySchema
MessageSchema
UserSchema
RoomSchema
File: https://github.com/mihaelamj/nodechat/blob/master/models/db/mongo/schemas.js
But in MongoDB, they are all saved in the same document store: 'entity models' not separate ones, like Messages, Users..
Did I get what was supposed to happen, but not what I wanted, separate stores?
If so I will just make a basic JSON/object as entity and append the appropriate properties for each entity. Or is there a better way?
Thanks.
Discriminators are a schema inheritance mechanism. They enable you to have multiple models with overlapping schemas on top of the same underlying MongoDB collection. rather than different documents. It seems that you misunderstand the discriminators of mongoose. Here is one article could help you to catch it correctly.
Guide to mongoose discriminators
Here are some codes sample to meet your requirement, to save the derived schema as separated documents
function AbstractEntitySchema() {
//call super
Schema.apply(this, arguments);
//add
this.add({
entityName: {type: String, required: false},
timestamp: {type: Date, default: Date.now},
index: {type: Number, required: false},
objectID: {type: String},
id: {type: String}
});
};
util.inherits(AbstractEntitySchema, Schema);
//Message Schema
var MessageSchema = new AbstractEntitySchema();
MessageSchema.add({
text: {type: String, required: true},
author: {type: String, required: true},
type: {type: String, required: false}
});
//Room Schema
var RoomSchema = new AbstractEntitySchema();
RoomSchema.add({
name: {type: String, required: true},
author: {type: String, required: false},
messages : [MessageSchema],
});
var Message = mongoose.model('Message', MessageSchema);
var Room = mongoose.model('Room', RoomSchema);
// save data to Message and Room
var aMessage = new Message({
entityName: 'message',
text: 'Hello',
author: 'mmj',
type: 'article'
});
var aRoom = new Room({
entityName: 'room',
name: 'Room1',
author: 'mmj',
type: 'article'
});
aRoom.save(function(err, myRoom) {
if (err)
console.log(err);
else
console.log("room is saved");
});
aMessage.save(function(err) {
if (err)
console.log(err);
else
console.log('user is saved');
});
If you want multiple overlapping models with different MongoDB collections, then you use this approach:
function extendSchema (Schema, definition, options) {
return new mongoose.Schema(
Object.assign({}, Schema.obj, definition),
options
);
}
Example
const extendSchema = require('mongoose-extend-schema');
const UserSchema = new mongoose.Schema({
firstname: {type: String},
lastname: {type: String}
});
const ClientSchema = extendSchema(UserSchema, {
phone: {type: String, required: true}
});
You simply extend the original object the schema was created with and recreate a new schema on its basis. This is some sort of abstract schema which you inherit from.
Check this npm module: https://www.npmjs.com/package/mongoose-extend-schema
Since ES6 this works as well:
var ImageSchema: Schema = new Schema({
...CommonMetadataSchema.obj,
src: String,
description: String,
});

Resources