diff --git a/src/core/server/graph/tenant/resolvers/asset.ts b/src/core/server/graph/tenant/resolvers/asset.ts index e5487ba08..95f05daa3 100644 --- a/src/core/server/graph/tenant/resolvers/asset.ts +++ b/src/core/server/graph/tenant/resolvers/asset.ts @@ -6,6 +6,7 @@ const Asset: GQLAssetTypeResolver = { ctx.loaders.Comments.forAsset(asset.id, input), // TODO: implement this. isClosed: () => false, + commentCounts: asset => asset.comment_counts, }; export default Asset; diff --git a/src/core/server/graph/tenant/resolvers/comment_counts.ts b/src/core/server/graph/tenant/resolvers/comment_counts.ts new file mode 100644 index 000000000..b88a54c3b --- /dev/null +++ b/src/core/server/graph/tenant/resolvers/comment_counts.ts @@ -0,0 +1,14 @@ +import { + GQLCOMMENT_STATUS, + GQLCommentCountsTypeResolver, +} from "talk-server/graph/tenant/schema/__generated__/types"; +import { CommentStatusCounts } from "talk-server/models/asset"; + +const CommentCounts: GQLCommentCountsTypeResolver = { + totalVisible: commentCounts => + commentCounts[GQLCOMMENT_STATUS.ACCEPTED] + + commentCounts[GQLCOMMENT_STATUS.NONE], + statuses: commentCounts => commentCounts, +}; + +export default CommentCounts; diff --git a/src/core/server/graph/tenant/resolvers/index.ts b/src/core/server/graph/tenant/resolvers/index.ts index f8754d04b..e1bf08143 100644 --- a/src/core/server/graph/tenant/resolvers/index.ts +++ b/src/core/server/graph/tenant/resolvers/index.ts @@ -4,6 +4,7 @@ import { GQLResolver } from "talk-server/graph/tenant/schema/__generated__/types import Asset from "./asset"; import AuthIntegrations from "./auth_integrations"; import Comment from "./comment"; +import CommentCounts from "./comment_counts"; import Mutation from "./mutation"; import Profile from "./profile"; import Query from "./query"; @@ -12,6 +13,7 @@ const Resolvers: GQLResolver = { Asset, AuthIntegrations, Comment, + CommentCounts, Cursor, Mutation, Profile, diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index fdc17718c..34d0f4854 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -265,7 +265,7 @@ type KarmaThreshold { type KarmaThresholds { """ flag represents karma settings in relation to how well a User's flagging - ability aligns with the moderation decicions made by moderators. + ability aligns with the moderation decisions made by moderators. """ flag: KarmaThreshold! @@ -284,7 +284,7 @@ type Karma { """ karmaThresholds contains the currently set thresholds for triggering Trust - beheviour. + behavior. """ thresholds: KarmaThresholds! } @@ -741,6 +741,50 @@ type CommentsConnection { pageInfo: PageInfo! } +################################################################################ +## CommentCounts +################################################################################ + +type CommentStatusCounts { + """ + The comment is not PREMOD, but was not applied a moderation status by a + moderator. + """ + NONE: Int! + + """ + The comment has been accepted by a moderator. + """ + ACCEPTED: Int! + + """ + The comment has been rejected by a moderator. + """ + REJECTED: Int! + + """ + The comment was created while the asset's premoderation option was on, and + new comments that haven't been moderated yet are referred to as + "premoderated" or "premod" comments. + """ + PREMOD: Int! + + """ + SYSTEM_WITHHELD represents a comment that was withheld by the system because + it was flagged by an internal process for further review. + """ + SYSTEM_WITHHELD: Int! +} + +type CommentCounts { + """ + totalVisible will return the count of all visible Comments. + """ + totalVisible: Int! + + statuses: CommentStatusCounts! @auth(roles: [ADMIN, MODERATOR]) +} + ################################################################################ ## Asset ################################################################################ @@ -795,6 +839,11 @@ type Asset { """ isClosed: Boolean! + """ + commentCounts stores all the counts of Comments that are left on the Comment. + """ + commentCounts: CommentCounts! + """ createdAt is the date that the Asset was created at. """ @@ -1119,7 +1168,7 @@ input SettingsKarmaThresholdInput { input SettingsKarmaThresholdsInput { """ flag represents karma settings in relation to how well a User's flagging - ability aligns with the moderation decicions made by moderators. + ability aligns with the moderation decisions made by moderators. """ flag: SettingsKarmaThresholdInput @@ -1138,7 +1187,7 @@ input SettingsKarmaInput { """ karmaThresholds contains the currently set thresholds for triggering Trust - beheviour. + behavior. """ thresholds: SettingsKarmaThresholdsInput } diff --git a/src/core/server/models/asset.ts b/src/core/server/models/asset.ts index 8d412c522..b2d5bd866 100644 --- a/src/core/server/models/asset.ts +++ b/src/core/server/models/asset.ts @@ -1,9 +1,9 @@ -import { defaults } from "lodash"; import { Db } from "mongodb"; import uuid from "uuid"; import { Omit } from "talk-common/types"; import { dotize } from "talk-common/utils/dotize"; +import { GQLCOMMENT_STATUS } from "talk-server/graph/tenant/schema/__generated__/types"; import { ModerationSettings } from "talk-server/models/settings"; import { TenantResource } from "talk-server/models/tenant"; @@ -11,12 +11,24 @@ function collection(db: Db) { return db.collection>("assets"); } +// TODO: (wyattjoh) write a test to verify that this set of counts is always in sync with GQLCOMMENT_STATUS. +export interface CommentStatusCounts { + [GQLCOMMENT_STATUS.ACCEPTED]: number; + [GQLCOMMENT_STATUS.NONE]: number; + [GQLCOMMENT_STATUS.PREMOD]: number; + [GQLCOMMENT_STATUS.REJECTED]: number; + [GQLCOMMENT_STATUS.SYSTEM_WITHHELD]: number; +} + export interface Asset extends TenantResource { readonly id: string; url: string; scraped?: Date; closedAt?: Date; closedMessage?: string; + created_at: Date; + modified_date?: Date; + title?: string; description?: string; image?: string; @@ -24,11 +36,15 @@ export interface Asset extends TenantResource { subsection?: string; author?: string; publication_date?: Date; - modified_date?: Date; - created_at: Date; /** - * settings provides a point where the settings can be overriden for a + * comment_counts stores the different counts for each comment on the Asset + * according to their statuses. + */ + comment_counts: CommentStatusCounts; + + /** + * settings provides a point where the settings can be overridden for a * specific Asset. */ settings?: Partial; @@ -51,23 +67,22 @@ export async function upsertAsset( // Create the asset, optionally sourcing the id from the input, additionally // porting in the tenant_id. const update: { $setOnInsert: Asset } = { - $setOnInsert: defaults( - { - url, - tenant_id: tenantID, - created_at: now, - }, - { id }, - { - id: uuid.v4(), - } - ), + $setOnInsert: { + id: id ? id : uuid.v4(), + url, + tenant_id: tenantID, + created_at: now, + comment_counts: createEmptyCommentCounts(), + }, }; // Perform the find and update operation to try and find and or create the // asset. const { value: asset } = await collection(db).findOneAndUpdate( - { url }, + { + url, + tenant_id: tenantID, + }, update, { // Create the object if it doesn't already exist. @@ -89,6 +104,48 @@ export async function upsertAsset( return asset; } +/** + * updateCommentStatusCount increments the number of status counts for the + * given Asset ID. + * + * @param mongo the database handle + * @param tenantID the tenant that the Asset is on. + * @param id the ID of the Asset. + * @param commentStatusCounts the update document that contains a positive or + * negative number of comments to increment on the given Asset. + */ +export async function updateCommentStatusCount( + mongo: Db, + tenantID: string, + id: string, + commentStatusCounts: Partial +) { + const { value: asset } = await collection(mongo).findOneAndUpdate( + { + id, + tenant_id: tenantID, + }, + // Update all the specific comment status counts that are associated with + // each of the counts. + { $inc: dotize({ comment_counts: commentStatusCounts }) }, + // False to return the updated document instead of the original + // document. + { returnOriginal: false } + ); + + return asset; +} + +function createEmptyCommentCounts(): CommentStatusCounts { + return { + [GQLCOMMENT_STATUS.ACCEPTED]: 0, + [GQLCOMMENT_STATUS.NONE]: 0, + [GQLCOMMENT_STATUS.PREMOD]: 0, + [GQLCOMMENT_STATUS.REJECTED]: 0, + [GQLCOMMENT_STATUS.SYSTEM_WITHHELD]: 0, + }; +} + export interface FindOrCreateAssetInput { id?: string; url?: string; diff --git a/src/core/server/services/comments/index.ts b/src/core/server/services/comments/index.ts index 4d9151818..80b3b0408 100644 --- a/src/core/server/services/comments/index.ts +++ b/src/core/server/services/comments/index.ts @@ -1,7 +1,10 @@ import { Db } from "mongodb"; import { Omit } from "talk-common/types"; -import { retrieveAsset } from "talk-server/models/asset"; +import { + retrieveAsset, + updateCommentStatusCount, +} from "talk-server/models/asset"; import { createComment, CreateCommentInput, @@ -84,6 +87,11 @@ export async function create( ); } + // Increment the status count for the particular status on the Asset. + await updateCommentStatusCount(mongo, tenant.id, asset.id, { + [status]: 1, + }); + return comment; } @@ -100,7 +108,7 @@ export async function edit( req?: Request ) { // Get the comment that we're editing. - let comment = await retrieveComment(mongo, tenant.id, input.id); + const comment = await retrieveComment(mongo, tenant.id, input.id); if (!comment) { // TODO: replace to match error returned by the models/comments.ts throw new Error("comment not found"); @@ -124,7 +132,7 @@ export async function edit( // TODO: (wyattjoh) use the actions somehow. - comment = await editComment(mongo, tenant.id, { + const editedComment = await editComment(mongo, tenant.id, { id: input.id, author_id: author.id, body: input.body, @@ -139,5 +147,14 @@ export async function edit( ), }); - return comment; + if (comment.status !== editedComment.status) { + // Increment the status count for the particular status on the Asset, and + // decrement the status on the comment's previous status. + await updateCommentStatusCount(mongo, tenant.id, asset.id, { + [comment.status]: -1, + [editedComment.status]: 1, + }); + } + + return editedComment; }