mhatch
mhatch

Reputation: 4605

Mongoose refs not populating json

I am trying to learn MongoDB with a simple Express API. The API works well for fetching individual collections, but when I try to combine multiple collections into one schema using mongoose ref, I get a json response containing only the _id field.

Here are two Schemas in Mongoose:

var LinkSchema = require('./link.js');

var MyInfoSchema = new Schema({
    name: String,
    title: String,
    links: [ LinkSchema ]
});

var AboutSchema = new Schema({
    img: String,
    description: String
});

These work fine in endpoints calling them separately.

I want to have an endpoint that combines these two collections into a single json response. So, I made this model using ref

var MyInfoSchema = require('./myinfo.js');
var AboutSchema = require('./about.js');

var AllDataSchema = new Schema({
    myinfo: {
        type: mongoose.Schema.ObjectId,
        ref: 'MyInfoSchema'
    },
    about: { 
        type: mongoose.Schema.ObjectId,
        ref: 'AboutSchema'
    }
});

My Express API route looks like this

var router = express.Router();

router.route('/')
    .get(function(request, response){
        var data = new AllData();

        // add myinfo
        MyInfo.findOne(function(error, info){
            data.myinfo = info;
            console.log(data); // this gives an id
        });

        // add about
        About.findOne(function(error, about){
            data.about = about;
        });

        console.log(data); // this prints nothing
        response.json(data);
    });

However, the json response I get is only an _id

{
    "_id": "59bfed783f0ba490a34c9ce9"
}

Why is the response not populated?

I tried logging to the console. When I put the log inside the findOne(), I get an object like this

{ _id: 59bfed783f0ba490a34c9ce9,
  myinfo: 59b1ad02d551330000000002 }

When I place the log statement outside the find function and before response.json(), I get nothing. Why is that? data should still be in scope, no?

Edit:

As suggested by @Mikey in the answers, I've refactored to use Promises. This is getting closer, but there are 2 problems. 1. The keys for each collection are missing, and 2. It's returning an array for the root structure instead of an object.

Here is the update

.get(function(request, response){
    var data = new AllData();

    var p1 = MyInfo.findOne().exec(),
        p2 = Navigation.findOne().exec(),
        p3 = About.findOne().exec(),
        p4 = Contact.findOne().exec();

    Promise.all([p1, p2, p3, p4]).then(function(data) {
        data.myinfo = data[0];
        data.navigation = data[1];
        data.about = data[2];
        data.contact = data[3];
        response.json(data);
    });
});

and this is the response

[
    {
        "_id": "59b1ad02d551330000000002",
        "title": "Television Tester",
        "name": "Walter P. K.",
        "__v": 0,
        "links": [
            {
                "name": "...",
                "url": "...",
                "_id": "59b1ad02d551330000000004"
            },
            {
                "name": "more...",
                "url": "more...",
                "_id": "59b1ad02d551330000000003"
            }
        ]
    },
    {
        "_id": "59b2e606103ace0000000003",
        "__v": 1,
        "links": [
            {
                "name": "About",
                "url": "#",
                "_id": "59b2e62d103ace0000000009"
            },
            {
                "name": "Stuff",
                "url": "#",
                "_id": "59b2e62d103ace0000000008"
            },
            {
                "name": "Contact",
                "url": "#",
                "_id": "59b2e62d103ace0000000007"
            }
        ]
    }
    ...
]

It should be something like this

{
  myinfo: {}.
  navigation: {},
  about: {},
  contact: {}
}

Upvotes: 0

Views: 909

Answers (3)

mhatch
mhatch

Reputation: 4605

I was able to solve the problem with the helpful suggestions of the other answerers. Thank you for pointing out the async nature of find() and promises. What I needed to make it work was to use a plain JS object for the response structure (instead of a Mongoose Schema), and some variable name cleanup. In the answer by Mikey, and my edit to the question, the response and console.log array being returned was actually the promises array (i.e. the values from p1, p2, p3, etc), not the data structure I wanted to return.

Here is the working code:

router.route('/')
    .get(function(request, response){

        var p1 = MyInfo.findOne().exec(),
            p2 = Navigation.findOne().exec(),
            p3 = About.findOne().exec(),
            p4 = Contact.findOne().exec();

        Promise.all([p1, p2, p3, p4]).then(function(docs) {
            var data = {};
            console.log(docs);
            data.myinfo = docs[0];
            data.navigation = docs[1];
            data.about = docs[2];
            data.contact = docs[3];
            response.json(data);
        });
    });

Upvotes: 1

Steve Holgado
Steve Holgado

Reputation: 12071

findOne is async so your console log runs before the findOne callbacks.

You might be looking for something more like this, which will query your 'AllData' collection and replace the stored id with the referenced documents for 'myinfo' and 'about':

AllData.find()
  .populate('myinfo about')
  .exec(function(error, docs) {
    console.log(docs)
    response.json(docs)
  });

Upvotes: 1

Mikey
Mikey

Reputation: 6766

Mongoose operations are asynchronous. You need to wait for all the operations to be done before sending a response.

You can either nest your operations

router.route('/').get(function(request, response){
    var data = new AllData();

    MyInfo.findOne(function(err, info) {
        data.myinfo = info;
        About.findOne(function (err, about){
            data.about = about;
            response.json(data);
        });
    });
});

or use promises (I think it's like this)

router.route('/').get(function(request, response){
    var data = new AllData();

    var p1 = MyInfo.findOne().exec(),
        p2 = About.findOne().exec();

    Promise.all([p1, p2]).then(function(data) {
        data.myinfo = data[0];
        data.about = data[1];
        response.json(data);
    });
});

Upvotes: 1

Related Questions