api v3 adapt v2: correctly import dependencies, port create, get, list groups

This commit is contained in:
Matteo Pagliazzi
2016-04-04 23:18:49 +02:00
parent f6fc50f6c2
commit 0a40c56973
17 changed files with 258 additions and 123 deletions

View File

@@ -4,11 +4,11 @@ import {
resetHabiticaDB, resetHabiticaDB,
} from '../../../helpers/api-integration/v2'; } from '../../../helpers/api-integration/v2';
xdescribe('GET /groups', () => { describe('GET /groups', () => {
const NUMBER_OF_PUBLIC_GUILDS = 3; const NUMBER_OF_PUBLIC_GUILDS = 3;
const NUMBER_OF_USERS_GUILDS = 2;
let user; let user;
let leader;
before(async () => { before(async () => {
// Set up a world with a mixture of public and private guilds // Set up a world with a mixture of public and private guilds
@@ -16,7 +16,7 @@ xdescribe('GET /groups', () => {
await resetHabiticaDB(); await resetHabiticaDB();
user = await generateUser(); user = await generateUser();
let leader = await generateUser({ balance: 10 }); leader = await generateUser({ balance: 10 });
await generateGroup(leader, { await generateGroup(leader, {
name: 'public guild - is member', name: 'public guild - is member',
@@ -90,8 +90,8 @@ xdescribe('GET /groups', () => {
context('guilds passed in as query', () => { context('guilds passed in as query', () => {
it('returns all guilds user is a part of ', async () => { it('returns all guilds user is a part of ', async () => {
await expect(user.get('/groups', null, {type: 'guilds'})) await expect(leader.get('/groups', null, {type: 'guilds'}))
.to.eventually.have.a.lengthOf(NUMBER_OF_USERS_GUILDS); .to.eventually.have.a.lengthOf(4);
}); });
}); });
}); });

View File

@@ -8,7 +8,7 @@ import {
each, each,
} from 'lodash'; } from 'lodash';
xdescribe('GET /groups/:id', () => { describe('GET /groups/:id', () => {
let typesOfGroups = {}; let typesOfGroups = {};
typesOfGroups['public guild'] = { type: 'guild', privacy: 'public' }; typesOfGroups['public guild'] = { type: 'guild', privacy: 'public' };
typesOfGroups['private guild'] = { type: 'guild', privacy: 'private' }; typesOfGroups['private guild'] = { type: 'guild', privacy: 'private' };

View File

@@ -4,7 +4,7 @@ import {
translate as t, translate as t,
} from '../../../helpers/api-integration/v2'; } from '../../../helpers/api-integration/v2';
xdescribe('POST /groups', () => { describe('POST /groups', () => {
context('All groups', () => { context('All groups', () => {
let leader; let leader;

View File

@@ -4,7 +4,11 @@ import {
generateGroup, generateGroup,
generateUser, generateUser,
} from '../../../helpers/api-integration/v2'; } from '../../../helpers/api-integration/v2';
import { find } from 'lodash'; import {
find,
map,
} from 'lodash';
import Q from 'q';
xdescribe('DELETE /user', () => { xdescribe('DELETE /user', () => {
let user; let user;
@@ -19,6 +23,18 @@ xdescribe('DELETE /user', () => {
})).to.eventually.eql(false); })).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', () => { context('user has active subscription', () => {
it('does not delete account'); it('does not delete account');
}); });

View File

@@ -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 // gets the user's tasks ids
let ids = []; let ids = [];
each(user.tasksOrder, (idsForOrder) => { 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', { await user.del('/user', {
password, password,
}); });

View File

@@ -72,18 +72,18 @@ export async function createAndPopulateGroup (settings = {}) {
let groupLeader = await generateUser(leaderDetails); let groupLeader = await generateUser(leaderDetails);
let group = await generateGroup(groupLeader, groupDetails); let group = await generateGroup(groupLeader, groupDetails);
const groupMembershipTypes = {
party: { 'party._id': group._id},
guild: { guilds: [group._id] },
};
let members = await Q.all( let members = await Q.all(
times(numberOfMembers, () => { times(numberOfMembers, () => {
return generateUser(); return generateUser(groupMembershipTypes[group.type]);
}) })
); );
let memberIds = members.map((member) => { await group.update({ memberCount: numberOfMembers + 1});
return member._id;
});
memberIds.push(groupLeader._id);
await group.update({ members: memberIds });
let invitees = await Q.all( let invitees = await Q.all(
times(numberOfInvites, () => { times(numberOfInvites, () => {

View File

@@ -7,8 +7,13 @@ var utils = require('../../libs/api-v2/utils');
var nconf = require('nconf'); var nconf = require('nconf');
var request = require('request'); var request = require('request');
var FirebaseTokenGenerator = require('firebase-token-generator'); var FirebaseTokenGenerator = require('firebase-token-generator');
var User = require('../../models/user').model; import {
var EmailUnsubscription = require('../../models/emailUnsubscription').model; model as User,
} from '../../models/user';
import {
model as EmailUnsubscription,
} from '../../models/emailUnsubscription';
var analytics = utils.analytics; var analytics = utils.analytics;
var i18n = require('./../../libs/api-v2/i18n'); var i18n = require('./../../libs/api-v2/i18n');

View File

@@ -4,9 +4,15 @@ var _ = require('lodash');
var nconf = require('nconf'); var nconf = require('nconf');
var async = require('async'); var async = require('async');
var shared = require('../../../../common'); var shared = require('../../../../common');
var User = require('./../../models/user').model; import {
var Group = require('./../../models/group').model; model as User,
var Challenge = require('./../../models/challenge').model; } 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 logging = require('./../../libs/api-v2/logging');
var csvStringify = require('csv-stringify'); var csvStringify = require('csv-stringify');
var utils = require('../../libs/api-v2/utils'); var utils = require('../../libs/api-v2/utils');

View File

@@ -1,5 +1,7 @@
var _ = require('lodash'); var _ = require('lodash');
var Coupon = require('./../../models/coupon').model; import {
model as Coupon,
} from '../../models/coupon';
var api = module.exports; var api = module.exports;
var csvStringify = require('csv-stringify'); var csvStringify = require('csv-stringify');
var async = require('async'); var async = require('async');

View File

@@ -5,7 +5,9 @@ var nconf = require('nconf');
var moment = require('moment'); var moment = require('moment');
var js2xmlparser = require("js2xmlparser"); var js2xmlparser = require("js2xmlparser");
var pd = require('pretty-data').pd; var pd = require('pretty-data').pd;
var User = require('../../models/user').model; import {
model as User,
} from '../../models/user';
// Avatar screenshot/static-page includes // Avatar screenshot/static-page includes
//var Pageres = require('pageres'); //https://github.com/sindresorhus/pageres //var Pageres = require('pageres'); //https://github.com/sindresorhus/pageres

View File

@@ -11,10 +11,20 @@ var async = require('async');
var Q = require('q'); var Q = require('q');
var utils = require('./../../libs/api-v2/utils'); var utils = require('./../../libs/api-v2/utils');
var shared = require('../../../../common'); var shared = require('../../../../common');
var User = require('./../../models/user').model;
var Group = require('./../../models/group').model; import {
var Challenge = require('./../../models/challenge').model; model as User,
var EmailUnsubscription = require('./../../models/emailUnsubscription').model; } 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 isProd = nconf.get('NODE_ENV') === 'production';
var api = module.exports; var api = module.exports;
var pushNotify = require('./pushNotifications'); var pushNotify = require('./pushNotifications');
@@ -70,31 +80,41 @@ api.list = function(req, res, next) {
// unecessary given our ui-router setup // unecessary given our ui-router setup
party: function(cb){ party: function(cb){
if (!~type.indexOf('party')) return cb(null, {}); 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){ .select(groupFields).exec(function(err, party){
if (err) return cb(err); 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) { guilds: function(cb) {
if (!~type.indexOf('guilds')) return cb(null, []); if (!~type.indexOf('guilds')) return cb(null, []);
Group.find({members: {'$in': [user._id]}, type:'guild'}) Group.find({_id: {'$in': user.guilds}, type:'guild'})
.select(groupFields).sort(sort).exec(cb); .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) { 'public': function(cb) {
if (!~type.indexOf('public')) return cb(null, []); if (!~type.indexOf('public')) return cb(null, []);
Group.find({privacy: 'public'}) Group.find({privacy: 'public'})
.select(groupFields + ' members') .select(groupFields)
.sort(sort) .sort(sort)
.lean() .lean()
.exec(function(err, groups){ .exec(function(err, groups){
if (err) return cb(err); if (err) return cb(err);
_.each(groups, function(g){ _.each(groups, function(g){
// To save some client-side performance, don't send down the full members arr, just send down temp var _isMember // 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; if (user.guilds.indexOf(g._id) !== -1) g._isMember = true;
g.members = undefined;
}); });
cb(null, groups); cb(null, groups);
}); });
@@ -105,7 +125,10 @@ api.list = function(req, res, next) {
if (!~type.indexOf('tavern')) return cb(null, {}); if (!~type.indexOf('tavern')) return cb(null, {});
Group.findById('habitrpg').select(groupFields).exec(function(err, tavern){ Group.findById('habitrpg').select(groupFields).exec(function(err, tavern){
if (err) return cb(err); 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) { api.get = function(req, res, next) {
var user = res.locals.user; var user = res.locals.user;
var gid = req.params.gid; var gid = req.params.gid;
let isUserGuild = user.guilds.indexOf(gid) !== -1;
var q = (gid == 'party') var q;
? Group.findOne({type: 'party', members: {'$in': [user._id]}})
: Group.findOne({$or:[ if (gid === 'party' || gid === user.party._id) {
{_id:gid, privacy:'public'}, q = Group.findOne({_id: user.party._id, type: 'party'})
{_id:gid, privacy:'private', members: {$in:[user._id]}} // if the group is private, only return if they have access } else {
]});
populateQuery(gid, q); 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){ q.exec(function(err, group){
if (err) return next(err); if (err) return next(err);
if(!group){ if(!group){
@@ -150,34 +183,27 @@ api.get = function(req, res, next) {
return res.json(group); return res.json(group);
} }
group.getTransformedData({
cb: function (err, transformedGroup) {
if (err) return next(err);
if (!user.contributor.admin) { if (!user.contributor.admin) {
_purgeFlagInfoFromChat(group, user); _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 //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; }); var userInGroup = _.find(transformedGroup.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 ((gid === 'party' || isUserGuild) && !userInGroup) {
if (group.privacy === 'private' || gid === 'party') { transformedGroup.members.splice(0,1);
//If the user is not in the group query, remove a user and add the current user transformedGroup.members.push(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){
if (err) return next(err);
if (group2 && !userInGroup) {
group.members.splice(0,1);
group.members.push(user);
}
res.json(group);
});
} }
gid = null; 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) { api.create = function(req, res, next) {
var group = new Group(req.body); var group = new Group(req.body);
var user = res.locals.user; var user = res.locals.user;
group.members = [user._id]; //group.members = [user._id];
group.leader = user._id; group.leader = user._id;
if (!group.name) group.name = 'group name';
if(group.type === 'guild'){ if(group.type === 'guild'){
user.guilds.push(group._id);
if(user.balance < 1) return res.status(401).json({err: shared.i18n.t('messageInsufficientGems')}); if(user.balance < 1) return res.status(401).json({err: shared.i18n.t('messageInsufficientGems')});
group.balance = 1; group.balance = 1;
@@ -200,33 +228,31 @@ api.create = function(req, res, next) {
function(saved,ct,cb){ function(saved,ct,cb){
firebase.updateGroupData(saved); firebase.updateGroupData(saved);
firebase.addUserToGroup(saved._id, user._id); 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); if (err) return next(err);
res.json(saved); res.json(groupTransformed);
group = user = null; group = user = null;
}); });
} else{ } else{
async.waterfall([ if (user.party._id) return res.status(400).json({err:shared.i18n.t('messageGroupAlreadyInParty')});
function(cb){ user.party._id = group._id;
Group.findOne({type:'party',members:{$in:[user._id]}},cb); user.save(function (err) {
},
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 (err) return next(err); if (err) return next(err);
group = user = null; group.save(function(err, saved) {
return res.json(populated); if (err) return next(err);
saved.getTransformedData({
populateMembers: nameFields,
cb (err, groupTransformed) {
res.json(groupTransformed);
},
});
});
}) })
} }
} }

View File

@@ -2,8 +2,12 @@ var _ = require('lodash');
var nconf = require('nconf'); var nconf = require('nconf');
var async = require('async'); var async = require('async');
var shared = require('../../../../common'); var shared = require('../../../../common');
var User = require('./../../models/user').model; import {
var Group = require('./../../models/group').model; model as User,
} from '../../models/user';
import {
model as Group,
} from '../../models/group';
var api = module.exports; var api = module.exports;
api.ensureAdmin = function(req, res, next) { api.ensureAdmin = function(req, res, next) {

View File

@@ -1,6 +1,11 @@
var User = require('mongoose').model('User'); import {
var groups = require('../../models/group'); model as groups,
var partyFields = require('./groups').partyFields chatDefaults,
} from '../../models/group';
import {
model as User,
} from '../../models/user';
let partyFields = require('./groups').partyFields;
var api = module.exports; var api = module.exports;
var async = require('async'); var async = require('async');
var _ = require('lodash'); var _ = require('lodash');
@@ -49,12 +54,12 @@ api.sendMessage = function(user, member, data){
} }
msg += data.message ? data.message : ''; 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.inbox.newMessages++;
member._v++; member._v++;
member.markModified('inbox.messages'); 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'); user.markModified('inbox.messages');
} }

View File

@@ -1,5 +1,9 @@
var User = require('../../models/user').model; import {
var EmailUnsubscription = require('../../models/emailUnsubscription').model; model as User,
} from '../../models/user';
import {
model as EmailUnsubscription,
} from '../../models/emailUnsubscription';
var utils = require('../../libs/api-v2/utils'); var utils = require('../../libs/api-v2/utils');
var i18n = require('../../../../common').i18n; var i18n = require('../../../../common').i18n;

View File

@@ -4,14 +4,21 @@ var _ = require('lodash');
var nconf = require('nconf'); var nconf = require('nconf');
var async = require('async'); var async = require('async');
var shared = require('../../../../common'); var shared = require('../../../../common');
var User = require('./../../models/user').model; import {
model as User,
} from '../../models/user';
import * as Tasks from '../../models/task'; import * as Tasks from '../../models/task';
import Q from 'q'; import Q from 'q';
import {removeFromArray} from './../../libs/api-v3/collectionManipulators'; import {removeFromArray} from './../../libs/api-v3/collectionManipulators';
var utils = require('./../../libs/api-v2/utils'); var utils = require('./../../libs/api-v2/utils');
var analytics = utils.analytics; var analytics = utils.analytics;
var Group = require('./../../models/group').model; import {
var Challenge = require('./../../models/challenge').model; basicFields as basicGroupFields,
model as Group,
} from '../../models/group';
import {
model as Challenge,
} from '../../models/challenge';
var moment = require('moment'); var moment = require('moment');
var logging = require('./../../libs/api-v2/logging'); var logging = require('./../../libs/api-v2/logging');
var acceptablePUTPaths; 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."}); return res.status(400).json({err:"You have an active subscription, cancel your plan before deleting your account."});
} }
Group.find({ let types = ['party', 'publicGuilds', 'privateGuilds'];
members: { let groupFields = basicGroupFields.concat(' leader memberCount');
'$in': [user._id]
}
}, function(err, groups){
if(err) return next(err);
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);
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); firebase.deleteUser(user._id);
res.sendStatus(200); res.sendStatus(200);
}); })
}); .catch(next);
});
} }
/* /*

View File

@@ -24,6 +24,7 @@ api.getFrontPage = {
}, },
}; };
// TODO remove api static page
let staticPages = ['front', 'privacy', 'terms', 'api', 'features', let staticPages = ['front', 'privacy', 'terms', 'api', 'features',
'videos', 'contact', 'plans', 'new-stuff', 'community-guidelines', 'videos', 'contact', 'plans', 'new-stuff', 'community-guidelines',
'old-news', 'press-kit', 'faq', 'overview', 'apps', 'old-news', 'press-kit', 'faq', 'overview', 'apps',

View File

@@ -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 // 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 // changes made directly to the db will cause Firebase to get out of sync
export let schema = new Schema({ export let schema = new Schema({
// TODO don't break validation on _id === 'habitrpg'
name: {type: String, required: true}, name: {type: String, required: true},
description: String, description: String,
leader: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.'], required: true}, 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. // '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 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 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: () => { members: {type: Schema.Types.Mixed, default: () => {
return {}; return {};
}}, }},
@@ -669,6 +667,63 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
return Q.all(promises); 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 const INVITES_LIMIT = 100;
export let model = mongoose.model('Group', schema); export let model = mongoose.model('Group', schema);