From 45b662a1ddb46a0f17fab7b2383c82aa1e1620ef Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Tue, 8 Sep 2015 20:19:42 +0200 Subject: Centralize all mutations at the model level This commit uses a new package that I need to document. It tries to solve the long-standing debate in the Meteor community about allow/deny rules versus methods (RPC). This approach gives us both the centralized security rules of allow/deny and the white-list of allowed mutations similarly to Meteor methods. The idea to have static mutation descriptions is also inspired by Facebook's Relay/GraphQL. This will allow the development of a REST API using the high-level methods instead of the MongoDB queries to do the mapping between the HTTP requests and our collections. --- collections/activities.js | 51 -------- collections/attachments.js | 79 ------------ collections/avatars.js | 27 ----- collections/boards.js | 256 --------------------------------------- collections/cards.js | 284 -------------------------------------------- collections/lists.js | 94 --------------- collections/unsavedEdits.js | 34 ------ collections/users.js | 151 ----------------------- 8 files changed, 976 deletions(-) delete mode 100644 collections/activities.js delete mode 100644 collections/attachments.js delete mode 100644 collections/avatars.js delete mode 100644 collections/boards.js delete mode 100644 collections/cards.js delete mode 100644 collections/lists.js delete mode 100644 collections/unsavedEdits.js delete mode 100644 collections/users.js (limited to 'collections') diff --git a/collections/activities.js b/collections/activities.js deleted file mode 100644 index 5de07ee5..00000000 --- a/collections/activities.js +++ /dev/null @@ -1,51 +0,0 @@ -// Activities don't need a schema because they are always set from the a trusted -// environment - the server - and there is no risk that a user change the logic -// we use with this collection. Moreover using a schema for this collection -// would be difficult (different activities have different fields) and wouldn't -// bring any direct advantage. -// -// XXX The activities API is not so nice and need some functionalities. For -// instance if a user archive a card, and un-archive it a few seconds later we -// should remove both activities assuming it was an error the user decided to -// revert. -Activities = new Mongo.Collection('activities'); - -Activities.helpers({ - board() { - return Boards.findOne(this.boardId); - }, - user() { - return Users.findOne(this.userId); - }, - member() { - return Users.findOne(this.memberId); - }, - list() { - return Lists.findOne(this.listId); - }, - oldList() { - return Lists.findOne(this.oldListId); - }, - card() { - return Cards.findOne(this.cardId); - }, - comment() { - return CardComments.findOne(this.commentId); - }, - attachment() { - return Attachments.findOne(this.attachmentId); - }, -}); - -Activities.before.insert((userId, doc) => { - doc.createdAt = new Date(); -}); - -// For efficiency create an index on the date of creation. -if (Meteor.isServer) { - Meteor.startup(() => { - Activities._collection._ensureIndex({ - createdAt: -1, - }); - }); -} diff --git a/collections/attachments.js b/collections/attachments.js deleted file mode 100644 index 8ef0fef0..00000000 --- a/collections/attachments.js +++ /dev/null @@ -1,79 +0,0 @@ -Attachments = new FS.Collection('attachments', { - stores: [ - - // XXX Add a new store for cover thumbnails so we don't load big images in - // the general board view - new FS.Store.GridFS('attachments'), - ], -}); - -if (Meteor.isServer) { - Attachments.allow({ - insert(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - update(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - remove(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - // We authorize the attachment download either: - // - if the board is public, everyone (even unconnected) can download it - // - if the board is private, only board members can download it - // - // XXX We have a bug with the `userId` verification: - // - // https://github.com/CollectionFS/Meteor-CollectionFS/issues/449 - // - download(userId, doc) { - const query = { - $or: [ - { 'members.userId': userId }, - { permission: 'public' }, - ], - }; - return Boolean(Boards.findOne(doc.boardId, query)); - }, - - fetch: ['boardId'], - }); -} - -// XXX Enforce a schema for the Attachments CollectionFS - -Attachments.files.before.insert((userId, doc) => { - const file = new FS.File(doc); - doc.userId = userId; - - // If the uploaded document is not an image we need to enforce browser - // download instead of execution. This is particularly important for HTML - // files that the browser will just execute if we don't serve them with the - // appropriate `application/octet-stream` MIME header which can lead to user - // data leaks. I imagine other formats (like PDF) can also be attack vectors. - // See https://github.com/libreboard/libreboard/issues/99 - // XXX Should we use `beforeWrite` option of CollectionFS instead of - // collection-hooks? - if (!file.isImage()) { - file.original.type = 'application/octet-stream'; - } -}); - -if (Meteor.isServer) { - Attachments.files.after.insert((userId, doc) => { - Activities.insert({ - userId, - type: 'card', - activityType: 'addAttachment', - attachmentId: doc._id, - boardId: doc.boardId, - cardId: doc.cardId, - }); - }); - - Attachments.files.after.remove((userId, doc) => { - Activities.remove({ - attachmentId: doc._id, - }); - }); -} diff --git a/collections/avatars.js b/collections/avatars.js deleted file mode 100644 index 53924ffb..00000000 --- a/collections/avatars.js +++ /dev/null @@ -1,27 +0,0 @@ -Avatars = new FS.Collection('avatars', { - stores: [ - new FS.Store.GridFS('avatars'), - ], - filter: { - maxSize: 72000, - allow: { - contentTypes: ['image/*'], - }, - }, -}); - -function isOwner(userId, file) { - return userId && userId === file.userId; -} - -Avatars.allow({ - insert: isOwner, - update: isOwner, - remove: isOwner, - download() { return true; }, - fetch: ['userId'], -}); - -Avatars.files.before.insert((userId, doc) => { - doc.userId = userId; -}); diff --git a/collections/boards.js b/collections/boards.js deleted file mode 100644 index fcd04153..00000000 --- a/collections/boards.js +++ /dev/null @@ -1,256 +0,0 @@ -Boards = new Mongo.Collection('boards'); - -Boards.attachSchema(new SimpleSchema({ - title: { - type: String, - }, - slug: { - type: String, - }, - archived: { - type: Boolean, - }, - createdAt: { - type: Date, - denyUpdate: true, - }, - // XXX Inconsistent field naming - modifiedAt: { - type: Date, - denyInsert: true, - optional: true, - }, - // De-normalized number of users that have starred this board - stars: { - type: Number, - }, - // De-normalized label system - 'labels.$._id': { - // We don't specify that this field must be unique in the board because that - // will cause performance penalties and is not necessary since this field is - // always set on the server. - // XXX Actually if we create a new label, the `_id` is set on the client - // without being overwritten by the server, could it be a problem? - type: String, - }, - 'labels.$.name': { - type: String, - optional: true, - }, - 'labels.$.color': { - type: String, - allowedValues: [ - 'green', 'yellow', 'orange', 'red', 'purple', - 'blue', 'sky', 'lime', 'pink', 'black', - ], - }, - // XXX We might want to maintain more informations under the member sub- - // documents like de-normalized meta-data (the date the member joined the - // board, the number of contributions, etc.). - 'members.$.userId': { - type: String, - }, - 'members.$.isAdmin': { - type: Boolean, - }, - 'members.$.isActive': { - type: Boolean, - }, - permission: { - type: String, - allowedValues: ['public', 'private'], - }, - color: { - type: String, - allowedValues: [ - 'belize', - 'nephritis', - 'pomegranate', - 'pumpkin', - 'wisteria', - 'midnight', - ], - }, -})); - -if (Meteor.isServer) { - Boards.allow({ - insert: Meteor.userId, - update: allowIsBoardAdmin, - remove: allowIsBoardAdmin, - fetch: ['members'], - }); - - // The number of users that have starred this board is managed by trusted code - // and the user is not allowed to update it - Boards.deny({ - update(userId, board, fieldNames) { - return _.contains(fieldNames, 'stars'); - }, - fetch: [], - }); - - // We can't remove a member if it is the last administrator - Boards.deny({ - update(userId, doc, fieldNames, modifier) { - if (!_.contains(fieldNames, 'members')) - return false; - - // We only care in case of a $pull operation, ie remove a member - if (!_.isObject(modifier.$pull && modifier.$pull.members)) - return false; - - // If there is more than one admin, it's ok to remove anyone - const nbAdmins = _.filter(doc.members, (member) => { - return member.isAdmin; - }).length; - if (nbAdmins > 1) - return false; - - // If all the previous conditions were verified, we can't remove - // a user if it's an admin - const removedMemberId = modifier.$pull.members.userId; - return Boolean(_.findWhere(doc.members, { - userId: removedMemberId, - isAdmin: true, - })); - }, - fetch: ['members'], - }); -} - -Boards.helpers({ - isPublic() { - return this.permission === 'public'; - }, - - lists() { - return Lists.find({ boardId: this._id, archived: false }, - { sort: { sort: 1 }}); - }, - - activities() { - return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 }}); - }, - - activeMembers() { - return _.where(this.members, {isActive: true}); - }, - - absoluteUrl() { - return FlowRouter.path('board', { id: this._id, slug: this.slug }); - }, - - colorClass() { - return `board-color-${this.color}`; - }, -}); - -Boards.before.insert((userId, doc) => { - // XXX We need to improve slug management. Only the id should be necessary - // to identify a board in the code. - // XXX If the board title is updated, the slug should also be updated. - // In some cases (Chinese and Japanese for instance) the `getSlug` function - // return an empty string. This is causes bugs in our application so we set - // a default slug in this case. - doc.slug = doc.slug || getSlug(doc.title) || 'board'; - doc.createdAt = new Date(); - doc.archived = false; - doc.members = doc.members || [{ - userId, - isAdmin: true, - isActive: true, - }]; - doc.stars = 0; - doc.color = Boards.simpleSchema()._schema.color.allowedValues[0]; - - // Handle labels - const colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues; - const defaultLabelsColors = _.clone(colors).splice(0, 6); - doc.labels = _.map(defaultLabelsColors, (color) => { - return { - color, - _id: Random.id(6), - name: '', - }; - }); -}); - -Boards.before.update((userId, doc, fieldNames, modifier) => { - modifier.$set = modifier.$set || {}; - modifier.$set.modifiedAt = new Date(); -}); - -if (Meteor.isServer) { - // Let MongoDB ensure that a member is not included twice in the same board - Meteor.startup(() => { - Boards._collection._ensureIndex({ - _id: 1, - 'members.userId': 1, - }, { unique: true }); - }); - - // Genesis: the first activity of the newly created board - Boards.after.insert((userId, doc) => { - Activities.insert({ - userId, - type: 'board', - activityTypeId: doc._id, - activityType: 'createBoard', - boardId: doc._id, - }); - }); - - // If the user remove one label from a board, we cant to remove reference of - // this label in any card of this board. - Boards.after.update((userId, doc, fieldNames, modifier) => { - if (!_.contains(fieldNames, 'labels') || - !modifier.$pull || - !modifier.$pull.labels || - !modifier.$pull.labels._id) - return; - - const removedLabelId = modifier.$pull.labels._id; - Cards.update( - { boardId: doc._id }, - { - $pull: { - labels: removedLabelId, - }, - }, - { multi: true } - ); - }); - - // Add a new activity if we add or remove a member to the board - Boards.after.update((userId, doc, fieldNames, modifier) => { - if (!_.contains(fieldNames, 'members')) - return; - - let memberId; - - // Say hello to the new member - if (modifier.$push && modifier.$push.members) { - memberId = modifier.$push.members.userId; - Activities.insert({ - userId, - memberId, - type: 'member', - activityType: 'addBoardMember', - boardId: doc._id, - }); - } - - // Say goodbye to the former member - if (modifier.$pull && modifier.$pull.members) { - memberId = modifier.$pull.members.userId; - Activities.insert({ - userId, - memberId, - type: 'member', - activityType: 'removeBoardMember', - boardId: doc._id, - }); - } - }); -} diff --git a/collections/cards.js b/collections/cards.js deleted file mode 100644 index 97ba4e3c..00000000 --- a/collections/cards.js +++ /dev/null @@ -1,284 +0,0 @@ -Cards = new Mongo.Collection('cards'); -CardComments = new Mongo.Collection('card_comments'); - -// XXX To improve pub/sub performances a card document should include a -// de-normalized number of comments so we don't have to publish the whole list -// of comments just to display the number of them in the board view. -Cards.attachSchema(new SimpleSchema({ - title: { - type: String, - }, - archived: { - type: Boolean, - }, - listId: { - type: String, - }, - // The system could work without this `boardId` information (we could deduce - // the board identifier from the card), but it would make the system more - // difficult to manage and less efficient. - boardId: { - type: String, - }, - coverId: { - type: String, - optional: true, - }, - createdAt: { - type: Date, - denyUpdate: true, - }, - dateLastActivity: { - type: Date, - }, - description: { - type: String, - optional: true, - }, - labelIds: { - type: [String], - optional: true, - }, - members: { - type: [String], - optional: true, - }, - // XXX Should probably be called `authorId`. Is it even needed since we have - // the `members` field? - userId: { - type: String, - }, - sort: { - type: Number, - decimal: true, - }, -})); - -CardComments.attachSchema(new SimpleSchema({ - boardId: { - type: String, - }, - cardId: { - type: String, - }, - // XXX Rename in `content`? `text` is a bit vague... - text: { - type: String, - }, - // XXX We probably don't need this information here, since we already have it - // in the associated comment creation activity - createdAt: { - type: Date, - denyUpdate: false, - }, - // XXX Should probably be called `authorId` - userId: { - type: String, - }, -})); - -if (Meteor.isServer) { - Cards.allow({ - insert(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - update(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - remove(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - fetch: ['boardId'], - }); - - CardComments.allow({ - insert(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - update(userId, doc) { - return userId === doc.userId; - }, - remove(userId, doc) { - return userId === doc.userId; - }, - fetch: ['userId', 'boardId'], - }); -} - -Cards.helpers({ - list() { - return Lists.findOne(this.listId); - }, - board() { - return Boards.findOne(this.boardId); - }, - labels() { - const boardLabels = this.board().labels; - const cardLabels = _.filter(boardLabels, (label) => { - return _.contains(this.labelIds, label._id); - }); - return cardLabels; - }, - hasLabel(labelId) { - return _.contains(this.labelIds, labelId); - }, - user() { - return Users.findOne(this.userId); - }, - isAssigned(memberId) { - return _.contains(this.members, memberId); - }, - activities() { - return Activities.find({ cardId: this._id }, { sort: { createdAt: -1 }}); - }, - comments() { - return CardComments.find({ cardId: this._id }, { sort: { createdAt: -1 }}); - }, - attachments() { - return Attachments.find({ cardId: this._id }, { sort: { uploadedAt: -1 }}); - }, - cover() { - return Attachments.findOne(this.coverId); - }, - absoluteUrl() { - const board = this.board(); - return FlowRouter.path('card', { - boardId: board._id, - slug: board.slug, - cardId: this._id, - }); - }, - rootUrl() { - return Meteor.absoluteUrl(this.absoluteUrl().replace('/', '')); - }, -}); - -CardComments.helpers({ - user() { - return Users.findOne(this.userId); - }, -}); - -CardComments.hookOptions.after.update = { fetchPrevious: false }; -Cards.before.insert((userId, doc) => { - doc.createdAt = new Date(); - doc.dateLastActivity = new Date(); - - // defaults - doc.archived = false; - - // userId native set. - if (!doc.userId) - doc.userId = userId; -}); - -CardComments.before.insert((userId, doc) => { - doc.createdAt = new Date(); - doc.userId = userId; -}); - -if (Meteor.isServer) { - Cards.after.insert((userId, doc) => { - Activities.insert({ - userId, - activityType: 'createCard', - boardId: doc.boardId, - listId: doc.listId, - cardId: doc._id, - }); - }); - - // New activity for card (un)archivage - Cards.after.update((userId, doc, fieldNames) => { - if (_.contains(fieldNames, 'archived')) { - if (doc.archived) { - Activities.insert({ - userId, - activityType: 'archivedCard', - boardId: doc.boardId, - listId: doc.listId, - cardId: doc._id, - }); - } else { - Activities.insert({ - userId, - activityType: 'restoredCard', - boardId: doc.boardId, - listId: doc.listId, - cardId: doc._id, - }); - } - } - }); - - // New activity for card moves - Cards.after.update(function(userId, doc, fieldNames) { - const oldListId = this.previous.listId; - if (_.contains(fieldNames, 'listId') && doc.listId !== oldListId) { - Activities.insert({ - userId, - oldListId, - activityType: 'moveCard', - listId: doc.listId, - boardId: doc.boardId, - cardId: doc._id, - }); - } - }); - - // Add a new activity if we add or remove a member to the card - Cards.before.update((userId, doc, fieldNames, modifier) => { - if (!_.contains(fieldNames, 'members')) - return; - let memberId; - // Say hello to the new member - if (modifier.$addToSet && modifier.$addToSet.members) { - memberId = modifier.$addToSet.members; - if (!_.contains(doc.members, memberId)) { - Activities.insert({ - userId, - memberId, - activityType: 'joinMember', - boardId: doc.boardId, - cardId: doc._id, - }); - } - } - - // Say goodbye to the former member - if (modifier.$pull && modifier.$pull.members) { - memberId = modifier.$pull.members; - Activities.insert({ - userId, - memberId, - activityType: 'unjoinMember', - boardId: doc.boardId, - cardId: doc._id, - }); - } - }); - - // Remove all activities associated with a card if we remove the card - Cards.after.remove((userId, doc) => { - Activities.remove({ - cardId: doc._id, - }); - }); - - CardComments.after.insert((userId, doc) => { - Activities.insert({ - userId, - activityType: 'addComment', - boardId: doc.boardId, - cardId: doc.cardId, - commentId: doc._id, - }); - }); - - CardComments.after.remove((userId, doc) => { - const activity = Activities.findOne({ commentId: doc._id }); - if (activity) { - Activities.remove(activity._id); - } - }); -} diff --git a/collections/lists.js b/collections/lists.js deleted file mode 100644 index 0c6ba407..00000000 --- a/collections/lists.js +++ /dev/null @@ -1,94 +0,0 @@ -Lists = new Mongo.Collection('lists'); - -Lists.attachSchema(new SimpleSchema({ - title: { - type: String, - }, - archived: { - type: Boolean, - }, - boardId: { - type: String, - }, - createdAt: { - type: Date, - denyUpdate: true, - }, - sort: { - type: Number, - decimal: true, - // XXX We should probably provide a default - optional: true, - }, - updatedAt: { - type: Date, - denyInsert: true, - optional: true, - }, -})); - -if (Meteor.isServer) { - Lists.allow({ - insert(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - update(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - remove(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - fetch: ['boardId'], - }); -} - -Lists.helpers({ - cards() { - return Cards.find(Filter.mongoSelector({ - listId: this._id, - archived: false, - }), { sort: ['sort'] }); - }, - board() { - return Boards.findOne(this.boardId); - }, -}); - -// HOOKS -Lists.hookOptions.after.update = { fetchPrevious: false }; - -Lists.before.insert((userId, doc) => { - doc.createdAt = new Date(); - doc.archived = false; - if (!doc.userId) - doc.userId = userId; -}); - -Lists.before.update((userId, doc, fieldNames, modifier) => { - modifier.$set = modifier.$set || {}; - modifier.$set.modifiedAt = new Date(); -}); - -if (Meteor.isServer) { - Lists.after.insert((userId, doc) => { - Activities.insert({ - userId, - type: 'list', - activityType: 'createList', - boardId: doc.boardId, - listId: doc._id, - }); - }); - - Lists.after.update((userId, doc) => { - if (doc.archived) { - Activities.insert({ - userId, - type: 'list', - activityType: 'archivedList', - listId: doc._id, - boardId: doc.boardId, - }); - } - }); -} diff --git a/collections/unsavedEdits.js b/collections/unsavedEdits.js deleted file mode 100644 index 87a70e22..00000000 --- a/collections/unsavedEdits.js +++ /dev/null @@ -1,34 +0,0 @@ -// This collection shouldn't be manipulated directly by instead throw the -// `UnsavedEdits` API on the client. -UnsavedEditCollection = new Mongo.Collection('unsaved-edits'); - -UnsavedEditCollection.attachSchema(new SimpleSchema({ - fieldName: { - type: String, - }, - docId: { - type: String, - }, - value: { - type: String, - }, - userId: { - type: String, - }, -})); - -if (Meteor.isServer) { - function isAuthor(userId, doc, fieldNames = []) { - return userId === doc.userId && fieldNames.indexOf('userId') === -1; - } - UnsavedEditCollection.allow({ - insert: isAuthor, - update: isAuthor, - remove: isAuthor, - fetch: ['userId'], - }); -} - -UnsavedEditCollection.before.insert((userId, doc) => { - doc.userId = userId; -}); diff --git a/collections/users.js b/collections/users.js deleted file mode 100644 index fa910c4a..00000000 --- a/collections/users.js +++ /dev/null @@ -1,151 +0,0 @@ -Users = Meteor.users; - -// Search a user in the complete server database by its name or username. This -// is used for instance to add a new user to a board. -const searchInFields = ['username', 'profile.name']; -Users.initEasySearch(searchInFields, { - use: 'mongo-db', - returnFields: [...searchInFields, 'profile.avatarUrl'], -}); - -Users.helpers({ - boards() { - return Boards.find({ userId: this._id }); - }, - - starredBoards() { - const starredBoardIds = this.profile.starredBoards || []; - return Boards.find({archived: false, _id: {$in: starredBoardIds}}); - }, - - hasStarred(boardId) { - const starredBoardIds = this.profile.starredBoards || []; - return _.contains(starredBoardIds, boardId); - }, - - isBoardMember() { - const board = Boards.findOne(Session.get('currentBoard')); - return board && _.contains(_.pluck(board.members, 'userId'), this._id) && - _.where(board.members, {userId: this._id})[0].isActive; - }, - - isBoardAdmin() { - const board = Boards.findOne(Session.get('currentBoard')); - return board && this.isBoardMember(board) && - _.where(board.members, {userId: this._id})[0].isAdmin; - }, - - getInitials() { - const profile = this.profile || {}; - if (profile.initials) - return profile.initials; - - else if (profile.fullname) { - return _.reduce(profile.fullname.split(/\s+/), (memo, word) => { - return memo + word[0]; - }, '').toUpperCase(); - - } else { - return this.username[0].toUpperCase(); - } - }, - - toggleBoardStar(boardId) { - const queryKind = this.hasStarred(boardId) ? '$pull' : '$addToSet'; - Meteor.users.update(this._id, { - [queryKind]: { - 'profile.starredBoards': boardId, - }, - }); - }, -}); - -Meteor.methods({ - setUsername(username) { - check(username, String); - const nUsersWithUsername = Users.find({ username }).count(); - if (nUsersWithUsername > 0) { - throw new Meteor.Error('username-already-taken'); - } else { - Users.update(this.userId, {$set: { username }}); - } - }, -}); - -Users.before.insert((userId, doc) => { - doc.profile = doc.profile || {}; - - if (!doc.username && doc.profile.name) { - doc.username = doc.profile.name.toLowerCase().replace(/\s/g, ''); - } -}); - -if (Meteor.isServer) { - // Let mongoDB ensure username unicity - Meteor.startup(() => { - Users._collection._ensureIndex({ - username: 1, - }, { unique: true }); - }); - - // Each board document contains the de-normalized number of users that have - // starred it. If the user star or unstar a board, we need to update this - // counter. - // We need to run this code on the server only, otherwise the incrementation - // will be done twice. - Users.after.update(function(userId, user, fieldNames) { - // The `starredBoards` list is hosted on the `profile` field. If this - // field hasn't been modificated we don't need to run this hook. - if (!_.contains(fieldNames, 'profile')) - return; - - // To calculate a diff of board starred ids, we get both the previous - // and the newly board ids list - function getStarredBoardsIds(doc) { - return doc.profile && doc.profile.starredBoards; - } - const oldIds = getStarredBoardsIds(this.previous); - const newIds = getStarredBoardsIds(user); - - // The _.difference(a, b) method returns the values from a that are not in - // b. We use it to find deleted and newly inserted ids by using it in one - // direction and then in the other. - function incrementBoards(boardsIds, inc) { - _.forEach(boardsIds, (boardId) => { - Boards.update(boardId, {$inc: {stars: inc}}); - }); - } - incrementBoards(_.difference(oldIds, newIds), -1); - incrementBoards(_.difference(newIds, oldIds), +1); - }); - - // XXX i18n - Users.after.insert((userId, doc) => { - const ExampleBoard = { - title: 'Welcome Board', - userId: doc._id, - permission: 'private', - }; - - // Insert the Welcome Board - Boards.insert(ExampleBoard, (err, boardId) => { - - _.forEach(['Basics', 'Advanced'], (title) => { - const list = { - title, - boardId, - userId: ExampleBoard.userId, - - // XXX Not certain this is a bug, but we except these fields get - // inserted by the Lists.before.insert collection-hook. Since this - // hook is not called in this case, we have to dublicate the logic and - // set them here. - archived: false, - createdAt: new Date(), - }; - - Lists.insert(list); - }); - }); - }); -} -- cgit v1.2.3-1-g7c22