0

EDIT: It's possible the problem is an issue with pathing. my current query looks like this:

router.route('/projects/:project_id/techDetails')
  .get(function(req, res) {
    Project.findById(req.params.project_Id, function(err, project) {
        if (err)
            return res.send(err);
        res.json(project);
        console.log('get success (project techDetails)');
    });
});

this returns null. even though it's identical to a working line of code in every way except for the addition of `/techDetails' to the route.

original question:

I'm building a MEAN stack app with express and mongo. I can't figure out how to route to nested documents properly.

here is my Project schema:

const ProjectSchema = new Schema({
  idnumber: { type: Number, required: true },
  customername: String,
  projectdetails: String,
  jobaddress: String,
  techDetails: [{
    scope: String,
    edgedetail: String,
    lamination: String,
    stonecolour: String,
    slabnumber: String,
    slabsupplier: String,
    purchaseordernum: String,
    splashbacks: String,
    apron: String,
    hotplate: String,
    sink: String,
    sinkdetails: String,
    tappos: String
  }],
  sitecontactname: String,
  sitecontactnum: String,
  specialreq: String,
  install_date: String,
  created_on: { type: Date, default: Date.now },
  created_by: { type: String, default: 'SYSTEM' },
  active: { type: Boolean, default: true },
  flagged: { type: Boolean, default: false },
});

I can successfully route to /projects with GET and POST, and /projects/:project_id with GET, PUT and DEL.

using the PUT route and a project's _ID i can push new entries to a project's techDetails subdoc array. the resulting JSON data looks like this:

{
"_id": "59e577e011a3f512b482ef13",
"idnumber": 52,
"install_date": "10/20/2017",
"specialreq": "some...",
"sitecontactnum": "987654321",
"sitecontactname": "bill",
"jobaddress": "123 st",
"projectdetails": "some stuff",
"customername": "B Builders",
"__v": 16,
"flagged": false,
"active": true,
"created_by": "SYSTEM",
"created_on": "2017-10-17T03:24:16.423Z",
"techDetails": [
    {
        "scope": "Howitzer",
        "edgedetail": "12mm",
        "lamination": "No",
        "stonecolour": "Urban™",
        "slabnumber": "1",
        "slabsupplier": "Caesarstone",
        "purchaseordernum": "no",
        "splashbacks": "No",
        "apron": "No",
        "hotplate": "N/A",
        "sink": "N/A",
        "sinkdetails": "no",
        "tappos": "no",
        "_id": "59e577e011a3f512b482ef14"
    },
    {
        "scope": "kitchen",
        "edgedetail": "12mm",
        "lamination": "etc",
        "_id": "59e7da445d9d7e109c18f38b"
    },
    {
        "scope": "Vanity",
        "edgedetail": "12mm",
        "lamination": "No",
        "stonecolour": "Linen™",
        "slabnumber": "1",
        "slabsupplier": "Caesarstone",
        "purchaseordernum": "1",
        "splashbacks": "No",
        "apron": "No",
        "hotplate": "N/A",
        "sink": "N/A",
        "sinkdetails": "no",
        "tappos": "woo",
        "_id": "59e81e3324fb750fb46f8248"
    }//, more entries omitted for brevity
  ]
}

as you can see everything so far is working as expected. However now i need to edit and delete individual entries in this techDetails array. i'd also like to route to them directly using projects/:project_id/techDetails and projects/:project_id/techDetails/:techdetails_id.

From what i can see there are two approaches to this. either i can:

A) use a new routing file for the techDetails that uses mergeParams. this is the approach i'm trying currently, however I can't figure out how to complete the .find to return all techDetails, since i can only use the Project model schema and i'm unsure how to access the sub docs.

an excerpt from my routes.js:

const techDetails = require('./techDetails');
//other routes here

//see techdetails file
router.use('/projects/:project_id/techdetails', techDetails);

//here lies an earlier, failed attempt
/* router.route('/projects/:project_id/techdetails/:techDetails_id')
.get(function(req, res) {
    Project.findById(req.params.project_id.techDetails_id, function(err, 
project) {
        if (err)
            return res.send(err);
        res.json(project.techDetails);
        console.log('get success (techDetails)');
    });
  })
; */

and my techdetails.js:

const express = require('express');
const Project = require('./models/project');
const router = express.Router({mergeParams: true});

router.get('/', function (req, res, next) {
/*  Project.find(function(err, techDetails) {
    if (err)
        return res.send(err);
    res.json(techDetails);
    console.log('get success (all items)');
  }); */
  res.send('itemroutes ' + req.params);
})

router.get('/:techDetails_id', function (req, res, next) {
  res.send('itemroutes ' + req.params._id)
})

module.exports = router

I can successfully check that the routes work with Postman, both will receive the response. now the problem is, instead of res.send i want to use res.json with Project.find (or similar) to get the techDetails.

however there is also another option:

B) put the techDetails document into it's own schema and then populate an array of IDs inside projects.

however this seems more complex so i'd rather avoid having to do so if i can.

any thoughts and suggestions welcome. let me know if more of my code is needed.

KuroKyo
  • 41
  • 12

2 Answers2

0

In this particular case I would put techDetails in a separate schema:

const ProjectSchema = new Schema({
  idnumber: { type: Number, required: true },
  customername: String,
  projectdetails: String,
  jobaddress: String,
  techDetails: [techDetailsSchema],
  sitecontactname: String,
  sitecontactnum: String,
  specialreq: String,
  install_date: String,
  created_on: { type: Date, default: Date.now },
  created_by: { type: String, default: 'SYSTEM' },
  active: { type: Boolean, default: true },
  flagged: { type: Boolean, default: false },
});

Don't register the techDetails schema with mongoose.model as it is a subdocument. Put it in a separate file and require it in the project model file (const techDetailsSchema = require('./techDetails.model');).

I would create the controller functions like this:

Getting with GET (all):

module.exports.techDetailsGetAll = function (req, res) {   
  const projectId = req.params.projectId;

  Project
    .findById(projectId)
    .select('techDetails')
    .exec(function (err, project) {
      let response = { };

      if (err) {
        response = responseDueToError(err);
      } else if (!project) {
        response = responseDueToNotFound();
      } else {
        response.status = HttpStatus.OK;
        response.message = project.techDetails;
      }

      res.status(response.status).json(response.message);
    });
}

Getting with GET (one):

module.exports.techDetailsGetOne = function (req, res) {
  const projectId = req.params.projectId;
  const techDetailId = req.params.techDetailId;

  Project
    .findById(projectId)
    .select('techDetails')
    .exec(function (err, project) {
      let response = { };

      if (err) {
        response = responseDueToError(err);
      } else if (!project) {
        response = responseDueToNotFound();
      } else {
        let techDetails = project.techDetails.id(techDetailId);

        if (techDetails === null) {
          response = responseDueToNotFound();
        } else {
          response.status = HttpStatus.OK;
          response.message = techDetails;
        }
      }

      res.status(response.status).json(response.message);
    });
}

For adding with POST:

module.exports.techDetailsAddOne = function (req, res) {
  const projectId = req.params.projectId;

  let newTechDetails = getTechDetailsFromBody(req.body);

  Project
  .findByIdAndUpdate(projectId,
    { '$push': { 'techDetails': newTechDetails } },
    {
      'new': true,
      'runValidators': true
    },
    function (err, project) {
      let response = { };

      if (err) {
        response = responseDueToError(err);
      } else if (!project) {
        response = responseDueToNotFound();
      } else {
        response.status = HttpStatus.CREATED;
        response.message = project.techDetails;  // for example
      }

      res.status(response.status).json(response.message);
    });
}

For updating with PUT

module.exports.techDetailsUpdateOne = function (req, res) {
  const projectId = req.params.projectId;
  const techDetailId = req.params.techDetailId;

  let theseTechDetails = getTechDetailsFromBody(req.body);
  theseTechDetails._id = techDetailId;  // can be skipped if body contains id

  Project.findOneAndUpdate(
    { '_id': projectId, 'techDetails._id': techDetailId },
    { '$set': { 'techDetails.$': theseTechDetails } },
    {
      'new': true,
      'runValidators': true
    },
    function (err, project) {
      let response = { };

      if (err) {
        response = responseDueToError(err);
        res.status(response.status).json(response.message);
      } else if (!project) {
        response = responseDueToNotFound();
        res.status(response.status).json(response.message);
      } else {
        project.save(function (err) {
          if (err) {
            response = responseDueToError(err);
          } else {
            response.status = HttpStatus.NO_CONTENT;
          }

          res.status(response.status).json(response.message);
        })
      }
    });
}

And deleting with DELETE:

module.exports.techDetailsDeleteOne = function (req, res) {
  const projectId = req.params.projectId;
  const techDetailId = req.params.techDetailId;

  Project
    .findById(projectId)
    .select('techDetails')
    .exec(function (err, project) {
      let response = { }

      if (err) {
        response = responseDueToError(err);
        res.status(response.status).json(response.message);
      } else if (!project) {
        response = responseDueToNotFound();
        res.status(response.status).json(response.message);
      } else {
        let techDetail = project.techDetails.id(techDetailId);

        if (techDetail !== null) {
          project.techDetails.pull({ '_id': techDetailId });

          project.save(function (err) {
            if (err) {
              response = responseDueToError(err);
            } else {
              response.status = HttpStatus.NO_CONTENT;
            }

            res.status(response.status).json(response.message);
          })
        } else {
          response = responseDueToNotFound();
          res.status(response.status).json(response.message);
        }
      }
    });
}

And finally routing like this:

router
    .route('/projects')
    .get(ctrlProjects.projectsGetAll)
    .post(ctrlProjects.projectsAddOne);

router
    .route('/projects/:projectId')
    .get(ctrlProjects.projectsGetOne)
    .put(ctrlProjects.projectsUpdateOne)
    .delete(ctrlProjects.projectsDeleteOne);

router
    .route('/projects/:projectId/techDetails')
    .get(ctrlTechDetails.techDetailsGetAll)
    .post(ctrlTechDetails.techDetailsAddOne);

router
    .route('/projects/:projectId/techDetails/:techDetailId')
    .get(ctrlTechDetails.techDetailsGetOne)
    .put(ctrlTechDetails.techDetailsUpdateOne)
    .delete(ctrlTechDetails.techDetailsDeleteOne);

This is what I prefer when I'm constantly updating the subdocument independently of the rest of the document. It doesn't create a separate collection, so no need for populate.

EDIT: This answer goes more into detail on whether you should use embedding or referencing. My answer uses embedding.

Mika Sundland
  • 18,120
  • 16
  • 38
  • 50
  • So you can define a subdocument as a schema without it being a model? thanks, that makes option B way easier. i prefer to avoid having separate controllers, but as my program grows in size it might be a good idea to start using them. I'll let you know if i can get it to work! thanks for the quick answer! – KuroKyo Oct 20 '17 at 01:39
  • `response.message = project.techDetails;` returns null and `techDetails` by itself returns undefined... this is pretty much the same issue i was having before. I can't seem to access anything nested. – KuroKyo Oct 20 '17 at 05:12
  • Registering the schema with mongoose.model will put techDetails in a separate collection (see [here](https://stackoverflow.com/questions/21142524/mongodb-mongoose-how-to-find-subdocument-in-found-document) for more info). If you plan to have a massive amount of techDetails per project then that might be what you want, as the techDetails array will make the project quite big. Or if you share techDetails bewteen projects. Otherwise I'd go for not having a separate collection. Max BSON documents size is 16 MB. – Mika Sundland Oct 20 '17 at 10:11
  • As for project.techDetails returning null: can you put a `console.log(project)` right after `exec(function(err, project) {` and see what it prints? You can also remove the select statement for now to return the entire project. If you've added the schema after the data was put into the project then it might be easier to drop the collection completely and then add the data back again. Returning null means the DB didn't find any subdocument. You might want to use `response.message = project.techDetails || [];` to return an empty array when not finding any techDetails. – Mika Sundland Oct 20 '17 at 10:17
  • Ah, could be that. not sure why I didn't think of that, i'm sure i must've gone over it in my head while i was making the changes. I'll try with all new data. – KuroKyo Oct 20 '17 at 23:56
  • Ok, sorry for the long wait! I haven't been at work for awhile due to reasons. So I tried it with an extra parameter in the findById field, which after reading the mongoose api documentation would indicate it is a select field, which narrows down the code somewhat, however it was still crashing my app with `techDetails of null`. so I changed the response to just project, and as suspected, it returns null. so i'm thinking it is having difficulty getting the _ID from the path. I recall seeing some solutions out there using split pathing so i will look into those. – KuroKyo Nov 01 '17 at 03:05
  • Thanks for the thorough answer, you had the gist of what I needed and helped me on the right path. obviously I can't plagiarize your solutions haha but I was able to work out what i needed using your solution as an example. Other than using a different structure, i also used `findOneAndUpdate` along with `pull` for the **delete** route, because of the way my app handles the subdoc array. Have yet to test for any major difference. – KuroKyo Nov 02 '17 at 23:39
0

So, the solution i came to was a combo of A) and B). I used a separate routing file and put ({mergeParams: true}) in the router declaration, and i created a separate file for the techDetails nested model, without declaring it. However I don't believe either of these actually made any significance... but anyway.

the working code i ended up with was, in my routes:

router.use('/projects/:project_id/techDetails', TechDetails);

and in techDetails.js:

const router = express.Router({mergeParams: true});

router.route('/')
  .get(function(req, res) {
    Project.findById(req.params.project_id,
      'techDetails', function(err, project) {
        if (err)
            return res.send(err);
        res.json(project);
        console.log('get success (project techDetails)');
    });
});

What's different about it? namely, the 'techDetails', parameter in the Project.findById line. According to the mongoose API this acts as a select statement. The only other major difference is I fixed a typo in my original code ( project_id was written project_Id. dubious... ). I probably would have noticed this if i was using VS or something instead of notepad++, but it is my preferred coding arena.

It may be possible to return res.json(project.techDetails) and remove the 'techDetails', select parameter, but I likely won't test this.

Edit: Turns out migrating techDetails to a separate file meant they no longer generated with objectIds, which is crucial for PUT and DEL. I might've been able to work around them with a simple pair of curly braces inside the array declaration, but I didn't think of that until after i re-migrated it back to the project schema...

KuroKyo
  • 41
  • 12