diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js index 8b3dee5f5..ea3fe4d31 100644 --- a/graph/loaders/metrics.js +++ b/graph/loaders/metrics.js @@ -1,13 +1,104 @@ const _ = require('lodash'); +const DataLoader = require('dataloader'); +const util = require('./util'); +const objectCacheKeyFn = util.objectCacheKeyFn; +const arrayCacheKeyFn = util.arrayCacheKeyFn; + const CommentModel = require('../../models/comment'); -const AssetModel = require('../../models/asset'); const ActionModel = require('../../models/action'); -const getMetrics = (context, {from, to, sort, limit}) => { +const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => { let commentMetrics = {}; let assetMetrics = []; + return Metrics.getRecentActions.load({from, to}) + .then((actionSummaries) => { + + commentMetrics = actionSummaries.reduce((acc, {item_id, action_type, count}) => { + if (!(item_id in acc)) { + acc[item_id] = []; + } + + acc[item_id].push({action_type, count}); + + return acc; + }, {}); + + // Collect just the comment id's. + let commentIDs = _.uniq(actionSummaries.map((as) => as.item_id)); + + // Find those comments. + return Metrics.getSpecificComments.load(commentIDs); + }) + .then((commentResults) => { + + assetMetrics = commentResults.map(({ids, asset_id}) => { + let summaries = _.groupBy(_.flatten(ids.map((id) => commentMetrics[id])), 'action_type'); + + let action_summaries = Object.keys(summaries).map((action_type) => ({ + action_type, + actionCount: summaries[action_type].reduce((acc, {count}) => acc + count, 0), + actionableItemCount: summaries[action_type].length + })); + + return {action_summaries, id: asset_id}; + }); + + // Sort these metrics by the predefined sort order. This will ensure that + // if the action summary does not exist on the object, that it is less + // prefered over the one that does have it. + assetMetrics.sort((a, b) => { + let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sort)); + let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sort)); + + // If either a or b don't have this action type, then one of them will + // automatically win. + if (aActionSummary == null || bActionSummary == null) { + if (bActionSummary != null) { + return 1; + } + + if (aActionSummary != null) { + return -1; + } + + return 0; + } + + // Both of them had an actionCount, hence we can determine that we could + // compare the actual values directly. + return bActionSummary.actionCount - aActionSummary.actionCount; + }); + + // Only keep the top `limit`. + assetMetrics = assetMetrics.slice(0, limit); + + // Determine the assets that we need to return. + return Assets.getByID.loadMany(assetMetrics.map((asset) => asset.id)); + }) + .then((assets) => { + + // Join up the assets that are returned by their id. + let groupedAssets = _.groupBy(assets, 'id'); + + // Return from the sorted asset metrics and return their assetes. + return assetMetrics.map(({id, action_summaries}) => { + if (id in groupedAssets) { + let asset = groupedAssets[id][0]; + + // Add the action summaries to the asset. + asset.action_summaries = action_summaries; + + return asset; + } + + return null; + }).filter((asset) => asset != null); + }); +}; + +const getRecentActions = (context, {from, to}) => { return ActionModel.aggregate([ // Find all actions that were created in the time range. @@ -36,120 +127,47 @@ const getMetrics = (context, {from, to, sort, limit}) => { action_type: '$_id.action_type', count: '$count' }} - ]).then((actionSummaries) => { + ]); +}; - // Collect all the action summaries into a dictionary. - actionSummaries.forEach(({item_id, action_type, count}) => { - if (!(item_id in commentMetrics)) { - commentMetrics[item_id] = []; - } +const getSpecificComments = (context, ids) => { + return CommentModel.aggregate([ - commentMetrics[item_id].push({action_type, count}); - }); - - // Collect just the comment id's. - let commentIDs = _.uniq(actionSummaries.map((as) => as.item_id)); - - // Find those comments. - return CommentModel.aggregate([ - - // Get only those comments. - {$match: { - id: { - $in: commentIDs - } - }}, - - // Group by their asset id and push in the comment id. - {$group: { - _id: { - asset_id: '$asset_id' - }, - ids: { - $addToSet: '$id' - } - }}, - - // Project that data only as better fields. - {$project: { - asset_id: '$_id.asset_id', - ids: '$ids' - }} - ]); - }) - .then((commentResults) => { - - assetMetrics = commentResults.map(({ids, asset_id}) => { - let summaries = _.groupBy(_.flatten(ids.map((id) => commentMetrics[id])), 'action_type'); - - let action_summaries = Object.keys(summaries).map((action_type) => ({ - action_type, - actionCount: summaries[action_type].reduce((acc, {count}) => acc + count, 0), - actionableItemCount: summaries[action_type].length - })); - - return {action_summaries, id: asset_id}; - }); - - // Sort these metrics by the predefined sort order. This will ensure that - // if the action summary does not exist on the object, that it is less - // prefered over the one that does have it. - assetMetrics.sort((a, b) => { - let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sort)); - let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sort)); - - // If either a or b don't have this action type, then one of them will - // automatically win. - if (aActionSummary == null || bActionSummary == null) { - if (bActionSummary != null) { - return 1; - } - - if (aActionSummary != null) { - return -1; - } - - return 0; - } - - // Both of them had an actionCount, hence we can determine that we could - // compare the actual values directly. - return bActionSummary.actionCount - aActionSummary.actionCount; - }); - - // Only keep the top `limit`. - assetMetrics = assetMetrics.slice(0, limit); - - // Determine the assets that we need to return. - return AssetModel.find({ + // Get only those comments. + {$match: { id: { - $in: assetMetrics.map((asset) => asset.id) + $in: ids } - }); - }) - .then((assets) => { + }}, - // Join up the assets that are returned by their id. - let groupedAssets = _.groupBy(assets, 'id'); - - // Return from the sorted asset metrics and return their assetes. - return assetMetrics.map(({id, action_summaries}) => { - if (id in groupedAssets) { - let asset = groupedAssets[id][0]; - - // Add the action summaries to the asset. - asset.action_summaries = action_summaries; - - return asset; + // Group by their asset id and push in the comment id. + {$group: { + _id: { + asset_id: '$asset_id' + }, + ids: { + $addToSet: '$id' } + }}, - return null; - }).filter((asset) => asset != null); - }); + // Project that data only as better fields. + {$project: { + asset_id: '$_id.asset_id', + ids: '$ids' + }} + ]); }; module.exports = (context) => ({ Metrics: { + getSpecificComments: new DataLoader(([ids]) => getSpecificComments(context, ids).then((c) => [c]), { + batch: false, + cacheKeyFn: arrayCacheKeyFn + }), + getRecentActions: new DataLoader(([{from, to}]) => getRecentActions(context, {from, to}).then((as) => [as]), { + batch: false, + cacheKeyFn: objectCacheKeyFn('from', 'to') + }), get: ({from, to, sort, limit}) => getMetrics(context, {from, to, sort, limit}) } }); diff --git a/graph/loaders/util.js b/graph/loaders/util.js index 4640d8245..ab6f8a1f3 100644 --- a/graph/loaders/util.js +++ b/graph/loaders/util.js @@ -130,10 +130,20 @@ const objectCacheKeyFn = (...paths) => (obj) => { return paths.map((path) => obj[path]).join(':'); }; +/** + * Maps an object's paths to a string that can be used as a cache key. + * @param {Array} paths paths on the object to be used to generate the cache + * key + */ +const arrayCacheKeyFn = (arr) => { + return arr.sort().join(':'); +}; + module.exports = { singleJoinBy, arrayJoinBy, objectCacheKeyFn, + arrayCacheKeyFn, SingletonResolver, SharedCacheDataLoader };