Search code examples
javascriptnode.jsmongoosemongoose-plugins

this.find(...) never reaches callback within static method


I am working on a module which adds Friendship-based relationships to a Schema.

I'm basically trying to do what this guy is trying to do (which, AFAIK, should work--which is discouraging)

Why is find(...) in FriendshipSchema.statics.getFriends never reaching its callback?

EDIT - Please allow me to explain the expected execution flow...

inside accounts.js:

  1. requires the 'friends-of-friends' module (loads friends-of-friends/index.js) which
    1. requires friends-of-friends/friendship.js which exports a function that creates FriendshipSchema, adds static methods, returns Friendship Model.
    2. requires friends-of-friends/plugin.js which exports the mongoose plugin that adds static and instance methods to `AccountSchema.
  2. uses FriendsOfFriends.plugin (see friends-of-friends/index.js) to plug-in the functionality from friends-of-friends/plugin.js
  3. defines AccountSchema.statics.search which calls this.getFriends.
    Since this refers to the Account model once it is compiled, and since the plugin added schema.statics.getFriends, calling this.getFriends within AccountSchema.statics.search will call schema.statics.getFriends as defined in friends-of-friends/plugin.js, which will call Friendship.getFriends (defined by FriendshipSchema.statics.getFriends in friends-of-friends/friendship.js) which calls this.find(...) which should translate to Friendship.find(...)`
  4. after retrieving an account document, I call account.search('foo', function (...) {...});, but as you can see in FriendshipSchema.statics.getFriends, the find method executes, but its callback is never invoked and the program hangs :(

I don't get any errors, so I know this is a logic problem, but I'm not sure why things are getting hung up where they are...

EDIT - see my answer below, I also needed to compile the models before I could call find on them.

account.js

var mongoose = require('mongoose'),
    passportLocalMongoose = require('passport-local-mongoose');

var FriendsOfFriends = require('friends-of-friends')();

// define the AccountSchema
// username, password, etc are added by passportLocalMongoose plugin
var AccountSchema = new mongoose.Schema({
    created:        { type: Date,       default:    Date.now                    },
    profile: {
        displayName:    { type: String,     required:   true,       unique : true,  index: true     },
        firstName:      { type: String,     required:   true,       trim: true,     index: true     }, 
        lastName:       { type: String,     required:   true,       trim: true,     index: true     }, 
    }
});

// plugin the FriendsOfFriends plugin to incorporate relationships and privacy
AccountSchema.plugin(FriendsOfFriends.plugin, FriendsOfFriends.options);

AccountSchema.statics.search = function (userId, term, done) {
    debug('search')

    var results = {
            friends: [],
            friendsOfFriends: [],
            nonFriends: []
        },
        self=this;

    this.getFriends(userId, function (err, friends) {

       // never reaches this callback!

    });

};

AccountSchema.methods.search = function (term, done) {
    debug('method:search')
    AccountSchema.statics.search(this._id, term, done);
};

module.exports = mongoose.model('Account', AccountSchema);

friends-of-friends/index.js

/**
 * @author  Jeff Harris
 * @ignore
 */

var debug = require('debug')('friends-of-friends');
    friendship = require('./friendship'),
    plugin = require('./plugin'),
    privacy = require('./privacy'),
    relationships = require('./relationships'),
    utils = require('techjeffharris-utils');

module.exports = function FriendsOfFriends(options) {

    if (!(this instanceof FriendsOfFriends)) {
      return new FriendsOfFriends(options);
    } 

    var defaults = {
        accountName:    'Account',
        friendshipName: 'Friendship', 
        privacyDefault: privacy.values.NOBODY
    };

    this.options = utils.extend(defaults, options);

    /**
     * The Friendship model
     * @type {Object}
     * @see  [friendship]{@link module:friendship}
     */
    this.friendship = friendship(this.options);

    /**
     * mongoose plugin
     * @type {Function}
     * @see  [plugin]{@link module:plugin}
     */
    this.plugin = plugin;

    debug('this.friendship', this.friendship);

};

friends-of-friends/friendship.js

var debug = require('debug')('friends-of-friends:friendship'),
    mongoose = require('mongoose'),
    privacy = require('./privacy'),
    relationships = require('./relationships'),
    utils = require('techjeffharris-utils');

module.exports = function friendshipInit(options) {

    var defaults = {
        accountName:    'Account',
        friendshipName: 'Friendship',
        privacyDefault: privacy.values.NOBODY
    };

    options = utils.extend(defaults, options);

    debug('options', options);

    var ObjectId = mongoose.Schema.Types.ObjectId;

    var FriendshipSchema = new mongoose.Schema({
        requester: { type: ObjectId, ref: options.accountName, required: true, index: true },
        requested: { type: ObjectId, ref: options.accountName, required: true, index: true },
        status: { type: String, default: 'Pending', index: true},
        dateSent: { type: Date, default: Date.now, index: true },
        dateAccepted: { type: Date, required: false, index: true }
    });

    ...

    FriendshipSchema.statics.getFriends = function (accountId, done) {
        debug('getFriends')

        var model = mongoose.model(options.friendshipName, schema),
            friendIds = [];

        var conditions = { 
            '$or': [
                { requester: accountId },
                { requested: accountId }
            ],
            status: 'Accepted'
        };

        debug('conditions', conditions);

        model.find(conditions, function (err, friendships) {
            debug('this callback is never reached!');

            if (err) {
                done(err);
            } else { 
                debug('friendships', friendships);

                friendships.forEach(function (friendship) {
                    debug('friendship', friendship);

                    if (accountId.equals(friendship.requester)) {
                        friendIds.push(friendship.requested);
                    } else {
                        friendIds.push(friendship.requester);
                    }

                });

                debug('friendIds', friendIds);

                done(null, friendIds);
            }

        });

        debug('though the find operation is executed...');
    };

    ...

    return mongoose.model(options.friendshipName, FriendshipSchema);
};

friends-of-friends/plugin.js

var debug = require('debug')('friends-of-friends:plugin'),
    mongoose = require('mongoose'),
    privacy = require('./privacy'),
    relationships = require('./relationships'),
    utils = require('techjeffharris-utils');

module.exports = function friendshipPlugin (schema, options) {

    var defaults = {
        accountName:    'Account',
        friendshipName: 'Friendship',
        privacyDefault: privacy.values.NOBODY
    };

    options = utils.extend(defaults, options);

    var Friendship = mongoose.model(options.friendshipName);

    ...

    schema.statics.getFriends = function (accountId, done) {
        debug('getFriends')

        var model = mongoose.model(options.accountName, schema);

        var select = '_id created email privacy profile';

        Friendship.getFriends(accountId, function (err, friendIds) {
            if (err) {
                done(err);
            } else {
                model.find({ '_id' : { '$in': friendIds } }, select, done);
            }
        });
    };

    ...

    schema.methods.getFriends = function (done) {
        schema.statics.getFriends(this._id, done);
    };
};

Solution

  • The issue was related to which instance of mongoose was being required.

    Within my main app, I was requiring mongoose from app/node_modules/mongoose whereas my friends-of-friends module--having listed mongoose as a dependency in package.json--was requiring mongoose from app/node_modules/friends-of-friends/node_modules/mongoose, which created two separate mongoose instances, which made things not work.

    I removed mongoose as a dependency, removed the nested node_modules folder, and vioala, it works, again :)

    should have RTFM

    app/
    |   lib/
    |   node_modules/ 
    |   |   mongoose/             <-- main app required here
    |   |   friends-of-friends/
    |   |   |   node_modules/     <-- deleted; mongoose was only dep
    |   |   |   |   mongoose/     <-- friends-of-friends module required here
    |   server.js