[CORL-688] Add user comment count tracking (#2744)

* feat: initial impl

* Create preliminary comment moderation slices

CORL-688

* Move slices logic into stacks

CORL-688

* Create user comment counts

CORL-688

* Create naive mutation that initializes user comment counts

CORL-688

* Use bulk updates in user counts migration

CORL-688

* fix: review

* fix: fixed issue with aggregation

* Migrate creating comment into stacks

CORL-688

* Migrate editing a comment to the stacks

CORL-688

* Break publishing comment status out of updateAllCounts

CORL-688

* review: removed variable scoping in favor of export

* revert: feb8e8196cd448f5cd24f1ca2eb0b91fe9bd43c7

* review: simplification of stacks implementation

This simplifies the stacks implementation to better reuse code related
to count management and event publishing. This can be used to great
effect with the upcomming events PR #2738.

* fix: check if authorID is null before update user counts

CORL-688

Co-authored-by: Wyatt Johnson <accounts+github@wyattjoh.ca>
This commit is contained in:
Nick Funk
2020-01-07 21:00:25 +00:00
committed by Wyatt Johnson
co-authored by Wyatt Johnson
parent 0dc3e8968a
commit e3e2e0f52e
21 changed files with 987 additions and 867 deletions
@@ -56,7 +56,8 @@ export type CreateCommentModerationActionInput = Omit<
export async function createCommentModerationAction(
mongo: Db,
tenantID: string,
input: CreateCommentModerationActionInput
input: CreateCommentModerationActionInput,
now: Date
) {
// default are the properties set by the application when a new comment
// moderation action is created.
@@ -66,7 +67,7 @@ export async function createCommentModerationAction(
> = {
id: uuid.v4(),
tenantID,
createdAt: new Date(),
createdAt: now,
};
// Merge the defaults and the input together.
+23 -26
View File
@@ -1,4 +1,4 @@
import { isEmpty, merge } from "lodash";
import { isEmpty } from "lodash";
import { Db } from "mongodb";
import performanceNow from "performance-now";
import uuid from "uuid";
@@ -253,19 +253,19 @@ export function validateEditable(
export interface EditComment {
/**
* oldComment is the Comment that was previously set.
* before is the comment before the edit.
*/
oldComment: Comment;
before: Readonly<Comment>;
/**
* editedComment is the Comment after the edit was performed.
* after is the comment after the edit.
*/
editedComment: Comment;
after: Readonly<Comment>;
/**
* newRevision returns the new revision that was created in the Comment.
* revision is the new revision generated.
*/
newRevision: Revision;
revision: Readonly<Revision>;
}
/**
@@ -345,25 +345,25 @@ export async function editComment(
throw new Error("comment edit failed for an unexpected reason");
}
// Create a new "editedComment" where the same changes were applied to it as
// Create a new "after" where the same changes were applied to it as
// we did to the MongoDB document.
const editedComment: Comment = merge({}, result.value, {
// Add in all the $set operations.
const after: Comment = {
...result.value,
// $set status
status,
metadata,
// Merge the actionCounts from the old Comment with the new actionCounts.
// $inc actionCounts
actionCounts: mergeCommentActionCounts(
result.value.actionCounts,
actionCounts
),
// Add in the $push operations.
// $push revisions
revisions: [...result.value.revisions, revision],
});
};
return {
oldComment: result.value,
editedComment,
newRevision: revision,
before: result.value,
after,
revision,
};
}
@@ -709,14 +709,14 @@ function applyInputToQuery(
export interface UpdateCommentStatus {
/**
* comment is the updated Comment with the new status associated with it.
* before is the comment before editing the status.
*/
comment: Readonly<Comment>;
before: Readonly<Comment>;
/**
* oldStatus is the previous status that the given Comment had.
* after is the comment after editing the status.
*/
oldStatus: GQLCOMMENT_STATUS;
after: Readonly<Comment>;
}
export async function updateCommentStatus(
@@ -748,15 +748,12 @@ export async function updateCommentStatus(
return null;
}
// Grab the old status.
const oldStatus = result.value.status;
return {
comment: {
before: result.value,
after: {
...result.value,
status,
},
oldStatus,
};
}
+1 -3
View File
@@ -126,8 +126,6 @@ export const updateStoryActionCounts = (
action: EncodedCommentActionCounts
) => updateStoryCounts(mongo, redis, tenantID, id, { action });
export type StoryCounts = DeepPartial<StoryCommentCounts>;
/**
* updateStoryCounts will update the comment counts for the story indicated.
*
@@ -142,7 +140,7 @@ export async function updateStoryCounts(
redis: AugmentedRedis,
tenantID: string,
id: string,
commentCounts: StoryCounts
commentCounts: DeepPartial<StoryCommentCounts>
) {
// Update all the specific comment moderation queue counts.
const update: DeepPartial<Story> = { commentCounts };
@@ -2,10 +2,11 @@ import { flatten, flattenDeep, identity, isEmpty, pickBy } from "lodash";
import { Db } from "mongodb";
import ms from "ms";
import { DeepPartial } from "coral-common/types";
import logger from "coral-server/logger";
import {
CommentModerationCountsPerQueue,
StoryCounts,
StoryCommentCounts,
} from "coral-server/models/story/counts";
import { stories as collection } from "coral-server/services/mongodb/collections";
import { AugmentedPipeline, AugmentedRedis } from "coral-server/services/redis";
@@ -180,7 +181,7 @@ export async function retrieveSharedModerationQueueQueuesCounts(
export async function updateSharedCommentCounts(
redis: AugmentedRedis,
tenantID: string,
commentCounts: StoryCounts
commentCounts: DeepPartial<StoryCommentCounts>
) {
const pipeline: AugmentedPipeline = redis.pipeline();
+54 -10
View File
@@ -1,4 +1,5 @@
import bcrypt from "bcryptjs";
import { identity, isEmpty, pickBy } from "lodash";
import { DateTime, DurationObject } from "luxon";
import { Db, MongoError } from "mongodb";
import uuid from "uuid";
@@ -19,16 +20,6 @@ import {
UsernameAlreadySetError,
UserNotFoundError,
} from "coral-server/errors";
import {
GQLBanStatus,
GQLDIGEST_FREQUENCY,
GQLPremodStatus,
GQLSuspensionStatus,
GQLTimeRange,
GQLUSER_ROLE,
GQLUsernameStatus,
GQLUserNotificationSettings,
} from "coral-server/graph/tenant/schema/__generated__/types";
import logger from "coral-server/logger";
import {
Connection,
@@ -40,6 +31,21 @@ import { TenantResource } from "coral-server/models/tenant";
import { DigestibleTemplate } from "coral-server/queue/tasks/mailer/templates";
import { users as collection } from "coral-server/services/mongodb/collections";
import {
GQLBanStatus,
GQLDIGEST_FREQUENCY,
GQLPremodStatus,
GQLSuspensionStatus,
GQLTimeRange,
GQLUSER_ROLE,
GQLUsernameStatus,
GQLUserNotificationSettings,
} from "coral-server/graph/tenant/schema/__generated__/types";
import {
CommentStatusCounts,
createEmptyCommentStatusCounts,
} from "../comment";
import { getLocalProfile, hasLocalProfile } from "./helpers";
export interface LocalProfile {
@@ -352,6 +358,10 @@ export interface Digest {
createdAt: Date;
}
export interface UserCommentCounts {
status: CommentStatusCounts;
}
/**
* User is someone that leaves Comments, and logs in.
*/
@@ -461,6 +471,8 @@ export interface User extends TenantResource {
* deletedAt is the time that this user was deleted from our system.
*/
deletedAt?: Date;
commentCounts: UserCommentCounts;
}
function hashPassword(password: string): Promise<string> {
@@ -514,6 +526,9 @@ async function findOrCreateUserInput(
moderatorNotes: [],
digests: [],
createdAt: now,
commentCounts: {
status: createEmptyCommentStatusCounts(),
},
};
if (input.username) {
@@ -2446,3 +2461,32 @@ export async function deleteModeratorNote(
}
return result.value;
}
export async function updateUserCommentCounts(
mongo: Db,
tenantID: string,
id: string,
commentCounts: DeepPartial<UserCommentCounts>
) {
// Update all the specific comment moderation queue counts.
const update: DeepPartial<User> = { commentCounts };
const $inc = pickBy(dotize(update), identity);
if (isEmpty($inc)) {
// Nothing needs to be incremented, just return the User.
return retrieveUser(mongo, tenantID, id);
}
logger.trace({ update: { $inc } }, "incrementing user comment counts");
const result = await collection(mongo).findOneAndUpdate(
{ id, tenantID },
{ $inc },
{
// False to return the updated document instead of the original
// document.
returnOriginal: false,
}
);
return result.value || null;
}