-2

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:

enter image description here

Cheesus Toast
  • 1,043
  • 2
  • 15
  • 21
  • I feel the need to offer constructive critism here. You keep talking about `BookInstance` and "relating" it to book. But no part of your question shows any structure of `BookInstance`, or probably more importantly shows us some data for a `Book` and an `BookInstance`. Or really importantly shows some sort of example output that could be achieved from said data related to what you are trying to achieve. You could loose 90% of the "background story" and focus on presenting the problem along with data and related code/structure to solve it. – Neil Lunn Mar 05 '19 at 11:14
  • I want a list of the BookInstances for each Book in the view. I will include the BookInstance Model then in the question. It is obviously not as obvious as what I thought it was. – Cheesus Toast Mar 05 '19 at 11:22

1 Answers1

0

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.

Cheesus Toast
  • 1,043
  • 2
  • 15
  • 21