UNPKG

22.6 kBJavaScriptView Raw
1/**
2* vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
3*
4* # Group Model
5*
6* ## License
7*
8* Licensed to the Apache Software Foundation (ASF) under one
9* or more contributor license agreements. See the NOTICE file
10* distributed with this work for additional information
11* regarding copyright ownership. The ASF licenses this file
12* to you under the Apache License, Version 2.0 (the
13* "License"); you may not use this file except in compliance
14* with the License. You may obtain a copy of the License at
15*
16* http://www.apache.org/licenses/LICENSE-2.0
17*
18* Unless required by applicable law or agreed to in writing,
19* software distributed under the License is distributed on an
20* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21* KIND, either express or implied. See the License for the
22* specific language governing permissions and limitations
23* under the License.
24*/
25
26module.exports = (function () {
27 'use strict';
28
29 // Dependencies
30 var ld = require('lodash');
31 var cuid = require('cuid');
32 var slugg = require('slugg');
33 var asyncMod = require('async');
34 var storage = require('../storage.js');
35 var common = require('./common.js');
36 var commonGroupPad = require ('./common-group-pad.js');
37 var userCache = require('./user-cache.js');
38 var deletePad = require('./pad.js').del;
39 var GPREFIX = storage.DBPREFIX.GROUP;
40 var UPREFIX = storage.DBPREFIX.USER;
41 var PPREFIX = storage.DBPREFIX.PAD;
42
43 /**
44 * ## Description
45 *
46 * Groups belongs to users. Each user can have multiple groups of pads.
47 *
48 * A group object can be represented like :
49 *
50 * var group = {
51 * _id: 'autoGeneratedUniqueString',
52 * name: 'group1',
53 * pads: [ 'padkey1', 'padkey2' ],
54 * admins: [ 'userkey1', 'userkey2' ],
55 * users: [ 'ukey1' ],
56 * visibility: 'restricted' || 'public' || 'private',
57 * password: 'secret',
58 * readonly: false,
59 * tags: ['important', 'domain1'],
60 * allowUsersToCreatePads: false,
61 * archived: false
62 * };
63 *
64 */
65
66 var group = {};
67
68 /**
69 * ## Public Functions
70 *
71 * ### get
72 *
73 * Group reading
74 *
75 * This function uses `common.getDel` with `del` to *false* and `GPREFIX`
76 * fixed. It will takes mandatory key string and callback function. See
77 * `common.getDel` for documentation.
78 */
79
80 group.get = ld.partial(common.getDel, false, GPREFIX);
81
82 /**
83 * ## getWithPads
84 *
85 * This function uses `group.get` to retrieve the group record, plus it
86 * returns list of attached pads. As `group.get`, it takes `gid` group unique
87 * identifier and a `callback` function. In case of success, it returns
88 * *null*, the *group* object and an Object of *pads* (where keys are _ids).
89 */
90
91 group.getWithPads = function (gid, callback) {
92 if (!ld.isString(gid)) {
93 throw new TypeError('BACKEND.ERROR.TYPE.KEY_STR');
94 }
95 if (!ld.isFunction(callback)) {
96 throw new TypeError('BACKEND.ERROR.TYPE.CALLBACK_FN');
97 }
98 group.get(gid, function (err, g) {
99 if (err) { return callback(err); }
100 if (g.pads.length === 0) {
101 return callback(null, g, {});
102 }
103 var padsKeys = ld.map(g.pads, function (p) { return PPREFIX + p; });
104 storage.fn.getKeys(padsKeys, function (err, pads) {
105 if (err) { return callback(err); }
106 pads = ld.reduce(pads, function (memo, val, key) {
107 key = key.substr(PPREFIX.length);
108 memo[key] = val;
109 return memo;
110 }, {});
111 return callback(null, g, pads);
112 });
113 });
114 };
115
116 /**
117 * ### getByUser
118 *
119 * `getByUser` is an asynchronous function that returns all groups for a
120 * defined user, using `storage.fn.getKeys`. It takes :
121 *
122 * - a `user` object
123 * - a `withExtra` boolean, for gathering or not pads information alongside
124 * with groups, with the help of `getPadsAndUsersByGroups` private function
125 * - a `callback` function, called with *error* if needed, *null* and the
126 * results, an object with keys and groups values, otherwise.
127 *
128 */
129
130 group.getByUser = function (user, withExtra, callback) {
131 if (!ld.isObject(user) || !ld.isArray(user.groups)) {
132 throw new TypeError('BACKEND.ERROR.TYPE.USER_INVALID');
133 }
134 if (!ld.isBoolean(withExtra)) {
135 throw new TypeError('BACKEND.ERROR.TYPE.WITHEXTRA_BOOL');
136 }
137 if (!ld.isFunction(callback)) {
138 throw new TypeError('BACKEND.ERROR.TYPE.CALLBACK_FN');
139 }
140 storage.fn.getKeys(
141 ld.map(user.groups, function (g) { return GPREFIX + g; }),
142 function (err, groups) {
143 if (err) { return callback(err); }
144 groups = ld.reduce(groups, function (memo, val, key) {
145 key = key.substr(GPREFIX.length);
146 memo[key] = val;
147 return memo;
148 }, {});
149 if (withExtra) {
150 group.fn.getPadsAndUsersByGroups(groups, callback);
151 } else {
152 callback(null, groups);
153 }
154 }
155 );
156 };
157
158 /**
159 * ### getBookmarkedGroupsByUser
160 *
161 * `getBookmarkedGroupsByUser` is an asynchronous function that returns all
162 * bookmarked groups for a defined user, using `storage.fn.getKeys`. It takes :
163 *
164 * - a `user` object
165 * - a `callback` function, called with *error* if needed, *null* and the
166 * results, an object with keys and groups values, otherwise.
167 *
168 */
169
170 group.getBookmarkedGroupsByUser = function (user, callback) {
171 if (!ld.isObject(user) || !ld.isArray(user.bookmarks.groups)) {
172 throw new TypeError('BACKEND.ERROR.TYPE.USER_INVALID');
173 }
174 if (!ld.isFunction(callback)) {
175 throw new TypeError('BACKEND.ERROR.TYPE.CALLBACK_FN');
176 }
177 storage.fn.getKeys(
178 ld.map(user.bookmarks.groups, function (g) { return GPREFIX + g; }),
179 function (err, groups) {
180 if (err) { return callback(err); }
181 groups = ld.reduce(groups, function (memo, val, key) {
182 key = key.substr(GPREFIX.length);
183 memo[key] = val;
184 return memo;
185 }, {});
186 callback(null, groups);
187 }
188 );
189 };
190
191 /**
192 * ### set
193 *
194 * This function adds a new group or updates an existing one.
195 * It checks the fields, throws error if needed, set defaults options. As
196 * arguments, it takes mandatory :
197 *
198 * - `params` object, with
199 *
200 * - a `name` string that can't be empty
201 * - an optional `description` string
202 * - an `admin` string, the unique key identifying the initial administrator
203 * of the group
204 * - `visibility`, a string defined as *restricted* by default to invited
205 * users. Can be set to *public*, letting non authenticated users access to
206 * all pads in the group with the URL, or *private*, protected by a password
207 * phrase chosen by the administrator
208 * - `readonly`, *false* on creation. If *true*, pads that will be linked to
209 * the group will be set on readonly mode
210 * - `password` string field, only usefull if visibility has been fixed to
211 * private, by default an empty string
212 * - `users` and `admins` arrays, with ids of users invited to read and/or edit
213 * pads, for restricted visibility only; and group administrators
214 *
215 * - `callback` function returning *Error* if error, *null* otherwise and the
216 * group object.
217 *
218 * `set` creates an empty `pads` array in case of creation, otherwise it just
219 * gets back old value. `pads` array contains ids of pads attached to the
220 * group via `model.pad` creation or update. Also, `password` is repopulated
221 * from old value if the group has already been set as *private* and no
222 * `password` has been given.
223 *
224 * Finally, in case of new group, it sets the unique identifier to the name
225 * slugged and suffixes by random id generator and uses a special ctime
226 * field for epoch time.
227 */
228
229 group.set = function (params, callback) {
230 common.addSetInit(params, callback, ['name', 'admin']);
231 var g = group.fn.assignProps(params);
232 var check = function () {
233 commonGroupPad.handlePassword(g, function (err, password) {
234 if (err) { return callback(err); }
235 if (password) { g.password = password; }
236 group.fn.checkSet(g, callback);
237 });
238 };
239 if (params._id) {
240 g._id = params._id;
241 storage.db.get(GPREFIX + g._id, function (err, res) {
242 if (err) { return callback(err); }
243 if (!res) {
244 return callback(new Error('BACKEND.ERROR.GROUP.INEXISTENT'));
245 }
246 g.pads = res.pads;
247 if ((res.visibility === 'private') && !g.password) {
248 g.password = res.password;
249 }
250 g.ctime = res.ctime;
251 check();
252 });
253 } else {
254 g._id = (slugg(g.name) + '-' + cuid.slug());
255 g.pads = [];
256 g.ctime = Date.now();
257 check();
258 }
259 };
260
261
262 /**
263 * ### del
264 *
265 * Group removal
266 *
267 * This function uses `common.getDel` with `del` to *true* and *GPREFIX*
268 * fixed. It will takes mandatory key string and callback function. See
269 * `common.getDel` for documentation.
270 *
271 * It uses the `callback` function to handle secondary indexes for users and
272 * pads.
273 */
274
275 group.del = function (key, callback) {
276 if (!ld.isFunction(callback)) {
277 throw new TypeError('BACKEND.ERROR.TYPE.CALLBACK_FN');
278 }
279 common.getDel(false, GPREFIX, key, function (err, gr) {
280 if (err) { return callback(err); }
281 group.fn.cascadePads(gr, function(err) {
282 if (err) { return callback(err); }
283 common.getDel(true, GPREFIX, key, function (err, g) {
284 if (err) { return callback(err); }
285 var uids = ld.union(g.admins, g.users);
286 group.fn.indexUsers(true, g._id, uids, callback);
287 });
288 });
289 });
290 };
291
292 /**
293 * ### resign
294 *
295 * `resign` os an asynchronous function that resigns current user from the
296 * given group. It checks if the user is currently a user or administrator of
297 * the group and accept resignation, except if the user is the unique
298 * administrator. It takes care of internal index for the user.
299 *
300 * It takes :
301 *
302 * - `gid` group unique identifier;
303 * - 'uid' user unique identifier;
304 * - `callback` function calling with *error* if error or *null* and the
305 * updated group otherwise.
306 */
307
308 group.resign = function (gid, uid, callback) {
309 if (!ld.isString(gid) || !(ld.isString(uid))) {
310 throw new TypeError('BACKEND.ERROR.TYPE.ID_STR');
311 }
312 if (!ld.isFunction(callback)) {
313 throw new TypeError('BACKEND.ERROR.TYPE.CALLBACK_FN');
314 }
315 group.get(gid, function (err, g) {
316 if (err) { return callback(err); }
317 var users = ld.union(g.admins, g.users);
318 if (!ld.includes(users, uid)) {
319 return callback(new Error('BACKEND.ERROR.GROUP.NOT_USER'));
320 }
321 if ((ld.size(g.admins) === 1) && (ld.first(g.admins) === uid)) {
322 return callback(new Error('BACKEND.ERROR.GROUP.RESIGN_UNIQUE_ADMIN'));
323 }
324 ld.pull(g.admins, uid);
325 ld.pull(g.users, uid);
326 storage.db.set(GPREFIX + g._id, g, function (err) {
327 if (err) { return callback(err); }
328 storage.db.get(UPREFIX + uid, function (err, u) {
329 if (err) { return callback(err); }
330 ld.pull(u.groups, gid);
331 ld.pull(u.bookmarks.groups, gid);
332 storage.db.set(UPREFIX + uid, u, function (err) {
333 if (err) { return callback(err); }
334 return callback(null, g);
335 });
336 });
337 });
338 });
339 };
340
341 /**
342 * ### inviteOrShare
343 *
344 * `inviteOrShare` is an asynchronous function that check if given data, users
345 * or admins logins, are correct and transforms it to expected values : unique
346 * identifiers, before saving it to database.
347 *
348 * It takes :
349 *
350 * - `invite` boolean, *true* for user invitation, *false* for admin sharing;
351 * - `gid` group unique identifier;
352 * - array of users `loginsOrEmails`;
353 * - `callback` function calling with *error* if error or *null* and the
354 * updated group otherwise, plus accepted and refused invitations logins or
355 * emails.
356 *
357 * It takes care of exclusion of admins and users : admin status is a
358 * escalation of user.
359 *
360 * As login list should be exhaustive, it also takes care or reindexing user
361 * local groups.
362 */
363
364 group.inviteOrShare = function (invite, gid, loginsOrEmails, callback) {
365 if (!ld.isBoolean(invite)) {
366 throw new TypeError('BACKEND.ERROR.TYPE.INVITE_BOOL');
367 }
368 if (!ld.isString(gid)) {
369 throw new TypeError('BACKEND.ERROR.TYPE.GID_STR');
370 }
371 if (!ld.isArray(loginsOrEmails)) {
372 throw new TypeError('BACKEND.ERROR.TYPE.LOGINS_ARR');
373 }
374 if (!ld.isFunction(callback)) {
375 throw new TypeError('BACKEND.ERROR.TYPE.CALLBACK_FN');
376 }
377 var users = userCache.fn.getIdsFromLoginsOrEmails(loginsOrEmails);
378 group.get(gid, function (err, g) {
379 if (err) { return callback(err); }
380 var removed;
381 if (invite) {
382 // Remove users from admin before setting them as invited
383 var toRemoveFromAdmins = ld.intersection(g.admins, users.uids);
384 g.admins = ld.filter(g.admins, function(n) {
385 return (ld.indexOf(toRemoveFromAdmins, n) === -1);
386 });
387 if (ld.size(g.admins) === 0) {
388 return callback(
389 new Error('BACKEND.ERROR.GROUP.RESIGN_UNIQUE_ADMIN')
390 );
391 }
392
393 // Setting users as invited
394 removed = ld.difference(g.users, users.uids);
395 g.users = ld.unique(ld.reject(users.uids,
396 ld.partial(ld.includes, g.admins)));
397 } else {
398 // Remove users from invite before setting them as admins
399 var toRemoveFromUsers = ld.intersection(g.users, users.uids);
400 g.users = ld.filter(g.users, function(n) {
401 return (ld.indexOf(toRemoveFromUsers, n) === -1);
402 });
403
404 // Setting users as admins
405 removed = ld.difference(g.admins, users.uids);
406 g.admins = ld.unique(ld.reject(users.uids,
407 ld.partial(ld.includes, g.users)));
408 if ((ld.size(g.admins)) === 0) {
409 return callback(
410 new Error('BACKEND.ERROR.GROUP.RESIGN_UNIQUE_ADMIN')
411 );
412 }
413 }
414 // indexUsers with deletion for full reindexation process
415 group.fn.indexUsers(true, g._id, removed, function (err) {
416 if (err) { return callback(err); }
417 group.fn.set(g, function (err, g) {
418 if (err) { return callback(err); }
419 callback(null, g, ld.omit(users, 'uids'));
420 });
421 });
422 });
423 };
424
425 /**
426 * ## Helper Functions
427 *
428 * Helper here are public functions created to facilitate interaction with
429 * the API and improve performance, avoiding extra checking when not needed.
430 * TODO : may be written to improve API usage
431 */
432
433 group.helper = {};
434
435 /**
436 * ### linkPads
437 *
438 * `linkPads` is a function to attach new pads to an existing group.
439 * It takes mandatory arguments :
440 *
441 * - the pad `_id`entifier, a string
442 * - `add`, a string for only one addition, an array for multiple adds.
443 */
444
445 group.helper.linkPads = ld.noop;
446
447 group.helper.unlinkPads = ld.noop;
448
449 /**
450 * ### inviteUsers
451 * string or array
452 */
453
454 group.helper.inviteUsers = ld.noop;
455
456 /**
457 * ### setAdmins
458 * string or array
459 */
460
461 group.helper.setAdmins = ld.noop;
462
463 /**
464 * ### setPassword
465 * string of false
466 */
467
468 group.helper.setPassword = ld.noop;
469
470 /**
471 * ### setPublic
472 * boolean
473 */
474
475 group.helper.setPublic = ld.noop;
476
477 /**
478 * ### archive
479 * boolean
480 */
481
482 group.helper.archive = ld.noop;
483
484 /**
485 * ## Internal Functions
486 *
487 * These functions are not private like with closures, for testing purposes,
488 * but they are expected be used only internally by other MyPads functions.
489 * All of these are tested through public API.
490 */
491
492 group.fn = {};
493
494 /**
495 * ### assignProps
496 *
497 * `assignProps` takes params object and assign defaults if needed.
498 * It creates :
499 *
500 * - an `admins` array, unioning admin key to optional others admins
501 * - a `users` array, empty or with given keys
502 * - a `pads` array, empty on creation, can't be fixed either
503 * - a `visibility` string, defaults to *restricted*, with only two other
504 * possibilities : *private* or *public*
505 * - a `password` string, *null* by default
506 * - a `readonly` boolean, *false* by default
507 *
508 * It returns the group object.
509 */
510
511 group.fn.assignProps = function (params) {
512 var p = params;
513 var g = { name: p.name };
514
515 g.description = (ld.isString(p.description) ? p.description : '');
516 p.admins = ld.isArray(p.admins) ? ld.filter(p.admins, ld.isString) : [];
517 g.admins = ld.union([ p.admin ], p.admins);
518 g.users = ld.uniq(p.users);
519
520 var v = p.visibility;
521 var vVal = ['restricted', 'private', 'public'];
522
523 g.visibility = (ld.isString(v) && ld.includes(vVal, v)) ? v : 'restricted';
524 g.password = ld.isString(p.password) ? p.password : null;
525 g.readonly = ld.isBoolean(p.readonly) ? p.readonly : false;
526 g.allowUsersToCreatePads = ld.isBoolean(p.allowUsersToCreatePads) ? p.allowUsersToCreatePads : false;
527 g.archived = ld.isBoolean(p.archived) ? p.archived : false;
528 g.tags = ld.isArray(p.tags) ? p.tags : [];
529 return g;
530 };
531
532 /**
533 * ### cascadePads
534 *
535 * `cascadePads` is an asynchronous function which handle cascade removals
536 * after group removal. It takes :
537 *
538 * - the `group` object
539 * - a `callback` function, returning *Error* or *null* if succeeded
540 */
541
542 group.fn.cascadePads = function (group, callback) {
543 if (!ld.isEmpty(group.pads)) {
544 asyncMod.map(group.pads, deletePad, function(err, res) {
545 if (err) { return callback(err); }
546 var e = new Error('BACKEND.ERROR.GROUP.CASCADE_REMOVAL_PROBLEM');
547 if (!res) { return callback(e); }
548 callback(null);
549 });
550 } else {
551 callback(null);
552 }
553 };
554
555 /**
556 * ### indexUsers
557 *
558 * `indexUsers` is an asynchronous function which handles secondary indexes
559 * for *users.groups* after group creation, update, removal. It takes :
560 *
561 * - a `del` boolean to know if we have to delete key from index or add it
562 * - the group `gid` unique identifier
563 * - `uids`, an array of user keys
564 * - a `callback` function, returning *Error* or *null* if succeeded
565 */
566
567 group.fn.indexUsers = function (del, gid, uids, callback) {
568 var usersKeys = ld.map(uids, function (u) { return UPREFIX + u; });
569 storage.fn.getKeys(usersKeys, function (err, users) {
570 if (err) { return callback(err); }
571 ld.forIn(users, function (u, k) {
572 // When deleting the user, storage.fn.getKeys(usersKeys)
573 // returns undefined because the user record has already
574 // been deleted
575 if (typeof(u) !== 'undefined') {
576 if (del) {
577 ld.pull(u.groups, gid);
578 ld.pull(u.bookmarks.groups, gid);
579 } else if (!ld.includes(u.groups, gid)) {
580 u.groups.push(gid);
581 }
582 users[k] = u;
583 }
584 });
585 storage.fn.setKeys(users, function (err) {
586 if (err) { return callback(err); }
587 return callback(null);
588 });
589 });
590 };
591
592 /**
593 * ### set
594 *
595 * `set` is internal function that set the user group into the database.
596 * It takes care of secondary indexes for users and pads by calling
597 * `indexUsers`.
598 *
599 * It takes, as arguments :
600 *
601 * - the `g` group object
602 * - the `callback` function returning an *Error* or *null* and the `g`
603 * object.
604 */
605
606 group.fn.set = function (g, callback) {
607 storage.db.set(GPREFIX + g._id, g, function (err) {
608 if (err) { return callback(err); }
609 var uids = ld.union(g.admins, g.users);
610 group.fn.indexUsers(false, g._id, uids, function (err) {
611 if (err) { return callback(err); }
612 return callback(null, g);
613 });
614 });
615 };
616
617 /**
618 * ### checkSet
619 *
620 * `checkSet` will ensure that all users and pads exist. If true, it calls
621 * `fn.set`, else it will return an *Error*. `checkSet` takes :
622 *
623 * - a `g` group object
624 * - a `callback` function returning an *Error* or *null* and the `g` object.
625 */
626
627 group.fn.checkSet = function (g, callback) {
628 var pre = ld.curry(function (pre, val) { return pre + val; });
629 var admins = ld.map(g.admins, pre(UPREFIX));
630 var users = ld.map(g.users, pre(UPREFIX));
631 var pads = ld.map(g.pads, pre(PPREFIX));
632 var allKeys = ld.union(admins, users, pads);
633 common.checkMultiExist(allKeys, function (err, res) {
634 if (err) { return callback(err); }
635 if (!res) {
636 var e = new Error('BACKEND.ERROR.GROUP.ITEMS_NOT_FOUND');
637 return callback(e);
638 }
639 group.fn.set(g, callback);
640 });
641 };
642
643 /**
644 * ### getPadsAndUsersByGroups
645 *
646 * `getPadsAndUsersByGroups` is an asynchronous private function which return
647 * *pads* and *users* objects from an object of *group* objects (key: group).
648 * It also takes a classic `callback` function.
649 */
650
651 group.fn.getPadsAndUsersByGroups = function (groups, callback) {
652 var defs = { pads: PPREFIX, users: UPREFIX };
653 var addPfx = function (pfx, values) {
654 return ld.map(values, function (v) { return pfx + v; });
655 };
656 var keys = ld.reduce(groups, function (memo, val) {
657 memo.pads = ld.union(memo.pads, addPfx(PPREFIX, val.pads));
658 memo.users = ld.union(memo.users, addPfx(UPREFIX, val.users),
659 addPfx(UPREFIX, val.admins));
660 return memo;
661 }, { pads: [], users: [] });
662 storage.fn.getKeys(ld.flatten(ld.values(keys)), function (err, res) {
663 if (err) { return callback(err); }
664 res = ld.reduce(res, function (memo, val, key) {
665 var field;
666 ld.forIn(keys, function (vals, f) {
667 if (ld.includes(vals, key)) { field = f; }
668 });
669 key = key.substr(defs[field].length);
670 memo[field][key] = val;
671 return memo;
672 }, { groups: groups, pads: {}, users: {} });
673 callback(null, res);
674 });
675 };
676
677 /**
678 * ### count
679 *
680 * Returns the number of groups of the MyPads instance
681 * As arguments, it takes mandatory :
682 * - a `callback` function
683 */
684
685 group.count = function(callback) {
686 storage.db.findKeys(GPREFIX + '*', null, function (err, res) {
687 if (err) { return callback(err); }
688 return callback(null, ld.size(res));
689 });
690 };
691
692 return group;
693
694
695}).call(this);