[next] Comment Moderation Actions (#2068)

* fix: renamed snake case to camel case

* fix: changed case for mutators

* fix: renamed all snake case to camel case for db

* feat: added support for comment revisions + split comment actions

* fix: updated tests

* feat: implemented CommentModerationAction

* fix: fixed case issues

* feat: enabled WeakMap for wordList processsing

* chore: npm audit
This commit is contained in:
Wyatt Johnson
2018-11-21 17:42:47 +01:00
committed by Kiwi
parent 13147c4ba4
commit 21e1a5cbef
66 changed files with 2323 additions and 2224 deletions
@@ -0,0 +1,23 @@
import TenantContext from "talk-server/graph/tenant/context";
import {
CommentModerationActionFilter,
retrieveCommentModerationActionConnection,
retrieveCommentModerationActions,
} from "talk-server/models/action/moderation/comment";
import { UserToCommentModerationActionHistoryArgs } from "../schema/__generated__/types";
export default (ctx: TenantContext) => ({
commentModerationActions: (filter: CommentModerationActionFilter) =>
retrieveCommentModerationActions(ctx.mongo, ctx.tenant.id, filter),
commentModerationActionsConnection: (
{ first = 10, after }: UserToCommentModerationActionHistoryArgs,
moderatorID: string
) =>
retrieveCommentModerationActionConnection(ctx.mongo, ctx.tenant.id, {
first,
after,
filter: {
moderatorID,
},
}),
});
@@ -8,10 +8,7 @@ import {
GQLCOMMENT_SORT,
StoryToCommentsArgs,
} from "talk-server/graph/tenant/schema/__generated__/types";
import {
ACTION_ITEM_TYPE,
retrieveManyUserActionPresence,
} from "talk-server/models/action";
import { retrieveManyUserActionPresence } from "talk-server/models/action/comment";
import {
Comment,
retrieveCommentParentsConnection,
@@ -44,14 +41,13 @@ export default (ctx: Context) => ({
retrieveManyComments(ctx.mongo, ctx.tenant.id, ids)
),
retrieveMyActionPresence: new DataLoader<string, GQLActionPresence>(
(itemIDs: string[]) =>
(commentIDs: string[]) =>
retrieveManyUserActionPresence(
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
commentIDs
)
),
forUser: (
@@ -1,12 +1,14 @@
import Context from "talk-server/graph/tenant/context";
import Auth from "./auth";
import Comments from "./comments";
import Stories from "./stories";
import Users from "./users";
import Actions from "./Actions";
import Auth from "./Auth";
import Comments from "./Comments";
import Stories from "./Stories";
import Users from "./Users";
export default (ctx: Context) => ({
Auth: Auth(ctx),
Actions: Actions(ctx),
Stories: Stories(ctx),
Comments: Comments(ctx),
Users: Users(ctx),
@@ -0,0 +1,21 @@
import TenantContext from "talk-server/graph/tenant/context";
import { accept, reject } from "talk-server/services/moderation";
import {
GQLAcceptCommentInput,
GQLRejectCommentInput,
} from "../schema/__generated__/types";
export const Actions = (ctx: TenantContext) => ({
acceptComment: (input: GQLAcceptCommentInput) =>
accept(ctx.mongo, ctx.tenant, {
commentID: input.commentID,
commentRevisionID: input.commentRevisionID,
moderatorID: ctx.user!.id,
}),
rejectComment: (input: GQLRejectCommentInput) =>
reject(ctx.mongo, ctx.tenant, {
commentID: input.commentID,
commentRevisionID: input.commentRevisionID,
moderatorID: ctx.user!.id,
}),
});
@@ -19,54 +19,62 @@ import {
removeReaction,
} from "talk-server/services/comments/actions";
export default (ctx: TenantContext) => ({
create: (input: GQLCreateCommentInput) =>
export const Comment = (ctx: TenantContext) => ({
create: ({ storyID, body, parentID }: GQLCreateCommentInput) =>
create(
ctx.mongo,
ctx.tenant,
ctx.user!,
{
author_id: ctx.user!.id,
story_id: input.storyID,
body: input.body,
parent_id: input.parentID,
},
{ authorID: ctx.user!.id, storyID, body, parentID },
ctx.req
),
edit: (input: GQLEditCommentInput) =>
edit: ({ commentID, body }: GQLEditCommentInput) =>
edit(
ctx.mongo,
ctx.tenant,
ctx.user!,
{
id: input.commentID,
body: input.body,
id: commentID,
body,
},
ctx.req
),
createReaction: (input: GQLCreateCommentReactionInput) =>
createReaction: ({
commentID,
commentRevisionID,
}: GQLCreateCommentReactionInput) =>
createReaction(ctx.mongo, ctx.tenant, ctx.user!, {
item_id: input.commentID,
commentID,
commentRevisionID,
}),
removeReaction: (input: GQLRemoveCommentReactionInput) =>
removeReaction: ({ commentID }: GQLRemoveCommentReactionInput) =>
removeReaction(ctx.mongo, ctx.tenant, ctx.user!, {
item_id: input.commentID,
commentID,
}),
createDontAgree: (input: GQLCreateCommentDontAgreeInput) =>
createDontAgree: ({
commentID,
commentRevisionID,
}: GQLCreateCommentDontAgreeInput) =>
createDontAgree(ctx.mongo, ctx.tenant, ctx.user!, {
item_id: input.commentID,
commentID,
commentRevisionID,
}),
removeDontAgree: (input: GQLRemoveCommentDontAgreeInput) =>
removeDontAgree: ({ commentID }: GQLRemoveCommentDontAgreeInput) =>
removeDontAgree(ctx.mongo, ctx.tenant, ctx.user!, {
item_id: input.commentID,
commentID,
}),
createFlag: (input: GQLCreateCommentFlagInput) =>
createFlag: ({
commentID,
commentRevisionID,
reason,
}: GQLCreateCommentFlagInput) =>
createFlag(ctx.mongo, ctx.tenant, ctx.user!, {
item_id: input.commentID,
reason: input.reason,
commentID,
commentRevisionID,
reason,
}),
removeFlag: (input: GQLRemoveCommentFlagInput) =>
removeFlag: ({ commentID }: GQLRemoveCommentFlagInput) =>
removeFlag(ctx.mongo, ctx.tenant, ctx.user!, {
item_id: input.commentID,
commentID,
}),
});
@@ -16,7 +16,12 @@ import {
updateOIDCAuthIntegration,
} from "talk-server/services/tenant";
export default ({ mongo, redis, tenantCache, tenant }: TenantContext) => ({
export const Settings = ({
mongo,
redis,
tenantCache,
tenant,
}: TenantContext) => ({
update: (input: GQLSettingsInput): Promise<Tenant | null> =>
update(mongo, redis, tenantCache, tenant, omitBy(input, isNull)),
regenerateSSOKey: (): Promise<Tenant | null> =>
@@ -8,12 +8,14 @@ import {
GQLScrapeStoryInput,
GQLUpdateStoryInput,
} from "talk-server/graph/tenant/schema/__generated__/types";
import { Story } from "talk-server/models/story";
import * as story from "talk-server/models/story";
import { create, merge, remove, update } from "talk-server/services/stories";
import { scrape } from "talk-server/services/stories/scraper";
export default (ctx: TenantContext) => ({
create: async (input: GQLCreateStoryInput): Promise<Readonly<Story> | null> =>
export const Story = (ctx: TenantContext) => ({
create: async (
input: GQLCreateStoryInput
): Promise<Readonly<story.Story> | null> =>
create(
ctx.mongo,
ctx.tenant,
@@ -21,12 +23,20 @@ export default (ctx: TenantContext) => ({
input.story.url,
omitBy(input.story, isNull)
),
update: async (input: GQLUpdateStoryInput): Promise<Readonly<Story> | null> =>
update: async (
input: GQLUpdateStoryInput
): Promise<Readonly<story.Story> | null> =>
update(ctx.mongo, ctx.tenant, input.id, omitBy(input.story, isNull)),
merge: async (input: GQLMergeStoriesInput): Promise<Readonly<Story> | null> =>
merge: async (
input: GQLMergeStoriesInput
): Promise<Readonly<story.Story> | null> =>
merge(ctx.mongo, ctx.tenant, input.destinationID, input.sourceIDs),
remove: async (input: GQLRemoveStoryInput): Promise<Readonly<Story> | null> =>
remove: async (
input: GQLRemoveStoryInput
): Promise<Readonly<story.Story> | null> =>
remove(ctx.mongo, ctx.tenant, input.id, input.includeComments),
scrape: async (input: GQLScrapeStoryInput): Promise<Readonly<Story> | null> =>
scrape: async (
input: GQLScrapeStoryInput
): Promise<Readonly<story.Story> | null> =>
scrape(ctx.mongo, ctx.tenant.id, input.id),
});
@@ -1,10 +1,12 @@
import TenantContext from "talk-server/graph/tenant/context";
import Comment from "./comment";
import Settings from "./settings";
import Story from "./story";
import { Actions } from "./Actions";
import { Comment } from "./Comment";
import { Settings } from "./Settings";
import { Story } from "./Story";
export default (ctx: TenantContext) => ({
Actions: Actions(ctx),
Comment: Comment(ctx),
Settings: Settings(ctx),
Story: Story(ctx),
@@ -5,12 +5,12 @@ import {
const disabled = { enabled: false };
const AuthIntegrations: GQLAuthIntegrationsTypeResolver<GQLAuthIntegrations> = {
export const AuthIntegrations: GQLAuthIntegrationsTypeResolver<
GQLAuthIntegrations
> = {
local: auth => auth.local || disabled,
sso: auth => auth.sso || disabled,
oidc: auth => auth.oidc || disabled,
google: auth => auth.google || disabled,
facebook: auth => auth.facebook || disabled,
};
export default AuthIntegrations;
@@ -0,0 +1,80 @@
import { GraphQLResolveInfo } from "graphql";
import { getRequestedFields } from "talk-server/graph/tenant/resolvers/util";
import {
GQLComment,
GQLCommentTypeResolver,
} from "talk-server/graph/tenant/schema/__generated__/types";
import { decodeActionCounts } from "talk-server/models/action/comment";
import * as comment from "talk-server/models/comment";
import { getLatestRevision } from "talk-server/models/comment";
import { createConnection } from "talk-server/models/connection";
import TenantContext from "../context";
const maybeLoadOnlyID = (
ctx: TenantContext,
info: GraphQLResolveInfo,
id?: string
) => {
// If there isn't an id, then return nothing!
if (!id) {
return null;
}
// Get the field names of the fields being requested, if it's only the ID,
// we have that, so no need to make a database request.
const fields = getRequestedFields<GQLComment>(info);
if (fields.length === 1 && fields[0] === "id") {
return {
id,
};
}
// We want more than the ID! Get the comment!
// TODO: (wyattjoh) if the parent and the parents (containing the parent) are requested, the parent comment is retrieved from the database twice. Investigate ways of reducing i/o.
return ctx.loaders.Comments.comment.load(id);
};
export const Comment: GQLCommentTypeResolver<comment.Comment> = {
body: c => getLatestRevision(c).body,
// Send the whole comment back when you request revisions. This way, we get to
// know the comment ID. The field mapping is handled by the CommentRevision
// resolver.
revision: c => ({ revision: getLatestRevision(c), comment: c }),
revisionHistory: c => c.revisions.map(revision => ({ revision, comment: c })),
editing: ({ revisions, createdAt }, input, ctx) => ({
// When there is more than one body history, then the comment has been
// edited.
edited: revisions.length > 1,
// The date that the comment is editable until is the tenant's edit window
// length added to the comment created date.
editableUntil: new Date(
createdAt.valueOf() + ctx.tenant.editCommentWindowLength
),
}),
author: (c, input, ctx) => ctx.loaders.Users.user.load(c.authorID),
statusHistory: ({ id }, input, ctx) =>
ctx.loaders.Actions.commentModerationActions({
commentID: id,
}),
replies: (c, input, ctx) =>
c.replyCount > 0
? ctx.loaders.Comments.forParent(c.storyID, c.id, input)
: createConnection(),
actionCounts: c => decodeActionCounts(c.actionCounts),
myActionPresence: (c, input, ctx) =>
ctx.user ? ctx.loaders.Comments.retrieveMyActionPresence.load(c.id) : null,
parentCount: c => (c.parentID ? c.grandparentIDs.length + 1 : 0),
depth: c => (c.parentID ? c.grandparentIDs.length + 1 : 0),
rootParent: (c, input, ctx, info) =>
maybeLoadOnlyID(
ctx,
info,
c.grandparentIDs.length > 0 ? c.grandparentIDs[0] : c.parentID
),
parent: (c, input, ctx, info) => maybeLoadOnlyID(ctx, info, c.parentID),
parents: (c, input, ctx) =>
// Some resolver optimization.
c.parentID ? ctx.loaders.Comments.parents(c, input) : createConnection(),
story: (c, input, ctx) => ctx.loaders.Stories.story.load(c.storyID),
};
@@ -4,11 +4,11 @@ import {
} from "talk-server/graph/tenant/schema/__generated__/types";
import { CommentStatusCounts } from "talk-server/models/story";
const CommentCounts: GQLCommentCountsTypeResolver<CommentStatusCounts> = {
export const CommentCounts: GQLCommentCountsTypeResolver<
CommentStatusCounts
> = {
totalVisible: commentCounts =>
commentCounts[GQLCOMMENT_STATUS.ACCEPTED] +
commentCounts[GQLCOMMENT_STATUS.NONE],
statuses: commentCounts => commentCounts,
};
export default CommentCounts;
@@ -0,0 +1,24 @@
import * as actions from "talk-server/models/action/moderation/comment";
import { GQLCommentModerationActionTypeResolver } from "../schema/__generated__/types";
export const CommentModerationAction: GQLCommentModerationActionTypeResolver<
actions.CommentModerationAction
> = {
revision: async (action, input, ctx) => {
const comment = await ctx.loaders.Comments.comment.load(action.commentID);
if (!comment) {
return null;
}
const revision = comment.revisions.find(
({ id }) => id === action.commentRevisionID
);
if (!revision) {
return null;
}
return { comment, revision };
},
moderator: (action, input, ctx) =>
action.moderatorID ? ctx.loaders.Users.user.load(action.moderatorID) : null,
};
@@ -0,0 +1,18 @@
import { GQLCommentRevisionTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { decodeActionCounts } from "talk-server/models/action/comment";
import * as comment from "talk-server/models/comment";
export interface WrappedCommentRevision {
revision: comment.Revision;
comment: comment.Comment;
}
export const CommentRevision: Required<
GQLCommentRevisionTypeResolver<WrappedCommentRevision>
> = {
id: w => w.revision.id,
comment: w => w.comment,
actionCounts: w => decodeActionCounts(w.revision.actionCounts),
body: w => w.revision.body,
createdAt: w => w.revision.createdAt,
};
@@ -4,7 +4,7 @@ import {
GQLFacebookAuthIntegrationTypeResolver,
} from "talk-server/graph/tenant/schema/__generated__/types";
const FacebookAuthIntegration: GQLFacebookAuthIntegrationTypeResolver<
export const FacebookAuthIntegration: GQLFacebookAuthIntegrationTypeResolver<
GQLFacebookAuthIntegration
> = {
callbackURL: (integration, args, ctx) => {
@@ -22,5 +22,3 @@ const FacebookAuthIntegration: GQLFacebookAuthIntegrationTypeResolver<
return constructTenantURL(ctx.config, ctx.tenant, path);
},
};
export default FacebookAuthIntegration;
@@ -4,7 +4,7 @@ import {
GQLGoogleAuthIntegrationTypeResolver,
} from "talk-server/graph/tenant/schema/__generated__/types";
const GoogleAuthIntegration: GQLGoogleAuthIntegrationTypeResolver<
export const GoogleAuthIntegration: GQLGoogleAuthIntegrationTypeResolver<
GQLGoogleAuthIntegration
> = {
callbackURL: (integration, args, ctx) => {
@@ -22,5 +22,3 @@ const GoogleAuthIntegration: GQLGoogleAuthIntegrationTypeResolver<
return constructTenantURL(ctx.config, ctx.tenant, path);
},
};
export default GoogleAuthIntegration;
@@ -1,6 +1,6 @@
import { GQLMutationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
const Mutation: GQLMutationTypeResolver<void> = {
export const Mutation: GQLMutationTypeResolver<void> = {
editComment: async (source, { input }, ctx) => ({
comment: await ctx.mutators.Comment.edit(input),
clientMutationId: input.clientMutationId,
@@ -107,6 +107,12 @@ const Mutation: GQLMutationTypeResolver<void> = {
story: await ctx.mutators.Story.scrape(input),
clientMutationId: input.clientMutationId,
}),
acceptComment: async (source, { input }, ctx) => ({
comment: await ctx.mutators.Actions.acceptComment(input),
clientMutationId: input.clientMutationId,
}),
rejectComment: async (source, { input }, ctx) => ({
comment: await ctx.mutators.Actions.rejectComment(input),
clientMutationId: input.clientMutationId,
}),
};
export default Mutation;
@@ -4,7 +4,7 @@ import {
GQLOIDCAuthIntegrationTypeResolver,
} from "talk-server/graph/tenant/schema/__generated__/types";
const OIDCAuthIntegration: GQLOIDCAuthIntegrationTypeResolver<
export const OIDCAuthIntegration: GQLOIDCAuthIntegrationTypeResolver<
GQLOIDCAuthIntegration
> = {
callbackURL: (integration, args, ctx) => {
@@ -22,5 +22,3 @@ const OIDCAuthIntegration: GQLOIDCAuthIntegrationTypeResolver<
return constructTenantURL(ctx.config, ctx.tenant, path);
},
};
export default OIDCAuthIntegration;
@@ -1,8 +1,8 @@
import { GQLProfileTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { Profile } from "talk-server/models/user";
import * as user from "talk-server/models/user";
const resolveType: GQLProfileTypeResolver<Profile> = profile => {
const resolveType: GQLProfileTypeResolver<user.Profile> = profile => {
switch (profile.type) {
case "local":
return "LocalProfile";
@@ -16,6 +16,6 @@ const resolveType: GQLProfileTypeResolver<Profile> = profile => {
}
};
export default {
export const Profile = {
__resolveType: resolveType,
};
@@ -1,6 +1,6 @@
import { GQLQueryTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
const Query: GQLQueryTypeResolver<void> = {
export const Query: GQLQueryTypeResolver<void> = {
story: (source, args, ctx) => ctx.loaders.Stories.findOrCreate(args),
comment: (source, { id }, ctx) =>
id ? ctx.loaders.Comments.comment.load(id) : null,
@@ -11,5 +11,3 @@ const Query: GQLQueryTypeResolver<void> = {
debugScrapeStoryMetadata: (source, { url }, ctx) =>
ctx.loaders.Stories.debugScrapeMetadata.load(url),
};
export default Query;
@@ -0,0 +1,25 @@
import { DateTime } from "luxon";
import { GQLStoryTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { decodeActionCounts } from "talk-server/models/action/comment";
import * as story from "talk-server/models/story";
export const Story: GQLStoryTypeResolver<story.Story> = {
comments: (s, input, ctx) => ctx.loaders.Comments.forStory(s.id, input),
isClosed: () => false,
closedAt: (s, input, ctx) => {
if (s.closedAt) {
return s.closedAt;
}
if (ctx.tenant.autoCloseStream && ctx.tenant.closedTimeout) {
return DateTime.fromJSDate(s.createdAt)
.plus(ctx.tenant.closedTimeout)
.toJSDate();
}
return null;
},
commentActionCounts: s => decodeActionCounts(s.commentActionCounts),
commentCounts: s => s.commentCounts,
};
@@ -0,0 +1,8 @@
import { GQLUserTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import * as user from "talk-server/models/user";
export const User: GQLUserTypeResolver<user.User> = {
comments: ({ id }, input, ctx) => ctx.loaders.Comments.forUser(id, input),
commentModerationActionHistory: ({ id }, input, ctx) =>
ctx.loaders.Actions.commentModerationActionsConnection(input, id),
};
@@ -1,91 +0,0 @@
import { getRequestedFields } from "talk-server/graph/tenant/resolvers/util";
import {
GQLComment,
GQLCommentTypeResolver,
} from "talk-server/graph/tenant/schema/__generated__/types";
import { decodeActionCounts } from "talk-server/models/action";
import { Comment } from "talk-server/models/comment";
import { createConnection } from "talk-server/models/connection";
const Comment: GQLCommentTypeResolver<Comment> = {
editing: (comment, input, ctx) => ({
// When there is more than one body history, then the comment has been
// edited.
edited: comment.body_history.length > 1,
// The date that the comment is editable until is the tenant's edit window
// length added to the comment created date.
editableUntil: new Date(
comment.created_at.valueOf() + ctx.tenant.editCommentWindowLength
),
}),
createdAt: comment => comment.created_at,
author: (comment, input, ctx) =>
ctx.loaders.Users.user.load(comment.author_id),
replies: (comment, input, ctx) =>
comment.reply_count > 0
? ctx.loaders.Comments.forParent(comment.story_id, comment.id, input)
: createConnection(),
actionCounts: comment => decodeActionCounts(comment.action_counts),
myActionPresence: (comment, input, ctx) =>
ctx.user
? ctx.loaders.Comments.retrieveMyActionPresence.load(comment.id)
: null,
parentCount: comment =>
comment.parent_id ? comment.grandparent_ids.length + 1 : 0,
depth: comment =>
comment.parent_id ? comment.grandparent_ids.length + 1 : 0,
replyCount: comment => comment.reply_count,
rootParent: (comment, input, ctx, info) => {
// If there isn't a parent, then return nothing!
if (!comment.parent_id) {
return null;
}
// rootParentID is the root parent id for a given comment.
const rootParentID =
comment.grandparent_ids.length > 0
? comment.grandparent_ids[0]
: comment.parent_id;
// Get the field names of the fields being requested, if it's only the ID,
// we have that, so no need to make a database request.
const fields = getRequestedFields<GQLComment>(info);
if (fields.length === 1 && fields[0] === "id") {
return {
id: rootParentID,
};
}
// We want more than the ID! Get the comment!
// TODO: (wyattjoh) if the parent and the parents (containing the parent) are requested, the parent comment is retrieved from the database twice. Investigate ways of reducing i/o.
return ctx.loaders.Comments.comment.load(rootParentID);
},
parent: (comment, input, ctx, info) => {
// If there isn't a parent, then return nothing!
if (!comment.parent_id) {
return null;
}
// Get the field names of the fields being requested, if it's only the ID,
// we have that, so no need to make a database request.
const fields = getRequestedFields<GQLComment>(info);
if (fields.length === 1 && fields[0] === "id") {
return {
id: comment.parent_id,
};
}
// We want more than the ID! Get the comment!
// TODO: (wyattjoh) if the parent and the parents (containing the parent) are requested, the parent comment is retrieved from the database twice. Investigate ways of reducing i/o.
return ctx.loaders.Comments.comment.load(comment.parent_id);
},
parents: (comment, input, ctx) =>
// Some resolver optimization.
comment.parent_id
? ctx.loaders.Comments.parents(comment, input)
: createConnection(),
story: (comment, input, ctx) =>
ctx.loaders.Stories.story.load(comment.story_id),
};
export default Comment;
+15 -11
View File
@@ -3,22 +3,26 @@ import Time from "talk-server/graph/common/scalars/time";
import { GQLResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import AuthIntegrations from "./auth_integrations";
import Comment from "./comment";
import CommentCounts from "./comment_counts";
import FacebookAuthIntegration from "./facebook_auth_integration";
import GoogleAuthIntegration from "./google_auth_integration";
import Mutation from "./mutation";
import OIDCAuthIntegration from "./oidc_auth_integration";
import Profile from "./profile";
import Query from "./query";
import Story from "./story";
import User from "./user";
import { AuthIntegrations } from "./AuthIntegrations";
import { Comment } from "./Comment";
import { CommentCounts } from "./CommentCounts";
import { CommentModerationAction } from "./CommentModerationAction";
import { CommentRevision } from "./CommentRevision";
import { FacebookAuthIntegration } from "./FacebookAuthIntegration";
import { GoogleAuthIntegration } from "./GoogleAuthIntegration";
import { Mutation } from "./Mutation";
import { OIDCAuthIntegration } from "./OIDCAuthIntegration";
import { Profile } from "./Profile";
import { Query } from "./Query";
import { Story } from "./Story";
import { User } from "./User";
const Resolvers: GQLResolver = {
AuthIntegrations,
Comment,
CommentCounts,
CommentModerationAction,
CommentRevision,
Cursor,
Mutation,
OIDCAuthIntegration,
@@ -1,28 +0,0 @@
import { DateTime } from "luxon";
import { GQLStoryTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { decodeActionCounts } from "talk-server/models/action";
import { Story } from "talk-server/models/story";
const Story: GQLStoryTypeResolver<Story> = {
comments: (story, input, ctx) =>
ctx.loaders.Comments.forStory(story.id, input),
isClosed: () => false,
closedAt: (story, input, ctx) => {
if (story.closedAt) {
return story.closedAt;
}
if (ctx.tenant.autoCloseStream && ctx.tenant.closedTimeout) {
return DateTime.fromJSDate(story.created_at)
.plus(ctx.tenant.closedTimeout)
.toJSDate();
}
return null;
},
actionCounts: story => decodeActionCounts(story.action_counts),
commentCounts: story => story.comment_counts,
};
export default Story;
@@ -1,8 +0,0 @@
import { GQLUserTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { User } from "talk-server/models/user";
const User: GQLUserTypeResolver<User> = {
comments: (user, input, ctx) => ctx.loaders.Comments.forUser(user.id, input),
};
export default User;
@@ -975,6 +975,15 @@ type User {
orderBy: COMMENT_SORT = CREATED_AT_DESC
after: Cursor
): CommentsConnection! @auth(roles: [ADMIN, MODERATOR], userIDField: "id")
"""
commentModerationActionHistory returns a CommentModerationActionConnection that this User has
created.
"""
commentModerationActionHistory(
first: Int = 10
after: Cursor
): CommentModerationActionConnection! @auth(role: [MODERATOR, ADMIN])
}
################################################################################
@@ -1024,6 +1033,85 @@ enum COMMENT_STATUS {
SYSTEM_WITHHELD
}
type CommentModerationAction {
id: ID!
"""
revision is the moderated CommentRevision.
"""
revision: CommentRevision!
"""
status represents the status that was assigned by the moderator.
"""
status: COMMENT_STATUS!
"""
moderator is the User that performed the Moderator action. If null, this means
that the system has assigned the moderation status.
"""
moderator: User
"""
createdAt is the time that the CommentModerationAction was created.
"""
createdAt: Time!
}
type CommentModerationActionEdge {
"""
node is the CommentModerationAction for this edge.
"""
node: CommentModerationAction!
"""
"""
cursor: Cursor
}
type CommentModerationActionConnection {
"""
edges are a subset of CommentModerationActionEdge's.
"""
edges: [CommentModerationActionEdge!]!
"""
pageInfo is
"""
pageInfo: PageInfo!
}
type CommentRevision {
"""
id is the identifier of the CommentRevision.
"""
id: ID!
"""
comment is the reference to the original Comment associated with the current
Comment.
"""
comment: Comment!
"""
actionCounts stores the counts of all the actions for the CommentRevision
specifically.
"""
actionCounts: ActionCounts! @auth(roles: [MODERATOR, ADMIN])
"""
body is the content of the CommentRevision. If null, it indicates that the
body text was deleted.
"""
body: String
"""
createdAt is the time that the CommentRevision was created.
"""
createdAt: Time!
}
"""
Comment is a comment left by a User on an Story or another Comment as a reply.
"""
@@ -1034,10 +1122,23 @@ type Comment {
id: ID!
"""
body is the content of the Comment.
body is the content of the Comment, and is an alias to the body of the
`currentRevision.body`.
"""
body: String
"""
revision is the current revision of the Comment's body.
"""
revision: CommentRevision!
"""
revisionHistory stores the previous CommentRevision's, with the most recent
edit last.
"""
revisionHistory: [CommentRevision!]!
@auth(roles: [MODERATOR, ADMIN], userIDField: "author_id")
"""
createdAt is the date in which the Comment was created.
"""
@@ -1053,6 +1154,13 @@ type Comment {
"""
status: COMMENT_STATUS!
"""
statusHistory returns a CommentModerationActionConnection that will list
the history of moderator actions performed on the Comment, with the most
recent last.
"""
statusHistory: [CommentModerationAction!]! @auth(role: [MODERATOR, ADMIN])
"""
parentCount is the number of direct parents for this Comment. Currently this
value is the same as depth.
@@ -1107,7 +1215,7 @@ type Comment {
actionCounts: ActionCounts!
"""
myActionPresence stores the presense information for all the actions
myActionPresence stores the presence information for all the actions
left by the current User on this Comment.
"""
myActionPresence: ActionPresence
@@ -1302,10 +1410,10 @@ type Story {
): CommentsConnection!
"""
actionCounts stores the counts of all the actions against this Story and it's
commentActionCounts stores the counts of all the actions against this Story and it's
Comments.
"""
actionCounts: ActionCounts! @auth(roles: [ADMIN, MODERATOR])
commentActionCounts: ActionCounts! @auth(roles: [ADMIN, MODERATOR])
"""
closedAt is the Time that the Story is closed for commenting.
@@ -1927,6 +2035,12 @@ input CreateCommentReactionInput {
"""
commentID: ID!
"""
commentRevisionID is the revision ID of the Comment that we're creating the
Reaction on.
"""
commentRevisionID: ID!
"""
clientMutationId is required for Relay support.
"""
@@ -1983,6 +2097,12 @@ input CreateCommentDontAgreeInput {
"""
commentID: ID!
"""
commentRevisionID is the revision ID of the Comment that we're creating the
DontAgree on.
"""
commentRevisionID: ID!
"""
clientMutationId is required for Relay support.
"""
@@ -2039,6 +2159,12 @@ input CreateCommentFlagInput {
"""
commentID: ID!
"""
commentRevisionID is the revision ID of the Comment that we're creating the
Flag on.
"""
commentRevisionID: ID!
"""
reason is the selected reason why the Flag is being created.
"""
@@ -2603,6 +2729,72 @@ type ScrapeStoryPayload {
clientMutationId: String!
}
##################
# acceptComment
##################
input AcceptCommentInput {
"""
commentID is the ID of the Comment that was accepted.
"""
commentID: ID!
"""
commentRevisionID is the ID of the CommentRevision that is being accepted.
"""
commentRevisionID: ID!
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
}
type AcceptCommentPayload {
"""
comment is the Comment that was accepted.
"""
comment: Comment
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
}
##################
# rejectComment
##################
input RejectCommentInput {
"""
commentID is the ID of the Comment that was rejected.
"""
commentID: ID!
"""
commentRevisionID is the ID of the CommentRevision that is being rejected.
"""
commentRevisionID: ID!
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
}
type RejectCommentPayload {
"""
comment is the Comment that was rejected.
"""
comment: Comment
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
}
##################
## Mutation
##################
@@ -2730,6 +2922,18 @@ type Mutation {
"""
scrapeStory(input: ScrapeStoryInput!): ScrapeStoryPayload
@auth(roles: [ADMIN, MODERATOR])
"""
acceptComment will mark the Comment as ACCEPTED.
"""
acceptComment(input: AcceptCommentInput!): AcceptCommentPayload
@auth(roles: [MODERATOR, ADMIN])
"""
rejectComment will mark the Comment as REJECTED.
"""
rejectComment(input: RejectCommentInput!): RejectCommentPayload
@auth(roles: [MODERATOR, ADMIN])
}
################################################################################