From 0a40c5697305fc626685b65faf3d09de4c0c473d Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Mon, 4 Apr 2016 23:18:49 +0200 Subject: [PATCH] api v3 adapt v2: correctly import dependencies, port create, get, list groups --- test/api/v2/groups/GET-groups.test.js | 10 +- test/api/v2/groups/GET-groups_id.test.js | 2 +- test/api/v2/groups/POST-groups.test.js | 2 +- test/api/v2/user/DELETE-user.test.js | 18 +- .../v3/integration/user/DELETE-user.test.js | 4 +- .../api-integration/v2/object-generators.js | 14 +- website/src/controllers/api-v2/auth.js | 9 +- website/src/controllers/api-v2/challenges.js | 12 +- website/src/controllers/api-v2/coupon.js | 4 +- website/src/controllers/api-v2/dataexport.js | 4 +- website/src/controllers/api-v2/groups.js | 158 ++++++++++-------- website/src/controllers/api-v2/hall.js | 8 +- website/src/controllers/api-v2/members.js | 15 +- .../src/controllers/api-v2/unsubscription.js | 8 +- website/src/controllers/api-v2/user.js | 53 +++--- website/src/controllers/pages.js | 1 + website/src/models/group.js | 59 ++++++- 17 files changed, 258 insertions(+), 123 deletions(-) diff --git a/test/api/v2/groups/GET-groups.test.js b/test/api/v2/groups/GET-groups.test.js index 2da38e3664..e1b7635356 100644 --- a/test/api/v2/groups/GET-groups.test.js +++ b/test/api/v2/groups/GET-groups.test.js @@ -4,11 +4,11 @@ import { resetHabiticaDB, } from '../../../helpers/api-integration/v2'; -xdescribe('GET /groups', () => { +describe('GET /groups', () => { const NUMBER_OF_PUBLIC_GUILDS = 3; - const NUMBER_OF_USERS_GUILDS = 2; let user; + let leader; before(async () => { // Set up a world with a mixture of public and private guilds @@ -16,7 +16,7 @@ xdescribe('GET /groups', () => { await resetHabiticaDB(); user = await generateUser(); - let leader = await generateUser({ balance: 10 }); + leader = await generateUser({ balance: 10 }); await generateGroup(leader, { name: 'public guild - is member', @@ -90,8 +90,8 @@ xdescribe('GET /groups', () => { context('guilds passed in as query', () => { it('returns all guilds user is a part of ', async () => { - await expect(user.get('/groups', null, {type: 'guilds'})) - .to.eventually.have.a.lengthOf(NUMBER_OF_USERS_GUILDS); + await expect(leader.get('/groups', null, {type: 'guilds'})) + .to.eventually.have.a.lengthOf(4); }); }); }); diff --git a/test/api/v2/groups/GET-groups_id.test.js b/test/api/v2/groups/GET-groups_id.test.js index 854a7a2681..1de69da586 100644 --- a/test/api/v2/groups/GET-groups_id.test.js +++ b/test/api/v2/groups/GET-groups_id.test.js @@ -8,7 +8,7 @@ import { each, } from 'lodash'; -xdescribe('GET /groups/:id', () => { +describe('GET /groups/:id', () => { let typesOfGroups = {}; typesOfGroups['public guild'] = { type: 'guild', privacy: 'public' }; typesOfGroups['private guild'] = { type: 'guild', privacy: 'private' }; diff --git a/test/api/v2/groups/POST-groups.test.js b/test/api/v2/groups/POST-groups.test.js index 4c9c2c41a4..8da165aaf7 100644 --- a/test/api/v2/groups/POST-groups.test.js +++ b/test/api/v2/groups/POST-groups.test.js @@ -4,7 +4,7 @@ import { translate as t, } from '../../../helpers/api-integration/v2'; -xdescribe('POST /groups', () => { +describe('POST /groups', () => { context('All groups', () => { let leader; diff --git a/test/api/v2/user/DELETE-user.test.js b/test/api/v2/user/DELETE-user.test.js index 17517cef21..1ba8dcfd9c 100644 --- a/test/api/v2/user/DELETE-user.test.js +++ b/test/api/v2/user/DELETE-user.test.js @@ -4,7 +4,11 @@ import { generateGroup, generateUser, } from '../../../helpers/api-integration/v2'; -import { find } from 'lodash'; +import { + find, + map, +} from 'lodash'; +import Q from 'q'; xdescribe('DELETE /user', () => { let user; @@ -19,6 +23,18 @@ xdescribe('DELETE /user', () => { })).to.eventually.eql(false); }); + it('deletes the user\'s tasks', async () => { + // gets the user's todos ids + let ids = user.todos.map(todo => todo._id); + expect(ids.length).to.be.above(0); // make sure the user has some task to delete + + await user.del('/user'); + + await Q.all(map(ids, id => { + return expect(checkExistence('tasks', id)).to.eventually.eql(false); + })); + }); + context('user has active subscription', () => { it('does not delete account'); }); diff --git a/test/api/v3/integration/user/DELETE-user.test.js b/test/api/v3/integration/user/DELETE-user.test.js index f954903857..6a631676e8 100644 --- a/test/api/v3/integration/user/DELETE-user.test.js +++ b/test/api/v3/integration/user/DELETE-user.test.js @@ -42,7 +42,7 @@ describe('DELETE /user', () => { }); }); - it('deletes the user', async () => { + it('deletes the user\'s tasks', async () => { // gets the user's tasks ids let ids = []; each(user.tasksOrder, (idsForOrder) => { @@ -60,7 +60,7 @@ describe('DELETE /user', () => { })); }); - it('delete the user\'s tasks', async () => { + it('deletes the user', async () => { await user.del('/user', { password, }); diff --git a/test/helpers/api-integration/v2/object-generators.js b/test/helpers/api-integration/v2/object-generators.js index e10a4daa9a..e30f96c7b8 100644 --- a/test/helpers/api-integration/v2/object-generators.js +++ b/test/helpers/api-integration/v2/object-generators.js @@ -72,18 +72,18 @@ export async function createAndPopulateGroup (settings = {}) { let groupLeader = await generateUser(leaderDetails); let group = await generateGroup(groupLeader, groupDetails); + const groupMembershipTypes = { + party: { 'party._id': group._id}, + guild: { guilds: [group._id] }, + }; + let members = await Q.all( times(numberOfMembers, () => { - return generateUser(); + return generateUser(groupMembershipTypes[group.type]); }) ); - let memberIds = members.map((member) => { - return member._id; - }); - memberIds.push(groupLeader._id); - - await group.update({ members: memberIds }); + await group.update({ memberCount: numberOfMembers + 1}); let invitees = await Q.all( times(numberOfInvites, () => { diff --git a/website/src/controllers/api-v2/auth.js b/website/src/controllers/api-v2/auth.js index 9ada4800b9..6ad3f30412 100644 --- a/website/src/controllers/api-v2/auth.js +++ b/website/src/controllers/api-v2/auth.js @@ -7,8 +7,13 @@ var utils = require('../../libs/api-v2/utils'); var nconf = require('nconf'); var request = require('request'); var FirebaseTokenGenerator = require('firebase-token-generator'); -var User = require('../../models/user').model; -var EmailUnsubscription = require('../../models/emailUnsubscription').model; +import { + model as User, +} from '../../models/user'; +import { + model as EmailUnsubscription, +} from '../../models/emailUnsubscription'; + var analytics = utils.analytics; var i18n = require('./../../libs/api-v2/i18n'); diff --git a/website/src/controllers/api-v2/challenges.js b/website/src/controllers/api-v2/challenges.js index 89d3ade502..db53ae80e3 100644 --- a/website/src/controllers/api-v2/challenges.js +++ b/website/src/controllers/api-v2/challenges.js @@ -4,9 +4,15 @@ var _ = require('lodash'); var nconf = require('nconf'); var async = require('async'); var shared = require('../../../../common'); -var User = require('./../../models/user').model; -var Group = require('./../../models/group').model; -var Challenge = require('./../../models/challenge').model; +import { + model as User, +} from '../../models/user'; +import { + model as Group, +} from '../../models/group'; +import { + model as Challenge, +} from '../../models/challenge'; var logging = require('./../../libs/api-v2/logging'); var csvStringify = require('csv-stringify'); var utils = require('../../libs/api-v2/utils'); diff --git a/website/src/controllers/api-v2/coupon.js b/website/src/controllers/api-v2/coupon.js index 8ab576cb6e..38136ce4e5 100644 --- a/website/src/controllers/api-v2/coupon.js +++ b/website/src/controllers/api-v2/coupon.js @@ -1,5 +1,7 @@ var _ = require('lodash'); -var Coupon = require('./../../models/coupon').model; +import { + model as Coupon, +} from '../../models/coupon'; var api = module.exports; var csvStringify = require('csv-stringify'); var async = require('async'); diff --git a/website/src/controllers/api-v2/dataexport.js b/website/src/controllers/api-v2/dataexport.js index 681749bd16..2b3c46ed31 100644 --- a/website/src/controllers/api-v2/dataexport.js +++ b/website/src/controllers/api-v2/dataexport.js @@ -5,7 +5,9 @@ var nconf = require('nconf'); var moment = require('moment'); var js2xmlparser = require("js2xmlparser"); var pd = require('pretty-data').pd; -var User = require('../../models/user').model; +import { + model as User, +} from '../../models/user'; // Avatar screenshot/static-page includes //var Pageres = require('pageres'); //https://github.com/sindresorhus/pageres diff --git a/website/src/controllers/api-v2/groups.js b/website/src/controllers/api-v2/groups.js index 4b9c4b199d..f1e42a68da 100644 --- a/website/src/controllers/api-v2/groups.js +++ b/website/src/controllers/api-v2/groups.js @@ -11,10 +11,20 @@ var async = require('async'); var Q = require('q'); var utils = require('./../../libs/api-v2/utils'); var shared = require('../../../../common'); -var User = require('./../../models/user').model; -var Group = require('./../../models/group').model; -var Challenge = require('./../../models/challenge').model; -var EmailUnsubscription = require('./../../models/emailUnsubscription').model; + +import { + model as User, +} from './../../models/user'; +import { + model as Group, +} from './../../models/group'; +import { + model as Challenge, +} from './../../models/challenge'; +import { + model as EmailUnsubscription, +} from './../../models/emailUnsubscription'; + var isProd = nconf.get('NODE_ENV') === 'production'; var api = module.exports; var pushNotify = require('./pushNotifications'); @@ -70,31 +80,41 @@ api.list = function(req, res, next) { // unecessary given our ui-router setup party: function(cb){ if (!~type.indexOf('party')) return cb(null, {}); - Group.findOne({type: 'party', members: {'$in': [user._id]}}) + Group.findOne({_id: user.party._id, type: 'party'}) .select(groupFields).exec(function(err, party){ if (err) return cb(err); - cb(null, (party === null ? [] : [party])); // return as an array for consistent ngResource use + if (!party) return cb(null, []); + party.getTransformedData({cb: function (err, transformedParty) { + if (err) return cb(err); + cb(null, (transformedParty === null ? [] : [transformedParty])); // return as an array for consistent ngResource use + }}); }); }, guilds: function(cb) { if (!~type.indexOf('guilds')) return cb(null, []); - Group.find({members: {'$in': [user._id]}, type:'guild'}) - .select(groupFields).sort(sort).exec(cb); + Group.find({_id: {'$in': user.guilds}, type:'guild'}) + .select(groupFields).sort(sort).exec(function (err, guilds) { + if (err) return cb(err); + async.map(guilds, function (guild, cb1) { + guild.getTransformedData({cb: cb1}) + }, function(err, guildsTransormed) { + cb(err, guildsTransormed); + }); + }); }, 'public': function(cb) { if (!~type.indexOf('public')) return cb(null, []); Group.find({privacy: 'public'}) - .select(groupFields + ' members') + .select(groupFields) .sort(sort) .lean() .exec(function(err, groups){ if (err) return cb(err); _.each(groups, function(g){ // To save some client-side performance, don't send down the full members arr, just send down temp var _isMember - if (~g.members.indexOf(user._id)) g._isMember = true; - g.members = undefined; + if (user.guilds.indexOf(g._id) !== -1) g._isMember = true; }); cb(null, groups); }); @@ -105,7 +125,10 @@ api.list = function(req, res, next) { if (!~type.indexOf('tavern')) return cb(null, {}); Group.findById('habitrpg').select(groupFields).exec(function(err, tavern){ if (err) return cb(err); - cb(null, [tavern]); // return as an array for consistent ngResource use + tavern.getTransformedData({cb: function (err, transformedTavern) { + if (err) return cb(err); + cb(null, ([transformedTavern])); // return as an array for consistent ngResource use + }}); }); } @@ -132,14 +155,24 @@ api.list = function(req, res, next) { api.get = function(req, res, next) { var user = res.locals.user; var gid = req.params.gid; + let isUserGuild = user.guilds.indexOf(gid) !== -1; - var q = (gid == 'party') - ? Group.findOne({type: 'party', members: {'$in': [user._id]}}) - : Group.findOne({$or:[ - {_id:gid, privacy:'public'}, - {_id:gid, privacy:'private', members: {$in:[user._id]}} // if the group is private, only return if they have access - ]}); - populateQuery(gid, q); + var q; + + if (gid === 'party' || gid === user.party._id) { + q = Group.findOne({_id: user.party._id, type: 'party'}) + } else { + + if (isUserGuild) { + q = Group.findOne({type: 'guild', _id: gid}); + } else { + q = Group.findOne({type: 'guild', privacy: 'public', _id: gid}); + } + } + + q.populate('leader', nameFields); + + //populateQuery(gid, q); q.exec(function(err, group){ if (err) return next(err); if(!group){ @@ -150,34 +183,27 @@ api.get = function(req, res, next) { return res.json(group); } - if (!user.contributor.admin) { - _purgeFlagInfoFromChat(group, user); - } - - //Since we have a limit on how many members are populate to the group, we want to make sure the user is always in the group - var userInGroup = _.find(group.members, function(member){ return member._id == user._id; }); - //If the group is private or the group is a party, then the user must be a member of the group based on access restrictions above - if (group.privacy === 'private' || gid === 'party') { - //If the user is not in the group query, remove a user and add the current user - if (!userInGroup) { - group.members.splice(0,1); - group.members.push(user); - } - res.json(group); - } else if ( group.privacy === "public" ) { //The group is public, we must do an extra check to see if the user is already in the group query - //We must see how to check if a user is a member of a public group, so we requery - var q2 = Group.findOne({ _id: group._id, privacy:'public', members: {$in:[user._id]} }); - q2.exec(function(err, group2){ + group.getTransformedData({ + cb: function (err, transformedGroup) { if (err) return next(err); - if (group2 && !userInGroup) { - group.members.splice(0,1); - group.members.push(user); - } - res.json(group); - }); - } - gid = null; + if (!user.contributor.admin) { + _purgeFlagInfoFromChat(transformedGroup, user); + } + + //Since we have a limit on how many members are populate to the group, we want to make sure the user is always in the group + var userInGroup = _.find(transformedGroup.members, function(member){ return member._id == user._id; }); + if ((gid === 'party' || isUserGuild) && !userInGroup) { + transformedGroup.members.splice(0,1); + transformedGroup.members.push(user); + } + + res.json(transformedGroup); + }, + populateMembers: group.type === 'party' ? partyFields : nameFields, + populateInvites: nameFields, + populateChallenges: challengeFields, + }); }); }; @@ -185,10 +211,12 @@ api.get = function(req, res, next) { api.create = function(req, res, next) { var group = new Group(req.body); var user = res.locals.user; - group.members = [user._id]; + //group.members = [user._id]; group.leader = user._id; + if (!group.name) group.name = 'group name'; if(group.type === 'guild'){ + user.guilds.push(group._id); if(user.balance < 1) return res.status(401).json({err: shared.i18n.t('messageInsufficientGems')}); group.balance = 1; @@ -200,33 +228,31 @@ api.create = function(req, res, next) { function(saved,ct,cb){ firebase.updateGroupData(saved); firebase.addUserToGroup(saved._id, user._id); - saved.populate('members', nameFields, cb); + saved.getTransformedData({ + populateMembers: nameFields, + cb, + }) } - ],function(err,saved){ + ],function(err,groupTransformed){ if (err) return next(err); - res.json(saved); + res.json(groupTransformed); group = user = null; }); } else{ - async.waterfall([ - function(cb){ - Group.findOne({type:'party',members:{$in:[user._id]}},cb); - }, - function(found, cb){ - if (found) return cb(shared.i18n.t('messageGroupAlreadyInParty')); - group.save(cb); - }, - function(saved, count, cb){ - firebase.updateGroupData(saved); - firebase.addUserToGroup(saved._id, user._id); - saved.populate('members', nameFields, cb); - } - ], function(err, populated){ - if (err === shared.i18n.t('messageGroupAlreadyInParty')) return res.status(400).json({err:err}); + if (user.party._id) return res.status(400).json({err:shared.i18n.t('messageGroupAlreadyInParty')}); + user.party._id = group._id; + user.save(function (err) { if (err) return next(err); - group = user = null; - return res.json(populated); + group.save(function(err, saved) { + if (err) return next(err); + saved.getTransformedData({ + populateMembers: nameFields, + cb (err, groupTransformed) { + res.json(groupTransformed); + }, + }); + }); }) } } diff --git a/website/src/controllers/api-v2/hall.js b/website/src/controllers/api-v2/hall.js index ec88894c62..05d3bf520c 100644 --- a/website/src/controllers/api-v2/hall.js +++ b/website/src/controllers/api-v2/hall.js @@ -2,8 +2,12 @@ var _ = require('lodash'); var nconf = require('nconf'); var async = require('async'); var shared = require('../../../../common'); -var User = require('./../../models/user').model; -var Group = require('./../../models/group').model; +import { + model as User, +} from '../../models/user'; +import { + model as Group, +} from '../../models/group'; var api = module.exports; api.ensureAdmin = function(req, res, next) { diff --git a/website/src/controllers/api-v2/members.js b/website/src/controllers/api-v2/members.js index 26020a86a7..232bb9ed73 100644 --- a/website/src/controllers/api-v2/members.js +++ b/website/src/controllers/api-v2/members.js @@ -1,6 +1,11 @@ -var User = require('mongoose').model('User'); -var groups = require('../../models/group'); -var partyFields = require('./groups').partyFields +import { + model as groups, + chatDefaults, +} from '../../models/group'; +import { + model as User, +} from '../../models/user'; +let partyFields = require('./groups').partyFields; var api = module.exports; var async = require('async'); var _ = require('lodash'); @@ -49,12 +54,12 @@ api.sendMessage = function(user, member, data){ } msg += data.message ? data.message : ''; } - shared.refPush(member.inbox.messages, groups.chatDefaults(msg, user)); + shared.refPush(member.inbox.messages, chatDefaults(msg, user)); member.inbox.newMessages++; member._v++; member.markModified('inbox.messages'); - shared.refPush(user.inbox.messages, _.defaults({sent:true}, groups.chatDefaults(msg, member))); + shared.refPush(user.inbox.messages, _.defaults({sent:true}, chatDefaults(msg, member))); user.markModified('inbox.messages'); } diff --git a/website/src/controllers/api-v2/unsubscription.js b/website/src/controllers/api-v2/unsubscription.js index ac7fb7bcf5..a91db19d86 100644 --- a/website/src/controllers/api-v2/unsubscription.js +++ b/website/src/controllers/api-v2/unsubscription.js @@ -1,5 +1,9 @@ -var User = require('../../models/user').model; -var EmailUnsubscription = require('../../models/emailUnsubscription').model; +import { + model as User, +} from '../../models/user'; +import { + model as EmailUnsubscription, +} from '../../models/emailUnsubscription'; var utils = require('../../libs/api-v2/utils'); var i18n = require('../../../../common').i18n; diff --git a/website/src/controllers/api-v2/user.js b/website/src/controllers/api-v2/user.js index 8587fed5ea..e88318a03f 100644 --- a/website/src/controllers/api-v2/user.js +++ b/website/src/controllers/api-v2/user.js @@ -4,14 +4,21 @@ var _ = require('lodash'); var nconf = require('nconf'); var async = require('async'); var shared = require('../../../../common'); -var User = require('./../../models/user').model; +import { + model as User, +} from '../../models/user'; import * as Tasks from '../../models/task'; import Q from 'q'; import {removeFromArray} from './../../libs/api-v3/collectionManipulators'; var utils = require('./../../libs/api-v2/utils'); var analytics = utils.analytics; -var Group = require('./../../models/group').model; -var Challenge = require('./../../models/challenge').model; +import { + basicFields as basicGroupFields, + model as Group, +} from '../../models/group'; +import { + model as Challenge, +} from '../../models/challenge'; var moment = require('moment'); var logging = require('./../../libs/api-v2/logging'); var acceptablePUTPaths; @@ -442,26 +449,28 @@ api.delete = function(req, res, next) { return res.status(400).json({err:"You have an active subscription, cancel your plan before deleting your account."}); } - Group.find({ - members: { - '$in': [user._id] - } - }, function(err, groups){ - if(err) return next(err); + let types = ['party', 'publicGuilds', 'privateGuilds']; + let groupFields = basicGroupFields.concat(' leader memberCount'); - async.each(groups, function(group, cb){ - group.leave(user, 'remove-all', cb); - }, function(err){ - if(err) return next(err); - - user.remove(function(err){ - if(err) return next(err); - - firebase.deleteUser(user._id); - res.sendStatus(200); - }); - }); - }); + Group.getGroups({user, types, groupFields}) + .then(groups => { + return Q.all(groups.map((group) => { + return group.leave(user, 'remove-all'); + })); + }) + .then(() => { + return Tasks.Task.remove({ + userId: user._id, + }).exec(); + }) + .then(() => { + return user.remove(); + }) + .then(() => { + firebase.deleteUser(user._id); + res.sendStatus(200); + }) + .catch(next); } /* diff --git a/website/src/controllers/pages.js b/website/src/controllers/pages.js index 2f6887b722..d655ce09bf 100644 --- a/website/src/controllers/pages.js +++ b/website/src/controllers/pages.js @@ -24,6 +24,7 @@ api.getFrontPage = { }, }; +// TODO remove api static page let staticPages = ['front', 'privacy', 'terms', 'api', 'features', 'videos', 'contact', 'plans', 'new-stuff', 'community-guidelines', 'old-news', 'press-kit', 'faq', 'overview', 'apps', diff --git a/website/src/models/group.js b/website/src/models/group.js index dbe17f72d0..216d702204 100644 --- a/website/src/models/group.js +++ b/website/src/models/group.js @@ -22,7 +22,6 @@ let Schema = mongoose.Schema; // NOTE once Firebase is enabled any change to groups' members in MongoDB will have to be run through the API // changes made directly to the db will cause Firebase to get out of sync export let schema = new Schema({ - // TODO don't break validation on _id === 'habitrpg' name: {type: String, required: true}, description: String, leader: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.'], required: true}, @@ -65,7 +64,6 @@ export let schema = new Schema({ // 'Accept', the quest begins. If a false user waits too long, probably a good sign to prod them or boot them. // TODO when booting user, remove from .joined and check again if we can now start the quest // TODO as long as quests are party only we can keep it here - // TODO are we sure we need this type of default for this to work? members: {type: Schema.Types.Mixed, default: () => { return {}; }}, @@ -669,6 +667,63 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') { return Q.all(promises); }; +// API v2 compatibility methods +schema.methods.getTransformedData = function getTransformedData (options) { + let cb = options.cb; + let populateMembers = options.populateMembers; + let populateInvites = options.populateInvites; + let populateChallenges = options.populateChallenges; + + let obj = this.toJSON(); + + let queryMembers = {}; + let queryInvites = {}; + + if (this.type === 'guild') { + queryInvites['invitations.guilds.id'] = this._id; + } else { + queryInvites['invitations.party.id'] = this._id; + } + + if (this.type === 'guild') { + queryMembers.guilds = this._id; + } else { + queryMembers['party._id'] = this._id; + } + + let selectDataMembers = '_id'; + let selectDataInvites = '_id'; + let selectDataChallenges = '_id'; + + if (populateMembers) { + selectDataMembers += ` ${populateMembers}`; + } + if (populateInvites) { + selectDataInvites += ` ${populateInvites}`; + } + if (populateChallenges) { + selectDataChallenges += ` ${populateChallenges}`; + } + + let membersQuery = User.find(queryMembers).select(selectDataMembers); + if (options.limitPopulation) membersQuery.limit(15); + + Q.all([ + membersQuery.exec(), + User.find(queryInvites).select(populateInvites).exec(), + Challenge.find({group: obj._id}).select(populateMembers).exec(), + ]) + .then((results) => { + obj.members = results[0]; + obj.invites = results[1]; + obj.challenges = results[2]; + + cb(null, obj); + }) + .catch(cb); +}; +// END API v2 compatibility methods + export const INVITES_LIMIT = 100; export let model = mongoose.model('Group', schema);