mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 07:56:39 +08:00
removed backend for metrics
This commit is contained in:
@@ -79,11 +79,6 @@ const findOrCreateAssetByURL = async (context, asset_url) => {
|
||||
return asset;
|
||||
};
|
||||
|
||||
const getAssetsForMetrics = async ({loaders: {Comments}}) => {
|
||||
return Comments.getByQuery({action_type: 'FLAG'})
|
||||
.then((connection) => connection.nodes);
|
||||
};
|
||||
|
||||
const findByUrl = async (context, asset_url) => {
|
||||
|
||||
// Verify that the asset_url is parsable.
|
||||
@@ -111,7 +106,6 @@ module.exports = (context) => ({
|
||||
findByUrl: (url) => findByUrl(context, url),
|
||||
getByQuery: (query) => getAssetsByQuery(context, query),
|
||||
getByID: new DataLoader((ids) => genAssetsByID(context, ids)),
|
||||
getForMetrics: () => getAssetsForMetrics(context),
|
||||
getAll: new util.SingletonResolver(() => AssetModel.find({}))
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ const debug = require('debug')('talk:graph:loaders');
|
||||
const Actions = require('./actions');
|
||||
const Assets = require('./assets');
|
||||
const Comments = require('./comments');
|
||||
const Metrics = require('./metrics');
|
||||
const Settings = require('./settings');
|
||||
const Tags = require('./tags');
|
||||
const Users = require('./users');
|
||||
@@ -17,7 +16,6 @@ let loaders = [
|
||||
Actions,
|
||||
Assets,
|
||||
Comments,
|
||||
Metrics,
|
||||
Settings,
|
||||
Tags,
|
||||
Users,
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const DataLoader = require('dataloader');
|
||||
const {objectCacheKeyFn} = require('./util');
|
||||
|
||||
const ActionModel = require('../../models/action');
|
||||
const CommentModel = require('../../models/comment');
|
||||
|
||||
/**
|
||||
* Returns the assets which have had comments made within the last time period.
|
||||
*/
|
||||
const getAssetActivityMetrics = ({loaders: {Assets}}, {from, to, limit}) => {
|
||||
let assetMetrics = [];
|
||||
|
||||
return CommentModel.aggregate([
|
||||
{$match: {
|
||||
parent_id: null,
|
||||
created_at: {
|
||||
$gt: from,
|
||||
$lt: to
|
||||
}
|
||||
}},
|
||||
{$group: {
|
||||
_id: '$asset_id',
|
||||
commentCount: {
|
||||
$sum: 1
|
||||
}
|
||||
}},
|
||||
{$project: {
|
||||
_id: false,
|
||||
asset_id: '$_id',
|
||||
commentCount: '$commentCount'
|
||||
}},
|
||||
{$sort: {
|
||||
commentCount: -1
|
||||
}},
|
||||
{$limit: limit}
|
||||
])
|
||||
.then((results) => {
|
||||
assetMetrics = results;
|
||||
|
||||
return Assets.getByID.loadMany(results.map((result) => result.asset_id));
|
||||
})
|
||||
.then((assets) => assets.map((asset, i) => {
|
||||
|
||||
// We're leveraging the fact that the comments returned by the aggregation
|
||||
// query are in the request order that we just made, it's what the
|
||||
// Assets.getByID loader does.
|
||||
asset.commentCount = assetMetrics[i].commentCount;
|
||||
|
||||
return asset;
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a list of assets with action metadata included on the models.
|
||||
*/
|
||||
const getAssetMetrics = async ({loaders: {Metrics, Assets, Comments}}, {from, to, sortBy, limit}) => {
|
||||
|
||||
// Get the recent actions.
|
||||
let actionSummaries = await Metrics.getRecentActions.load({from, to});
|
||||
|
||||
let commentMetrics = actionSummaries.reduce((acc, {item_id, action_type, count}) => {
|
||||
if (action_type !== sortBy) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
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 = Object.keys(commentMetrics);
|
||||
|
||||
// Find those comments.
|
||||
let comments = await Comments.get.loadMany(commentIDs);
|
||||
|
||||
let commentResults = _.groupBy(comments, 'asset_id');
|
||||
|
||||
let assetMetrics = Object.keys(commentResults)
|
||||
.map((asset_id) => {
|
||||
let ids = commentResults[asset_id].map((comment) => comment.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};
|
||||
})
|
||||
|
||||
.filter((asset) => {
|
||||
let contextActionSummary = asset.action_summaries.find((({action_type}) => action_type === sortBy));
|
||||
if (contextActionSummary === null || contextActionSummary.actionCount === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
|
||||
// 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.
|
||||
.sort((a, b) => {
|
||||
let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sortBy));
|
||||
let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sortBy));
|
||||
|
||||
// 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.
|
||||
let assets = await Assets.getByID.loadMany(assetMetrics.map((asset) => asset.id));
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a list of comments that are retrieved based on most activity within
|
||||
* the indicated time range.
|
||||
*/
|
||||
const getCommentMetrics = async ({loaders: {Metrics, Comments}}, {from, to, sortBy, limit}) => {
|
||||
|
||||
let commentActionSummaries = {};
|
||||
|
||||
let actionSummaries = await Metrics.getRecentActions.load({from, to});
|
||||
|
||||
actionSummaries.sort((a, b) => {
|
||||
let aActionSummary = a.action_type === sortBy ? a : null;
|
||||
let bActionSummary = b.action_type === sortBy ? b : null;
|
||||
|
||||
// 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.count - aActionSummary.count;
|
||||
});
|
||||
|
||||
commentActionSummaries = _.groupBy(actionSummaries, 'item_id');
|
||||
|
||||
// Grab the comment id's for comment where they have at least one of the
|
||||
// actions being sorted by.
|
||||
let commentIDs = Object.keys(commentActionSummaries).filter((item_id) => {
|
||||
let contextActionSummary = commentActionSummaries[item_id].find(({action_type}) => action_type === sortBy);
|
||||
if (contextActionSummary == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Only keep the top `limit`.
|
||||
commentIDs = commentIDs.slice(0, limit);
|
||||
|
||||
// If there are no comment's to get, then just continue with an empty
|
||||
// array.
|
||||
if (commentIDs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Find those comments, this is the final stage, so let's get all the
|
||||
// fields.
|
||||
let comments = await Comments.get.loadMany(commentIDs);
|
||||
|
||||
return comments.map((comment) => {
|
||||
|
||||
// Add in the action summaries genrerated.
|
||||
comment.action_summaries = commentActionSummaries[comment.id];
|
||||
|
||||
return comment;
|
||||
});
|
||||
};
|
||||
|
||||
const getRecentActions = (context, {from, to}) => {
|
||||
return ActionModel.aggregate([
|
||||
|
||||
// Find all actions that were created in the time range.
|
||||
{$match: {
|
||||
item_type: 'COMMENTS',
|
||||
created_at: {
|
||||
$gt: from,
|
||||
$lt: to
|
||||
}
|
||||
}},
|
||||
|
||||
// Count all those items.
|
||||
{$group: {
|
||||
_id: {
|
||||
item_id: '$item_id',
|
||||
action_type: '$action_type'
|
||||
},
|
||||
count: {
|
||||
$sum: 1
|
||||
}
|
||||
}},
|
||||
|
||||
// Project the count to a better field.
|
||||
{$project: {
|
||||
item_id: '$_id.item_id',
|
||||
action_type: '$_id.action_type',
|
||||
count: '$count'
|
||||
}}
|
||||
]);
|
||||
};
|
||||
|
||||
module.exports = (context) => ({
|
||||
Metrics: {
|
||||
getRecentActions: new DataLoader(([{from, to}]) => getRecentActions(context, {from, to}).then((as) => [as]), {
|
||||
batch: false,
|
||||
cacheKeyFn: objectCacheKeyFn('from', 'to')
|
||||
}),
|
||||
Assets: {
|
||||
get: ({from, to, sortBy, limit}) => getAssetMetrics(context, {from, to, sortBy, limit}),
|
||||
getActivity: ({from, to, limit}) => getAssetActivityMetrics(context, {from, to, limit}),
|
||||
},
|
||||
Comments: {
|
||||
get: ({from, to, sortBy, limit}) => getCommentMetrics(context, {from, to, sortBy, limit}),
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
const {
|
||||
SEARCH_ASSETS,
|
||||
SEARCH_OTHERS_COMMENTS,
|
||||
SEARCH_COMMENT_METRICS,
|
||||
SEARCH_OTHER_USERS
|
||||
} = require('../../perms/constants');
|
||||
|
||||
@@ -58,27 +57,6 @@ const RootQuery = {
|
||||
return Users.getCountByQuery(query);
|
||||
},
|
||||
|
||||
assetMetrics(_, query, {user, loaders: {Metrics: {Assets}}}) {
|
||||
if (user == null || !user.can(SEARCH_ASSETS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {sortBy} = query;
|
||||
if (sortBy === 'ACTIVITY') {
|
||||
return Assets.getActivity(query);
|
||||
}
|
||||
|
||||
return Assets.get(query);
|
||||
},
|
||||
|
||||
commentMetrics(_, query, {user, loaders: {Metrics: {Comments}}}) {
|
||||
if (user == null || !user.can(SEARCH_COMMENT_METRICS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Comments.get(query);
|
||||
},
|
||||
|
||||
// This returns the current user, ensure that if we aren't logged in, we
|
||||
// return null.
|
||||
me(_, args, {user}) {
|
||||
|
||||
@@ -5,7 +5,6 @@ const {
|
||||
SEARCH_OTHER_USERS,
|
||||
SEARCH_OTHERS_COMMENTS,
|
||||
UPDATE_USER_ROLES,
|
||||
SEARCH_COMMENT_METRICS,
|
||||
VIEW_SUSPENSION_INFO,
|
||||
LIST_OWN_TOKENS
|
||||
} = require('../../perms/constants');
|
||||
@@ -79,7 +78,7 @@ const User = {
|
||||
|
||||
// Extract the reliability from the user metadata if they have permission.
|
||||
reliable(user, _, {user: requestingUser}) {
|
||||
if (requestingUser && requestingUser.can(SEARCH_COMMENT_METRICS)) {
|
||||
if (requestingUser && requestingUser.can(SEARCH_ACTIONS)) {
|
||||
return KarmaService.model(user);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -818,19 +818,6 @@ enum USER_STATUS {
|
||||
APPROVED
|
||||
}
|
||||
|
||||
# Metrics for the assets.
|
||||
enum ASSET_METRICS_SORT {
|
||||
|
||||
# Represents a FlagAction.
|
||||
FLAG
|
||||
|
||||
# Represents a don't agree action.
|
||||
DONTAGREE
|
||||
|
||||
# Represents activity.
|
||||
ACTIVITY
|
||||
}
|
||||
|
||||
type RootQuery {
|
||||
|
||||
# Site wide settings and defaults.
|
||||
@@ -865,14 +852,6 @@ type RootQuery {
|
||||
|
||||
# a single User by id
|
||||
user(id: ID!): User
|
||||
|
||||
# Asset metrics related to user actions are saturated into the assets
|
||||
# returned. Parameters `from` and `to` are related to the action created_at field.
|
||||
assetMetrics(from: Date!, to: Date!, sortBy: ASSET_METRICS_SORT!, limit: Int = 10): [Asset!]
|
||||
|
||||
# Comment metrics related to user actions are saturated into the comments
|
||||
# returned. Parameters `from` and `to` are related to the action created_at field.
|
||||
commentMetrics(from: Date!, to: Date!, sortBy: ACTION_TYPE!, limit: Int = 10): [Comment!]
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
||||
@@ -26,7 +26,6 @@ module.exports = {
|
||||
SEARCH_ACTIONS: 'SEARCH_ACTIONS',
|
||||
SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS: 'SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS',
|
||||
SEARCH_OTHERS_COMMENTS: 'SEARCH_OTHERS_COMMENTS',
|
||||
SEARCH_COMMENT_METRICS: 'SEARCH_COMMENT_METRICS',
|
||||
LIST_OWN_TOKENS: 'LIST_OWN_TOKENS',
|
||||
SEARCH_COMMENT_STATUS_HISTORY: 'SEARCH_COMMENT_STATUS_HISTORY',
|
||||
VIEW_SUSPENSION_INFO: 'VIEW_SUSPENSION_INFO',
|
||||
|
||||
@@ -13,8 +13,6 @@ module.exports = (user, perm) => {
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SEARCH_OTHERS_COMMENTS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SEARCH_COMMENT_METRICS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.LIST_OWN_TOKENS:
|
||||
return check(user, ['ADMIN']);
|
||||
case types.SEARCH_COMMENT_STATUS_HISTORY:
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const UserModel = require('../../../../models/user');
|
||||
const AssetModel = require('../../../../models/asset');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const ActionModel = require('../../../../models/action');
|
||||
const CommentModel = require('../../../../models/comment');
|
||||
|
||||
const {expect} = require('chai');
|
||||
|
||||
describe('graph.loaders.Metrics', () => {
|
||||
beforeEach(() => SettingsService.init());
|
||||
|
||||
describe('#Comments', () => {
|
||||
const query = `
|
||||
query CommentMetrics($from: Date!, $to: Date!) {
|
||||
flagged: commentMetrics(from: $from, to: $to, sortBy: FLAG) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
describe('different comment states', () => {
|
||||
|
||||
beforeEach(() => CommentModel.create([
|
||||
{id: '1', body: 'a new comment!'},
|
||||
{id: '2', body: 'a new comment!'},
|
||||
{id: '3', body: 'a new comment!'}
|
||||
]));
|
||||
|
||||
[
|
||||
{flagged: 0, actions: []},
|
||||
{flagged: 1, actions: [{action_type: 'FLAG', item_id: '1', item_type: 'COMMENTS'}]},
|
||||
{flagged: 1, actions: [
|
||||
{action_type: 'FLAG', item_id: '1', item_type: 'COMMENTS'},
|
||||
]},
|
||||
{flagged: 1, actions: [
|
||||
{action_type: 'FLAG', item_id: '3', item_type: 'COMMENTS'}
|
||||
]}
|
||||
].forEach(({flagged, actions}) => {
|
||||
|
||||
describe(`with actions=${actions.length}`, () => {
|
||||
|
||||
beforeEach(() => ActionModel.create(actions));
|
||||
|
||||
it(`returns the correct amount of metrics flagged=${flagged}`, () => {
|
||||
const context = new Context({user: new UserModel({roles: ['ADMIN']})});
|
||||
|
||||
return graphql(schema, query, {}, context, {
|
||||
from: (new Date()).setMinutes((new Date()).getMinutes() - 5),
|
||||
to: (new Date()).setMinutes((new Date()).getMinutes() + 5)
|
||||
})
|
||||
.then(({data, errors}) => {
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.flagged).to.have.length(flagged);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('#Assets', () => {
|
||||
const query = `
|
||||
fragment metrics on Asset {
|
||||
id
|
||||
action_summaries {
|
||||
actionCount
|
||||
actionableItemCount
|
||||
}
|
||||
}
|
||||
|
||||
query Metrics($from: Date!, $to: Date!) {
|
||||
assetsByFlag: assetMetrics(from: $from, to: $to, sortBy: FLAG) {
|
||||
...metrics
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
describe('different comment states', () => {
|
||||
|
||||
beforeEach(() => Promise.all([
|
||||
AssetModel.create([
|
||||
{id: 'a1', url: 'http://localhost:3030/article/1'},
|
||||
{id: 'a2', url: 'http://localhost:3030/article/2'}
|
||||
]),
|
||||
CommentModel.create([
|
||||
{id: 'c1', asset_id: 'a1', body: 'a new comment!'},
|
||||
{id: 'c2', asset_id: 'a1', body: 'a new comment!'},
|
||||
{id: 'c3', asset_id: 'a1', body: 'a new comment!'}
|
||||
])
|
||||
]));
|
||||
|
||||
[
|
||||
{flagged: 0, actions: []},
|
||||
{flagged: 1, actions: [{action_type: 'FLAG', item_id: 'c1', item_type: 'COMMENTS'}]},
|
||||
{flagged: 1, actions: [
|
||||
{action_type: 'FLAG', item_id: 'c1', item_type: 'COMMENTS'},
|
||||
]},
|
||||
{flagged: 1, actions: [
|
||||
{action_type: 'FLAG', item_id: 'c3', item_type: 'COMMENTS'}
|
||||
]}
|
||||
].forEach(({flagged, actions}) => {
|
||||
|
||||
describe(`with actions=${actions.length}`, () => {
|
||||
|
||||
beforeEach(() => ActionModel.create(actions));
|
||||
|
||||
it(`returns the correct amount of metrics flagged=${flagged}`, () => {
|
||||
const context = new Context({user: new UserModel({roles: ['ADMIN']})});
|
||||
|
||||
return graphql(schema, query, {}, context, {
|
||||
from: (new Date()).setMinutes((new Date()).getMinutes() - 5),
|
||||
to: (new Date()).setMinutes((new Date()).getMinutes() + 5)
|
||||
})
|
||||
.then(({data, errors}) => {
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.assetsByFlag).to.have.length(flagged);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user