[CORL-1090] Sections Alpha (#2973)

* feat: added section filtering support

* fix: addressed issues around filtering sections

* fix: fixed issue with section filter
This commit is contained in:
Wyatt Johnson
2020-06-04 13:32:04 +02:00
committed by GitHub
parent 92c72f7041
commit ba08447d5e
54 changed files with 944 additions and 135 deletions
+41 -10
View File
@@ -2,6 +2,7 @@ import DataLoader from "dataloader";
import { defaultTo, isNil, omitBy } from "lodash";
import { DateTime } from "luxon";
import { SectionFilter } from "coral-common/section";
import Context from "coral-server/graph/context";
import { retrieveManyUserActionPresence } from "coral-server/models/action/comment";
import {
@@ -21,6 +22,7 @@ import {
import { retrieveSharedModerationQueueQueuesCounts } from "coral-server/models/comment/counts/shared";
import { hasPublishedStatus } from "coral-server/models/comment/helpers";
import { Connection } from "coral-server/models/helpers";
import { hasFeatureFlag, Tenant } from "coral-server/models/tenant";
import { User } from "coral-server/models/user";
import {
@@ -28,6 +30,7 @@ import {
CommentToRepliesArgs,
GQLActionPresence,
GQLCOMMENT_SORT,
GQLFEATURE_FLAG,
GQLTAG,
GQLUSER_ROLE,
QueryToCommentsArgs,
@@ -39,6 +42,18 @@ import {
import { SingletonResolver } from "./util";
/**
* requiredPropertyFilter will remove those properties that are nil from the
* object as they are not nilable on the database model. If we didn't do this,
* then any time that the property is nil, we'd be querying for comments that
* can't possibly exist!
*
* @param props properties that if nil should be removed from the return object
*/
const requiredPropertyFilter = (
props: CommentConnectionInput["filter"]
): CommentConnectionInput["filter"] => omitBy(props, isNil);
const tagFilter = (tag?: GQLTAG): CommentConnectionInput["filter"] => {
if (tag) {
return {
@@ -57,6 +72,22 @@ const queryFilter = (query?: string): CommentConnectionInput["filter"] => {
return {};
};
const sectionFilter = (
tenant: Pick<Tenant, "featureFlags">,
section?: SectionFilter
): CommentConnectionInput["filter"] => {
// Don't filter by section if the feature flag is disabled.
if (!hasFeatureFlag(tenant, GQLFEATURE_FLAG.SECTIONS)) {
return {};
}
if (section) {
return { section: section.name || null };
}
return {};
};
/**
* primeCommentsFromConnection will prime a given context with the comments
* retrieved via a connection.
@@ -139,6 +170,7 @@ export default (ctx: Context) => ({
after,
storyID,
siteID,
section,
status,
tag,
query,
@@ -147,16 +179,15 @@ export default (ctx: Context) => ({
first: defaultTo(first, 10),
after,
orderBy: GQLCOMMENT_SORT.CREATED_AT_DESC,
filter: omitBy(
{
...queryFilter(query),
...tagFilter(tag),
storyID,
siteID,
status,
},
isNil
),
filter: {
...queryFilter(query),
...tagFilter(tag),
...sectionFilter(ctx.tenant, section),
// If these properties are not provided or are null, remove them from
// the filter because they do not exist in a nullable state on the
// database model.
...requiredPropertyFilter({ storyID, siteID, status }),
},
}).then(primeCommentsFromConnection(ctx)),
retrieveMyActionPresence: new DataLoader<string, GQLActionPresence>(
(commentIDs: string[]) => {
+2
View File
@@ -17,6 +17,7 @@ import {
findOrCreate,
FindOrCreateStory,
FindStory,
retrieveSections,
} from "coral-server/services/stories";
import { scraper } from "coral-server/services/stories/scraper";
@@ -185,6 +186,7 @@ export default (ctx: GraphContext) => ({
cache: !ctx.disableCaching,
}
),
sections: () => retrieveSections(ctx.mongo, ctx.tenant),
story: new DataLoader<string, Story | null>(
(ids) => retrieveManyStories(ctx.mongo, ctx.tenant.id, ids),
{
@@ -5,6 +5,7 @@ import {
import { FilterQuery } from "coral-server/models/helpers";
import { Site } from "coral-server/models/site";
import { Story } from "coral-server/models/story";
import { hasFeatureFlag } from "coral-server/models/tenant";
import {
PENDING_STATUS,
REPORTED_STATUS,
@@ -12,7 +13,9 @@ import {
} from "coral-server/services/comments/moderation/counts";
import {
GQLFEATURE_FLAG,
GQLModerationQueuesTypeResolver,
GQLSectionFilter,
QueryToModerationQueuesResolver,
} from "coral-server/graph/schema/__generated__/types";
@@ -21,7 +24,7 @@ import { ModerationQueueInput } from "./ModerationQueue";
interface ModerationQueuesInput {
connection: Partial<CommentConnectionInput>;
counts: CommentModerationCountsPerQueue;
counts?: CommentModerationCountsPerQueue;
}
const mergeModerationInputFilters = (
@@ -80,6 +83,25 @@ export const storyModerationInputResolver = (
counts: story.commentCounts.moderationQueue.queues,
});
/**
* sectionModerationInputResolver can be used to retrieve the moderationQueue for
* a specific Story.
*
* @param section the section that will be used to base the comment moderation
* queues on
*/
export const sectionModerationInputResolver = async (
section: GQLSectionFilter
): Promise<ModerationQueuesInput> => ({
connection: {
filter: {
// This moderationQueues is being sourced from the section, so require
// that all the comments for theses queues are also for this section.
section: section.name || null,
},
},
});
/**
* sharedModerationInputResolver implements the resolver function style which
* allows it to be used in a type resolver.
@@ -122,6 +144,10 @@ export const moderationQueuesResolver: QueryToModerationQueuesResolver = async (
return storyModerationInputResolver(story);
}
if (args.section && hasFeatureFlag(ctx.tenant, GQLFEATURE_FLAG.SECTIONS)) {
return sectionModerationInputResolver(args.section);
}
if (args.siteID) {
const site = await ctx.loaders.Sites.site.load(args.siteID);
if (!site) {
+1
View File
@@ -24,6 +24,7 @@ export const Query: Required<GQLQueryTypeResolver<void>> = {
debugScrapeStoryMetadata: (source, { url }, ctx) =>
ctx.loaders.Stories.debugScrapeMetadata.load(url),
moderationQueues: moderationQueuesResolver,
sections: (source, args, ctx) => ctx.loaders.Stories.sections(),
activeStories: (source, { limit = 10 }, ctx) =>
ctx.loaders.Stories.activeStories(limit),
sites: (source, args, ctx) => ctx.loaders.Sites.connection(args),
@@ -1,4 +1,7 @@
import { hasFeatureFlag } from "coral-server/models/tenant";
import {
GQLFEATURE_FLAG,
GQLMODERATION_QUEUE,
SubscriptionToCommentEnteredModerationQueueResolver,
} from "coral-server/graph/schema/__generated__/types";
@@ -15,6 +18,8 @@ export interface CommentEnteredModerationQueueInput
queue: GQLMODERATION_QUEUE;
commentID: string;
storyID: string;
siteID: string;
section?: string;
}
export type CommentEnteredModerationQueueSubscription = SubscriptionType<
@@ -25,7 +30,7 @@ export type CommentEnteredModerationQueueSubscription = SubscriptionType<
export const commentEnteredModerationQueue: SubscriptionToCommentEnteredModerationQueueResolver<CommentEnteredModerationQueueInput> = createIterator(
SUBSCRIPTION_CHANNELS.COMMENT_ENTERED_MODERATION_QUEUE,
{
filter: (source, { storyID, queue }) => {
filter: (source, { storyID, siteID, section, queue }, ctx) => {
// If we're filtering by storyID, then only send back comments with the
// specific storyID.
if (storyID && source.storyID !== storyID) {
@@ -38,6 +43,25 @@ export const commentEnteredModerationQueue: SubscriptionToCommentEnteredModerati
return false;
}
// If we're filtering by siteID, then only send back comments from the
// specific site.
if (siteID && source.siteID !== siteID) {
return false;
}
// If we're filtering by section, then only send back comments from the
// specific section. If the source has a section, if it's not equal to the
// filter then return false. If the source does not have a section, then
// the filter must also be null/undefined, otherwise return false.
if (
section &&
((source.section && section.name !== source.section) ||
(!source.section && section.name)) &&
hasFeatureFlag(ctx.tenant, GQLFEATURE_FLAG.SECTIONS)
) {
return false;
}
return true;
},
}
@@ -1,4 +1,7 @@
import { hasFeatureFlag } from "coral-server/models/tenant";
import {
GQLFEATURE_FLAG,
GQLMODERATION_QUEUE,
SubscriptionToCommentLeftModerationQueueResolver,
} from "coral-server/graph/schema/__generated__/types";
@@ -14,6 +17,8 @@ export interface CommentLeftModerationQueueInput extends SubscriptionPayload {
queue: GQLMODERATION_QUEUE;
commentID: string;
storyID: string;
siteID: string;
section?: string;
}
export type CommentLeftModerationQueueSubscription = SubscriptionType<
@@ -24,7 +29,7 @@ export type CommentLeftModerationQueueSubscription = SubscriptionType<
export const commentLeftModerationQueue: SubscriptionToCommentLeftModerationQueueResolver<CommentLeftModerationQueueInput> = createIterator(
SUBSCRIPTION_CHANNELS.COMMENT_LEFT_MODERATION_QUEUE,
{
filter: (source, { storyID, queue }) => {
filter: (source, { storyID, siteID, section, queue }, ctx) => {
// If we're filtering by storyID, then only send back comments with the
// specific storyID.
if (storyID && source.storyID !== storyID) {
@@ -37,6 +42,25 @@ export const commentLeftModerationQueue: SubscriptionToCommentLeftModerationQueu
return false;
}
// If we're filtering by siteID, then only send back comments from the
// specific site.
if (siteID && source.siteID !== siteID) {
return false;
}
// If we're filtering by section, then only send back comments from the
// specific section. If the source has a section, if it's not equal to the
// filter then return false. If the source does not have a section, then
// the filter must also be null/undefined, otherwise return false.
if (
section &&
((source.section && section.name !== source.section) ||
(!source.section && section.name)) &&
hasFeatureFlag(ctx.tenant, GQLFEATURE_FLAG.SECTIONS)
) {
return false;
}
return true;
},
}
+46 -4
View File
@@ -91,6 +91,21 @@ Locale represents a language code in the BCP 47 format.
"""
scalar Locale
################################################################################
## Custom Input Types
################################################################################
input SectionFilter {
"""
name when provided will filter only those comments/stories that have this
section. When name is not provided or is null, only comments/stories without
a section (uncategorized) will be returned. In order to return all
comments/stories regardless of section, specify null or undefined for this
filter option.
"""
name: String
}
################################################################################
## Actions
################################################################################
@@ -343,6 +358,12 @@ type ActionPresence {
################################################################################
enum FEATURE_FLAG {
"""
SECTIONS when enabled will allow filtering comments in the moderation queue
by section.
"""
SECTIONS
"""
DISABLE_WARN_USER_OF_TOXIC_COMMENT when enabled will turn off warnings for
toxic comments.
@@ -3150,6 +3171,7 @@ type Query {
storyID: ID
siteID: ID
status: COMMENT_STATUS
section: SectionFilter
tag: TAG
query: String
): CommentsConnection! @auth(roles: [ADMIN, MODERATOR])
@@ -3236,8 +3258,16 @@ type Query {
moderationQueues returns the set of ModerationQueues that are available for
all stories or if given the story identified by the `storyID`.
"""
moderationQueues(storyID: ID, siteID: ID): ModerationQueues!
@auth(roles: [ADMIN, MODERATOR])
moderationQueues(
storyID: ID
siteID: ID
section: SectionFilter
): ModerationQueues! @auth(roles: [ADMIN, MODERATOR])
"""
sections will return the unique sections used by this Tenant.
"""
sections: [String!] @auth(roles: [ADMIN, MODERATOR])
"""
activeStories is the list of most recently commented on stories identified
@@ -4888,7 +4918,11 @@ type ApproveCommentPayload {
is provided, it will filter the moderation queues for only comments in that
Story.
"""
moderationQueues(storyID: ID): ModerationQueues
moderationQueues(
storyID: ID
siteID: ID
section: SectionFilter
): ModerationQueues
"""
clientMutationId is required for Relay support.
@@ -4928,7 +4962,11 @@ type RejectCommentPayload {
is provided, it will filter the moderation queues for only comments in that
Story.
"""
moderationQueues(storyID: ID): ModerationQueues
moderationQueues(
storyID: ID
siteID: ID
section: SectionFilter
): ModerationQueues
"""
clientMutationId is required for Relay support.
@@ -7281,6 +7319,8 @@ type Subscription {
"""
commentEnteredModerationQueue(
storyID: ID
siteID: ID
section: SectionFilter
queue: MODERATION_QUEUE
): CommentEnteredModerationQueuePayload! @auth(roles: [MODERATOR, ADMIN])
@@ -7290,6 +7330,8 @@ type Subscription {
"""
commentLeftModerationQueue(
storyID: ID
siteID: ID
section: SectionFilter
queue: MODERATION_QUEUE
): CommentLeftModerationQueuePayload! @auth(roles: [MODERATOR, ADMIN])