From b126cc3cb3fc5c73dc19b52ba8e5ddca0457c4f1 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 16 Aug 2017 15:47:56 -0600 Subject: [PATCH] initial impl of cached action counts using ee --- .vscode/settings.json | 8 ++++ graph/mutators/action.js | 6 +-- models/comment.js | 10 ++++- services/actions.js | 81 +++++++++++++++++++++++++--------------- services/comments.js | 43 +++++++++++++++++++++ services/events.js | 17 +++++++++ 6 files changed, 128 insertions(+), 37 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 services/events.js diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..838540390 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "eslint.autoFixOnSave": true, + "editor.tabSize": 2, + "editor.rulers": [ + 80 + ], + "files.trimTrailingWhitespace": true +} diff --git a/graph/mutators/action.js b/graph/mutators/action.js index d62d3b4c5..0b0d76895 100644 --- a/graph/mutators/action.js +++ b/graph/mutators/action.js @@ -1,4 +1,3 @@ -const ActionModel = require('../../models/action'); const ActionsService = require('../../services/actions'); const UsersService = require('../../services/users'); const errors = require('../../errors'); @@ -52,10 +51,7 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id, * @return {Promise} resolves to the deleted action, or null if not found. */ const deleteAction = ({user}, {id}) => { - return ActionModel.findOneAndRemove({ - id, - user_id: user.id - }); + return ActionsService.delete({id, user_id: user.id}); }; module.exports = (context) => { diff --git a/models/comment.js b/models/comment.js index 352172e57..39d233b8e 100644 --- a/models/comment.js +++ b/models/comment.js @@ -66,15 +66,23 @@ const CommentSchema = new Schema({ enum: COMMENT_STATUS, default: 'NONE' }, + + // parent_id is the id of the parent comment (null if there is none). parent_id: String, + // Counts to store related to actions taken on the given comment. + action_counts: { + default: {}, + type: Object, + }, + // Tags are added by the self or by administrators. tags: [TagLinkSchema], // Additional metadata stored on the field. metadata: { default: {}, - type: Object + type: Object, } }, { timestamps: { diff --git a/services/actions.js b/services/actions.js index 74c3bdad3..864666a07 100644 --- a/services/actions.js +++ b/services/actions.js @@ -1,6 +1,30 @@ const ActionModel = require('../models/action'); const _ = require('lodash'); const errors = require('../errors'); +const events = require('./events'); + +const findOneAndUpdate = async (query, update, options = {}) => new Promise((resolve, reject) => { + ActionModel.findOneAndUpdate(query, update, Object.assign({}, options, { + + // Use raw result to get `updatedExisting`. + passRawResult: true, + + // Ensure that if it's new, we return the new object created. + new: true, + + // Perform an upsert in the event that this doesn't exist. + upsert: true, + + // Set the default values if not provided based on the mongoose models. + setDefaultsOnInsert: true + }), (err, doc, raw) => { + if (err) { return reject(err); } + if (raw.lastErrorObject.updatedExisting) { + return reject(new errors.ErrAlreadyExists(raw.value)); + } + return resolve(raw.value); + }); +}); module.exports = class ActionsService { @@ -19,46 +43,26 @@ module.exports = class ActionsService { * @param {String} action the new action to the item * @return {Promise} */ - static insertUserAction(action) { + static async insertUserAction(action) { // Actions are made unique by using a query that can be reproducable, i.e., // not containing user inputable values. - let query = { + let foundAction = await findOneAndUpdate({ action_type: action.action_type, item_type: action.item_type, item_id: action.item_id, user_id: action.user_id, - group_id: action.group_id - }; + group_id: action.group_id, + }, { - // Create/Update the action. - return new Promise((resolve, reject) => { - ActionModel.findOneAndUpdate( - query, { - - // Only set when not existing. - $setOnInsert: action, - }, { - - // Ensure that if it's new, we return the new object created. - new: true, - - // Use raw result to get `updatedExisting`. - passRawResult: true, - - // Perform an upsert in the event that this doesn't exist. - upsert: true, - - // Set the default values if not provided based on the mongoose models. - setDefaultsOnInsert: true - }, (err, doc, raw) => { - if (err) { return reject(err); } - if (raw.lastErrorObject.updatedExisting) { - return reject(new errors.ErrAlreadyExists(raw.value)); - } - return resolve(raw.value); - }); + // Only set when not existing. + $setOnInsert: action, }); + + // Emit that there was a new action created. + events.emit('actions.new', foundAction); + + return foundAction; } /** @@ -190,6 +194,21 @@ module.exports = class ActionsService { }); } + static async delete({id, user_id}) { + let action = await ActionModel.findOneAndRemove({ + id, + user_id, + }); + if (!action) { + return; + } + + // Emit that the action was deleted. + events.emit('actions.delete', action); + + return action; + } + /** * Finds all comments ids for a specific action. * @param {String} action_type type of action diff --git a/services/comments.js b/services/comments.js index af7593a8e..c9bf92153 100644 --- a/services/comments.js +++ b/services/comments.js @@ -5,6 +5,49 @@ const ActionsService = require('./actions'); const SettingsService = require('./settings'); const errors = require('../errors'); +const events = require('./events'); + +// When a new action is created, modify the comment. +events.on('actions.new', async (action) => { + if (!action || action.item_type !== 'COMMENTS') { + return; + } + + const ACTION_TYPE = action.action_type.toLowerCase(); + + try { + await CommentModel.update({ + id: action.item_id, + }, { + $inc: { + [`action_counts.${ACTION_TYPE}`]: 1, + } + }); + } catch (err) { + console.error(`Can't increment the action_counts.${ACTION_TYPE}:`, err); + } +}); + +// When an action is deleted, modify the comment. +events.on('actions.delete', async (action) => { + if (!action || action.item_type !== 'COMMENTS') { + return; + } + + const ACTION_TYPE = action.action_type.toLowerCase(); + + try { + await CommentModel.update({ + id: action.item_id, + }, { + $inc: { + [`action_counts.${ACTION_TYPE}`]: -1, + } + }); + } catch (err) { + console.error(`Can't decrement the action_counts.${ACTION_TYPE}:`, err); + } +}); module.exports = class CommentsService { diff --git a/services/events.js b/services/events.js new file mode 100644 index 000000000..f32ddfcd1 --- /dev/null +++ b/services/events.js @@ -0,0 +1,17 @@ +const {EventEmitter2} = require('eventemitter2'); +const debug = require('debug')('talk:services:events'); +const enabled = require('debug').enabled('talk:services:events'); + +const events = new EventEmitter2({ + wildcard: true, +}); + +// If event debugging is enabled, bind the debugger to all events being emitted +// and log a debug message. +if (enabled) { + events.onAny(function(event) { + debug(`[${event}] ${arguments.length - 1} argument${arguments.length - 1 === 1 ? '' : 's'}`); + }); +} + +module.exports = events;