0

I'm building a REST API in nodejs/express/mongodb/mongoose. I've started using MongoDB/Mongoose recently and I still have some doubts.

What I'm trying to achieve is to access a specific user bag (a user can have multiple bags) and also I want to be able to add to that bags participants/payers. (a user bag can have multiple participants/payers)

My mongoose user modal contains the rest of the schemas. I created a schema for each one because I believe it would be easier to find a given bag or participant directly because of the ObjectId (not sure if this is correct).

Mongoose Modal/Schemas:

const PayerSchema = new Schema({
  name: {
    type: String
  },
  amount: {
    type: Number
  }
});

const BagSchema = new Schema({
  name: {
    type: String
  },
  type: {
    type: String
  },
  payers: [PayerSchema]
});

const UserSchema = new Schema({
  name: {
    type: String,
    required: [true, 'User name field is required']
  },
  bags: [BagSchema]
});

module.exports = mongoose.model('User', UserSchema);

I was able to create the CRUD controller methods for a new user, but I still not sure on:

  1. Creating a new bag for a specific user (I was able to do this but not sure if it's the right way)
  2. Creating a new participant in a specific bag for a specific user. (addPayer method is wrong need help here)

Check out my controller user/bags/participants methods:

const User = require('../models/userModel');

getAllUserBags: (req, res, next) => {
    User.findById({ _id: req.params.id }).then((user) => {
      res.send(user.bags);
    })
    .catch(next);
  },
  getOneUserBag: (req, res, next) => {
    console.log(req.params.bagid);
    User.find({ 'bags._id': req.params.bagid}, {"bags.$" : 1}).then((obj) => {
      res.send(obj);
    })
    .catch(next);
  },
  createBag: (req, res, next) => {
    let bag = req.body.bag;
    User.findOneAndUpdate(
      {_id: req.body.id},
      {$push: {bags: bag}
    }).then(() => {
      //Unnecessary - just to return update user with new bag.
      User.findOne({_id: req.body.id}).then((user) => {
        res.send(user);
      })
    }).catch(next);
  },
  addPayer: (req, res, next) => {
    let payer = req.body.payer;
    User.find(
      {'bags._id': req.params.bagid},
      {"bags.$" : 1},
      {$push: {payers: payer}
    }).then((obj) => {
      console.log(obj);
      //Unnecessary - just to return update user with new bag.
      // User.findOne({_id: req.body.id}).then((user) => {
      //   res.send(user);
      // })
    }).catch(next);
  } 

Thanks for the help

andybeli
  • 836
  • 7
  • 14

1 Answers1

1

Base on what we discuss, your User schema is good enough for your requirements, as long as making sure that one User document does not exceed the 16MB limit of MongoDB document.


Creating a new bag for a specific user (I was able to do this but not sure if it's the right way)

Yours is fine. However, there are some improvements:

createBag: (req, res, next) => {
  User.findByIdAndUpdate(req.body.id, {
      $push: { bags: req.body.bag }
    }, {
      new: true // this will make the query getting the updated document
    })
    .then(user => {
      res.json(user);
    })
    .catch(next);
})

Creating a new participant in a specific bag for a specific user. (addPayer method is wrong need help here)

Since you decided to nest the 'bags', the bag.id might be duplicated among User documents. See this to understand the possibility. Thus, I recommend using an userId along with bagId:

getOneUserBag: (req, res, next) => {
  User.findOne({
      _id: req.params.userId,
      bags._id: req.params.bagId
    })
    .then(user => {
      if (!user) res.status(404).end();

      let bag = user.bags.id(req.params.bagId);

      res.json(bag);
    })
    .catch(next);
}

addPayer: (req, res, next) => {
  User.findOneAndUpdate({
      _id: req.params.userId,
      bags: $elemMatch: {
        _id: req.params.bagId
      }
    }, {
      $push: { 'bags.$.payers': req.body.payer } // Use 'positional $' operator along with $elemMatch in the query to update only the matched bag
    }, {
      new: true // Do not forget the 'new' options to get the updated document
    })
    .then(user => {
      if (!user) res.status(404).end();

      res.json(user);
    })
    .catch(next);
}

and in the router

router.get('/users/:userId/bags/:bagId', getOneUserBag);
router.post('/users/:userId/bags/:bagId/payers', addPayer);

In the getAllUserBags(), you use the wrong syntax for User.findById():

getAllUserBags: (req, res, next) => {
  User.findById(req.params.id) // Not { _id: req.params.id }
    .then((user) => {
      res.json(user.bags);
    })
    .catch(next);
}
willie17
  • 901
  • 8
  • 12
  • Thanks for the tips and explanations, It's working perfectly now. I still need to fully understand why I could have duplicate IDs. I feel like you are not sure if I should go for a nested approach. A reference approach is the right way just in case sub documents get too big? – andybeli Apr 06 '18 at 14:50
  • You must use reference approach when you meet any of these requirements: 1. A large amount of item will be stored in an nested array; 2. You need to query for somethings like "get all the bags", "get bag by bagId",... That's why I asked you 2 questions above. But since you are not sure whether your `User` will have a lots of bags so that it will exceed the 16MB limit or not, I am not sure whether you should use the nested approach or reference approach neither. – willie17 Apr 06 '18 at 16:40