feat: added action counts to graph

This commit is contained in:
Wyatt Johnson
2018-09-20 23:35:56 -06:00
parent af22b9808b
commit be3ef78875
7 changed files with 181 additions and 45 deletions
+11 -13
View File
@@ -1647,9 +1647,9 @@
}
},
"@types/bson": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@types/bson/-/bson-1.0.10.tgz",
"integrity": "sha512-gRf+Qy5Qiyjz28ZkPRP37bDHtGG67op/lV2qcIMhWUq4vIMJ6/j13ajeYH7LFhJ5RNflyLHmdANPGDXZ5a8EzQ==",
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@types/bson/-/bson-1.0.11.tgz",
"integrity": "sha512-j+UcCWI+FsbI5/FQP/Kj2CXyplWAz39ktHFkXk84h7dNblKRSoNJs95PZFRd96NQGqsPEPgeclqnznWZr14ZDA==",
"dev": true,
"requires": {
"@types/node": "*"
@@ -1989,9 +1989,9 @@
}
},
"@types/mongodb": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.1.1.tgz",
"integrity": "sha512-lHEH+OwYNeuC28jlmdPT/wBAVMuB6M1sHjZKAtaho/LeJf78ILJPYUq2OD7mWVj9QR7uYiKVt4ExGkXegHFCJQ==",
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.1.7.tgz",
"integrity": "sha512-ljS4mE9o3apEkI59pftdnLf3b1ZczMPtXWp1myrhR+E/CLk0O5SjbTt6Rn3OxMB+Qc2eAytrQVufa4y1pFqE2A==",
"dev": true,
"requires": {
"@types/bson": "*",
@@ -10222,8 +10222,11 @@
"resolved": "https://registry.npmjs.org/fluent-intl-polyfill/-/fluent-intl-polyfill-0.1.0.tgz",
"integrity": "sha1-ETOUSrJHeINHOZVZaIPg05z4hc8=",
"dev": true,
"requires": {
"intl-pluralrules": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b"
"dependencies": {
"intl-pluralrules": {
"version": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b",
"from": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b"
}
}
},
"fluent-langneg": {
@@ -12667,11 +12670,6 @@
"integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=",
"dev": true
},
"intl-pluralrules": {
"version": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b",
"from": "github:projectfluent/IntlPluralRules#module",
"dev": true
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+1 -1
View File
@@ -117,7 +117,7 @@
"@types/lodash": "^4.14.111",
"@types/luxon": "^0.5.3",
"@types/mini-css-extract-plugin": "^0.2.0",
"@types/mongodb": "^3.1.1",
"@types/mongodb": "^3.1.7",
"@types/ms": "^0.7.30",
"@types/node": "^10.5.2",
"@types/node-fetch": "^2.1.2",
@@ -6,6 +6,11 @@ import {
CommentToRepliesArgs,
GQLCOMMENT_SORT,
} from "talk-server/graph/tenant/schema/__generated__/types";
import {
ACTION_ITEM_TYPE,
ActionCounts,
retrieveManyAuthoredActionCounts,
} from "talk-server/models/actions";
import {
retrieveCommentAssetConnection,
retrieveCommentRepliesConnection,
@@ -16,6 +21,17 @@ export default (ctx: Context) => ({
comment: new DataLoader((ids: string[]) =>
retrieveManyComments(ctx.mongo, ctx.tenant.id, ids)
),
retrieveAuthoredActionCounts: new DataLoader<string, ActionCounts>(
(itemIDs: string[]) =>
retrieveManyAuthoredActionCounts(
ctx.mongo,
ctx.tenant.id,
// This should only ever be accessed when a user is logged in.
ctx.user!.id,
ACTION_ITEM_TYPE.COMMENTS,
itemIDs
)
),
forAsset: (
assetID: string,
// Apply the graph schema defaults at the loader.
@@ -1,4 +1,5 @@
import { GQLCommentTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { decodeActionCounts } from "talk-server/models/actions";
import { Comment } from "talk-server/models/comment";
const Comment: GQLCommentTypeResolver<Comment> = {
@@ -17,6 +18,9 @@ const Comment: GQLCommentTypeResolver<Comment> = {
ctx.loaders.Users.user.load(comment.author_id),
replies: (comment, input, ctx) =>
ctx.loaders.Comments.forParent(comment.asset_id, comment.id, input),
actionCounts: comment => decodeActionCounts(comment.action_counts),
authoredActionCounts: (comment, input, ctx) =>
ctx.loaders.Comments.retrieveAuthoredActionCounts.load(comment.id),
};
export default Comment;
@@ -210,6 +210,37 @@ type CommentDontAgreeAction implements CommentAction {
createdAt: Time!
}
type ReactionActionCounts {
total: Int!
}
type DontAgreeActionCounts {
total: Int!
}
type FlagReasonActionCounts {
COMMENT_REPORTED_OFFENSIVE: Int!
COMMENT_REPORTED_SPAM: Int!
COMMENT_DETECTED_TOXIC: Int!
COMMENT_DETECTED_SPAM: Int!
COMMENT_DETECTED_BODY_COUNT: Int!
COMMENT_DETECTED_TRUST: Int!
COMMENT_DETECTED_LINKS: Int!
COMMENT_DETECTED_BANNED_WORD: Int!
COMMENT_DETECTED_SUSPECT_WORD: Int!
}
type FlagActionCounts {
total: Int!
reasons: FlagReasonActionCounts!
}
type ActionCounts {
reaction: ReactionActionCounts!
dontagree: DontAgreeActionCounts!
flag: FlagActionCounts!
}
################################################################################
## Settings
################################################################################
@@ -829,6 +860,17 @@ type Comment {
editing returns details about the edit status of a Comment.
"""
editing: EditInfo!
"""
actionCounts stores the counts of all the actions for the Comment.
"""
actionCounts: ActionCounts!
"""
authoredActionCounts stores the counts of all the actions for the Comment as
it is written by the current User.
"""
authoredActionCounts: ActionCounts! @auth
}
type PageInfo {
@@ -996,7 +1038,7 @@ type Query {
"""
me is the current logged in User.
"""
me: User
me: User @auth
"""
settings is the Settings for a given Tenant.
@@ -0,0 +1,24 @@
import {
GQLCOMMENT_FLAG_REASON,
GQLFlagReasonActionCounts,
} from "talk-server/graph/tenant/schema/__generated__/types";
type ExtractKeys<T> = { [P in keyof T]: P }[keyof T];
type A = ExtractKeys<typeof GQLCOMMENT_FLAG_REASON>;
type B = ExtractKeys<GQLFlagReasonActionCounts>;
// These tests ensure that the enums contained in GQLCOMMENT_FLAG_REASON are
// also defined on the GQLFlagReasonActionCounts type.
describe("GQLFlagReasonActionCounts", () => {
it("contains all the flag enum types", () => {
const a: A = "" as any;
let b: B = "" as any;
b = a;
expect(b).toBe("");
let c: A = "" as any;
const d: B = "" as any;
c = d;
expect(c).toBe("");
});
});
+82 -30
View File
@@ -193,6 +193,40 @@ export async function createActions(
return Promise.all(inputs.map(input => createAction(mongo, tenantID, input)));
}
/**
* retrieveManyAuthoredActionCounts returns the action counts for a specific
* user.
*/
export async function retrieveManyAuthoredActionCounts(
mongo: Db,
tenantID: string,
userID: string | null,
itemType: ACTION_ITEM_TYPE,
itemIDs: string[]
) {
const cursor = await collection(mongo).find({
tenant_id: tenantID,
user_id: userID,
item_type: itemType,
item_id: { $in: itemIDs },
});
const actions = await cursor.toArray();
// For each of the actions returned by the query, group the actions by the
// item id. Then compute the action counts for each of these action groups,
// which can be used to determine existence of actions.
return itemIDs
.map(itemID => actions.filter(action => action.item_id === itemID))
.map(itemActions =>
itemActions.reduce(
(actionCounts, { action_type, reason }) =>
incrementActionCounts(actionCounts, action_type, reason),
createEmptyActionCounts()
)
);
}
export type DeleteActionInput = Pick<
Action,
"action_type" | "item_type" | "item_id" | "reason" | "user_id"
@@ -349,16 +383,10 @@ function decodeActionCountKey(key: string): DecodedActionCountKey {
}
/**
* decodeActionCounts will take the encoded action counts and decode them into
* a useable format.
*
* @param encodedActionCounts the action counts to decode
* createEmptyActionCounts creates a default/empty set of action counts.
*/
export function decodeActionCounts(
encodedActionCounts: EncodedActionCounts
): ActionCounts {
// Default all the action counts to zero.
const actionCounts: ActionCounts = {
function createEmptyActionCounts(): ActionCounts {
return {
[ACTION_TYPE.REACTION]: {
total: 0,
},
@@ -376,35 +404,59 @@ export function decodeActionCounts(
) as Record<GQLCOMMENT_FLAG_REASON, number>,
},
};
}
/**
* decodeActionCounts will take the encoded action counts and decode them into
* a useable format.
*
* @param encodedActionCounts the action counts to decode
*/
export function decodeActionCounts(
encodedActionCounts: EncodedActionCounts
): ActionCounts {
// Default all the action counts to zero.
const actionCounts: ActionCounts = createEmptyActionCounts();
// Loop over all the encoded action counts to extract each of the action
// counts as they are encoded.
Object.entries(encodedActionCounts).forEach(([key, count]) => {
Object.entries(encodedActionCounts).map(([key, count]) => {
// Pull out the action type and the reason from the key.
const { actionType, reason } = decodeActionCountKey(key);
// Handle the different types and reasons.
switch (actionType) {
case ACTION_TYPE.REACTION:
actionCounts[ACTION_TYPE.REACTION].total += count;
break;
case ACTION_TYPE.DONT_AGREE:
actionCounts[ACTION_TYPE.DONT_AGREE].total += count;
break;
case ACTION_TYPE.FLAG:
// When we have a reason, we are incrementing for that particular reason
// rather than incrementing for the total. If we don't have a reason, we
// just got the updated reason.
if (reason) {
actionCounts[ACTION_TYPE.FLAG].reasons[reason] += count;
} else {
actionCounts[ACTION_TYPE.FLAG].total += count;
}
break;
default:
throw new Error("unexpected action type");
}
incrementActionCounts(actionCounts, actionType, reason, count);
});
return actionCounts;
}
function incrementActionCounts(
actionCounts: ActionCounts,
actionType: ACTION_TYPE,
reason: GQLCOMMENT_FLAG_REASON | undefined,
count: number = 1
) {
switch (actionType) {
case ACTION_TYPE.REACTION:
actionCounts[ACTION_TYPE.REACTION].total += count;
break;
case ACTION_TYPE.DONT_AGREE:
actionCounts[ACTION_TYPE.DONT_AGREE].total += count;
break;
case ACTION_TYPE.FLAG:
// When we have a reason, we are incrementing for that particular reason
// rather than incrementing for the total. If we don't have a reason, we
// just got the updated reason.
if (reason) {
actionCounts[ACTION_TYPE.FLAG].reasons[reason] += count;
} else {
actionCounts[ACTION_TYPE.FLAG].total += count;
}
break;
default:
throw new Error("unexpected action type");
}
return actionCounts;
}