mirror of
https://github.com/wassname/talk.git
synced 2026-08-12 12:30:39 +08:00
[CORL-667] Constraint Directive (#2641)
* feat: added constraints * fix: fixes bug with index creation - fixes #2612 * fix: added default resolver * fix: added defaultTo to handle defaulting
This commit is contained in:
committed by
Kim Gardner
parent
ea71230b90
commit
b38a4f117b
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
defaultFieldResolver,
|
||||
GraphQLArgument,
|
||||
GraphQLField,
|
||||
GraphQLInterfaceType,
|
||||
GraphQLObjectType,
|
||||
} from "graphql";
|
||||
import { SchemaDirectiveVisitor } from "graphql-tools";
|
||||
import { isNumber } from "lodash";
|
||||
|
||||
export default class extends SchemaDirectiveVisitor {
|
||||
public visitArgumentDefinition(
|
||||
argument: GraphQLArgument,
|
||||
details: {
|
||||
field: GraphQLField<any, any>;
|
||||
objectType: GraphQLObjectType | GraphQLInterfaceType;
|
||||
}
|
||||
) {
|
||||
const originalResolver = details.field.resolve || defaultFieldResolver;
|
||||
details.field.resolve = async (...resolveArgs) => {
|
||||
const argName = argument.name;
|
||||
const args = resolveArgs[1]; // (parent, args, context, info)
|
||||
const valueToValidate = args[argName];
|
||||
|
||||
// Apply number validations.
|
||||
if (isNumber(valueToValidate)) {
|
||||
// Apply min validation.
|
||||
if (isNumber(this.args.min)) {
|
||||
if (valueToValidate < this.args.min) {
|
||||
throw new Error("too short");
|
||||
}
|
||||
}
|
||||
|
||||
// Apply max validation.
|
||||
if (isNumber(this.args.max)) {
|
||||
if (valueToValidate > this.args.max) {
|
||||
throw new Error("too long");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return originalResolver.apply(this, resolveArgs);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { defaultTo } from "lodash";
|
||||
|
||||
import Context from "coral-server/graph/tenant/context";
|
||||
import {
|
||||
CommentActionConnectionInput,
|
||||
@@ -5,9 +7,9 @@ import {
|
||||
} from "coral-server/models/action/comment";
|
||||
|
||||
export default (ctx: Context) => ({
|
||||
connection: ({ first = 10, after, filter }: CommentActionConnectionInput) =>
|
||||
connection: ({ first, after, filter }: CommentActionConnectionInput) =>
|
||||
retrieveCommentActionConnection(ctx.mongo, ctx.tenant.id, {
|
||||
first,
|
||||
first: defaultTo(first, 10),
|
||||
after,
|
||||
filter,
|
||||
}),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { defaultTo } from "lodash";
|
||||
|
||||
import TenantContext from "coral-server/graph/tenant/context";
|
||||
import {
|
||||
CommentToStatusHistoryArgs,
|
||||
@@ -7,22 +9,22 @@ import { retrieveCommentModerationActionConnection } from "coral-server/models/a
|
||||
|
||||
export default (ctx: TenantContext) => ({
|
||||
forModerator: (
|
||||
{ first = 10, after }: UserToCommentModerationActionHistoryArgs,
|
||||
{ first, after }: UserToCommentModerationActionHistoryArgs,
|
||||
moderatorID: string
|
||||
) =>
|
||||
retrieveCommentModerationActionConnection(ctx.mongo, ctx.tenant.id, {
|
||||
first,
|
||||
first: defaultTo(first, 10),
|
||||
after,
|
||||
filter: {
|
||||
moderatorID,
|
||||
},
|
||||
}),
|
||||
forComment: (
|
||||
{ first = 10, after }: CommentToStatusHistoryArgs,
|
||||
{ first, after }: CommentToStatusHistoryArgs,
|
||||
commentID: string
|
||||
) =>
|
||||
retrieveCommentModerationActionConnection(ctx.mongo, ctx.tenant.id, {
|
||||
first,
|
||||
first: defaultTo(first, 10),
|
||||
after,
|
||||
filter: {
|
||||
commentID,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import DataLoader from "dataloader";
|
||||
import { isNil, omitBy } from "lodash";
|
||||
import { defaultTo, isNil, omitBy } from "lodash";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import Context from "coral-server/graph/tenant/context";
|
||||
@@ -126,7 +126,7 @@ export default (ctx: Context) => ({
|
||||
}
|
||||
),
|
||||
forFilter: ({
|
||||
first = 10,
|
||||
first,
|
||||
after,
|
||||
storyID,
|
||||
status,
|
||||
@@ -134,7 +134,7 @@ export default (ctx: Context) => ({
|
||||
query,
|
||||
}: QueryToCommentsArgs) =>
|
||||
retrieveCommentConnection(ctx.mongo, ctx.tenant.id, {
|
||||
first,
|
||||
first: defaultTo(first, 10),
|
||||
after,
|
||||
orderBy: GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
filter: omitBy(
|
||||
@@ -164,71 +164,45 @@ export default (ctx: Context) => ({
|
||||
);
|
||||
}
|
||||
),
|
||||
forUser: (
|
||||
userID: string,
|
||||
// Apply the graph schema defaults at the loader.
|
||||
{
|
||||
first = 10,
|
||||
orderBy = GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
after,
|
||||
}: UserToCommentsArgs
|
||||
) =>
|
||||
forUser: (userID: string, { first, orderBy, after }: UserToCommentsArgs) =>
|
||||
retrieveCommentUserConnection(ctx.mongo, ctx.tenant.id, userID, {
|
||||
first,
|
||||
orderBy,
|
||||
first: defaultTo(first, 10),
|
||||
orderBy: defaultTo(orderBy, GQLCOMMENT_SORT.CREATED_AT_DESC),
|
||||
after,
|
||||
}).then(primeCommentsFromConnection(ctx)),
|
||||
forUserAll: (
|
||||
userID: string,
|
||||
// Apply the graph schema defaults at the loader.
|
||||
{ first = 10, after }: UserToAllCommentsArgs
|
||||
) =>
|
||||
forUserAll: (userID: string, { first, after }: UserToAllCommentsArgs) =>
|
||||
retrieveAllCommentsUserConnection(ctx.mongo, ctx.tenant.id, userID, {
|
||||
first,
|
||||
first: defaultTo(first, 10),
|
||||
orderBy: GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
after,
|
||||
}).then(primeCommentsFromConnection(ctx)),
|
||||
forUserRejected: (
|
||||
userID: string,
|
||||
// Apply the graph schema defaults at the loader.
|
||||
{ first = 10, after }: UserToRejectedCommentsArgs
|
||||
{ first, after }: UserToRejectedCommentsArgs
|
||||
) =>
|
||||
retrieveRejectedCommentUserConnection(ctx.mongo, ctx.tenant.id, userID, {
|
||||
first,
|
||||
first: defaultTo(first, 10),
|
||||
orderBy: GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
after,
|
||||
}).then(primeCommentsFromConnection(ctx)),
|
||||
taggedForStory: (
|
||||
storyID: string,
|
||||
tag: GQLTAG,
|
||||
// Apply the graph schema defaults at the loader.
|
||||
{
|
||||
first = 10,
|
||||
orderBy = GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
after,
|
||||
}: StoryToCommentsArgs
|
||||
{ first, orderBy, after }: StoryToCommentsArgs
|
||||
) =>
|
||||
retrieveCommentStoryConnection(ctx.mongo, ctx.tenant.id, storyID, {
|
||||
first,
|
||||
orderBy,
|
||||
first: defaultTo(first, 10),
|
||||
orderBy: defaultTo(orderBy, GQLCOMMENT_SORT.CREATED_AT_DESC),
|
||||
after,
|
||||
filter: {
|
||||
// Filter optionally for comments with a specific tag.
|
||||
"tags.type": tag,
|
||||
},
|
||||
}).then(primeCommentsFromConnection(ctx)),
|
||||
forStory: (
|
||||
storyID: string,
|
||||
// Apply the graph schema defaults at the loader.
|
||||
{
|
||||
first = 10,
|
||||
orderBy = GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
after,
|
||||
}: StoryToCommentsArgs
|
||||
) =>
|
||||
forStory: (storyID: string, { first, orderBy, after }: StoryToCommentsArgs) =>
|
||||
retrieveCommentStoryConnection(ctx.mongo, ctx.tenant.id, storyID, {
|
||||
first,
|
||||
orderBy,
|
||||
first: defaultTo(first, 10),
|
||||
orderBy: defaultTo(orderBy, GQLCOMMENT_SORT.CREATED_AT_DESC),
|
||||
after,
|
||||
filter: {
|
||||
// Only get Comments that are top level. If the client wants to load
|
||||
@@ -239,12 +213,7 @@ export default (ctx: Context) => ({
|
||||
forParent: (
|
||||
storyID: string,
|
||||
parentID: string,
|
||||
// Apply the graph schema defaults at the loader.
|
||||
{
|
||||
first = 10,
|
||||
orderBy = GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
after,
|
||||
}: CommentToRepliesArgs
|
||||
{ first, orderBy, after }: CommentToRepliesArgs
|
||||
) =>
|
||||
retrieveCommentRepliesConnection(
|
||||
ctx.mongo,
|
||||
@@ -252,14 +221,14 @@ export default (ctx: Context) => ({
|
||||
storyID,
|
||||
parentID,
|
||||
{
|
||||
first,
|
||||
orderBy,
|
||||
first: defaultTo(first, 10),
|
||||
orderBy: defaultTo(orderBy, GQLCOMMENT_SORT.CREATED_AT_DESC),
|
||||
after,
|
||||
}
|
||||
).then(primeCommentsFromConnection(ctx)),
|
||||
parents: (comment: Comment, { last = 1, before }: CommentToParentsArgs) =>
|
||||
parents: (comment: Comment, { last, before }: CommentToParentsArgs) =>
|
||||
retrieveCommentParentsConnection(ctx.mongo, ctx.tenant.id, comment, {
|
||||
last,
|
||||
last: defaultTo(last, 1),
|
||||
// The cursor passed here is always going to be a number.
|
||||
before: before as number,
|
||||
}).then(primeCommentsFromConnection(ctx)),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import DataLoader from "dataloader";
|
||||
import { defaultTo } from "lodash";
|
||||
|
||||
import TenantContext from "coral-server/graph/tenant/context";
|
||||
import {
|
||||
@@ -99,9 +100,9 @@ export default (ctx: TenantContext) => ({
|
||||
cache: !ctx.disableCaching,
|
||||
}
|
||||
),
|
||||
connection: ({ first = 10, after, status, query }: QueryToStoriesArgs) =>
|
||||
connection: ({ first, after, status, query }: QueryToStoriesArgs) =>
|
||||
retrieveStoryConnection(ctx.mongo, ctx.tenant.id, {
|
||||
first,
|
||||
first: defaultTo(first, 10),
|
||||
after,
|
||||
filter: {
|
||||
// Merge the status filter into the connection filter.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import DataLoader from "dataloader";
|
||||
import { defaultTo } from "lodash";
|
||||
|
||||
import Context from "coral-server/graph/tenant/context";
|
||||
import {
|
||||
@@ -112,15 +113,9 @@ export default (ctx: Context) => {
|
||||
|
||||
return {
|
||||
user,
|
||||
connection: ({
|
||||
first = 10,
|
||||
after,
|
||||
role,
|
||||
query,
|
||||
status,
|
||||
}: QueryToUsersArgs) =>
|
||||
connection: ({ first, after, role, query, status }: QueryToUsersArgs) =>
|
||||
retrieveUserConnection(ctx.mongo, ctx.tenant.id, {
|
||||
first,
|
||||
first: defaultTo(first, 10),
|
||||
after,
|
||||
filter: {
|
||||
// Merge the role filters into the query.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GraphQLResolveInfo } from "graphql";
|
||||
import { defaultTo } from "lodash";
|
||||
|
||||
import { StoryNotFoundError } from "coral-server/errors";
|
||||
import { getRequestedFields } from "coral-server/graph/tenant/resolvers/util";
|
||||
@@ -83,9 +84,9 @@ export const Comment: GQLCommentTypeResolver<comment.Comment> = {
|
||||
},
|
||||
// Action Counts are encoded, decode them for use with the GraphQL system.
|
||||
actionCounts: c => decodeActionCounts(c.actionCounts),
|
||||
flags: ({ id }, { first = 10, after }, ctx) =>
|
||||
flags: ({ id }, { first, after }, ctx) =>
|
||||
ctx.loaders.CommentActions.connection({
|
||||
first,
|
||||
first: defaultTo(first, 10),
|
||||
after,
|
||||
filter: {
|
||||
actionType: ACTION_TYPE.FLAG,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { defaultTo } from "lodash";
|
||||
|
||||
import {
|
||||
CommentConnectionInput,
|
||||
retrieveCommentConnection,
|
||||
@@ -24,11 +26,12 @@ export const ModerationQueue: GQLModerationQueueTypeResolver<
|
||||
|
||||
return selector;
|
||||
},
|
||||
comments: ({ connection }, { first = 10, after }, { mongo, tenant }) =>
|
||||
retrieveCommentConnection(mongo, tenant.id, {
|
||||
comments: ({ connection }, { first, after }, { mongo, tenant }) => {
|
||||
return retrieveCommentConnection(mongo, tenant.id, {
|
||||
...connection,
|
||||
first,
|
||||
first: defaultTo(first, 10),
|
||||
after,
|
||||
orderBy: GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
}),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { attachDirectiveResolvers, IResolvers } from "graphql-tools";
|
||||
import {
|
||||
attachDirectiveResolvers,
|
||||
IResolvers,
|
||||
SchemaDirectiveVisitor,
|
||||
} from "graphql-tools";
|
||||
|
||||
import { loadSchema } from "coral-common/graphql";
|
||||
import auth from "coral-server/graph/common/directives/auth";
|
||||
import constraint from "coral-server/graph/common/directives/constraint";
|
||||
import resolvers from "coral-server/graph/tenant/resolvers";
|
||||
|
||||
export default function getTenantSchema() {
|
||||
@@ -10,5 +15,10 @@ export default function getTenantSchema() {
|
||||
// Attach the directive resolvers.
|
||||
attachDirectiveResolvers(schema, { auth });
|
||||
|
||||
// Attach the constraint directive.
|
||||
SchemaDirectiveVisitor.visitSchemaDirectives(schema, {
|
||||
constraint,
|
||||
});
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,12 @@ directive @auth(
|
||||
permit: [USER_AUTH_CONDITIONS!]
|
||||
) on FIELD_DEFINITION
|
||||
|
||||
"""
|
||||
constraint is a directive that will perform validation on input fields used as
|
||||
arguments to parameters passed in to operations.
|
||||
"""
|
||||
directive @constraint(min: Int, max: Int) on ARGUMENT_DEFINITION
|
||||
|
||||
################################################################################
|
||||
## Custom Scalar Types
|
||||
################################################################################
|
||||
@@ -372,7 +378,10 @@ type ModerationQueue {
|
||||
"""
|
||||
comments are the comments on the ModerationQueue.
|
||||
"""
|
||||
comments(first: Int = 10, after: Cursor): CommentsConnection!
|
||||
comments(
|
||||
first: Int = 10 @constraint(max: 50)
|
||||
after: Cursor
|
||||
): CommentsConnection!
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -1753,7 +1762,7 @@ type User {
|
||||
comments are the comments written by the User.
|
||||
"""
|
||||
comments(
|
||||
first: Int = 10
|
||||
first: Int = 10 @constraint(max: 50)
|
||||
orderBy: COMMENT_SORT = CREATED_AT_DESC
|
||||
after: Cursor
|
||||
): CommentsConnection!
|
||||
@@ -1766,14 +1775,18 @@ type User {
|
||||
"""
|
||||
allComments are comments regardless of visibility status.
|
||||
"""
|
||||
allComments(first: Int = 10, after: Cursor): CommentsConnection!
|
||||
@auth(roles: [ADMIN, MODERATOR])
|
||||
allComments(
|
||||
first: Int = 10 @constraint(max: 50)
|
||||
after: Cursor
|
||||
): CommentsConnection! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
rejectedComments are comments that have been rejected.
|
||||
"""
|
||||
rejectedComments(first: Int = 10, after: Cursor): CommentsConnection!
|
||||
@auth(roles: [ADMIN, MODERATOR])
|
||||
rejectedComments(
|
||||
first: Int = 10 @constraint(max: 50)
|
||||
after: Cursor
|
||||
): CommentsConnection! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
recentCommentHistory returns recent commenting history by the User.
|
||||
@@ -1785,7 +1798,7 @@ type User {
|
||||
that this User has created.
|
||||
"""
|
||||
commentModerationActionHistory(
|
||||
first: Int = 10
|
||||
first: Int = 10 @constraint(max: 50)
|
||||
after: Cursor
|
||||
): CommentModerationActionConnection! @auth(roles: [MODERATOR, ADMIN])
|
||||
|
||||
@@ -2127,7 +2140,7 @@ type Comment {
|
||||
recent last.
|
||||
"""
|
||||
statusHistory(
|
||||
first: Int = 10
|
||||
first: Int = 10 @constraint(max: 50)
|
||||
after: Cursor
|
||||
): CommentModerationActionConnection! @auth(roles: [MODERATOR, ADMIN])
|
||||
|
||||
@@ -2187,7 +2200,7 @@ type Comment {
|
||||
"""
|
||||
flags is the actual Flags that were left by the Users or the system.
|
||||
"""
|
||||
flags(first: Int = 10, after: Cursor): FlagsConnection!
|
||||
flags(first: Int = 10 @constraint(max: 50), after: Cursor): FlagsConnection!
|
||||
@auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
@@ -2466,7 +2479,7 @@ type Story {
|
||||
featuredComments are the Comments with the FEATURED tag on the Story.
|
||||
"""
|
||||
featuredComments(
|
||||
first: Int = 10
|
||||
first: Int = 10 @constraint(max: 50)
|
||||
orderBy: COMMENT_SORT = CREATED_AT_DESC
|
||||
after: Cursor
|
||||
): CommentsConnection!
|
||||
@@ -2475,7 +2488,7 @@ type Story {
|
||||
comments are the comments on the Story.
|
||||
"""
|
||||
comments(
|
||||
first: Int = 10
|
||||
first: Int = 10 @constraint(max: 50)
|
||||
orderBy: COMMENT_SORT = CREATED_AT_DESC
|
||||
after: Cursor
|
||||
): CommentsConnection!
|
||||
@@ -2571,7 +2584,7 @@ type Query {
|
||||
the dedicated edges for more optimized responses.
|
||||
"""
|
||||
comments(
|
||||
first: Int = 10
|
||||
first: Int = 10 @constraint(max: 50)
|
||||
after: Cursor
|
||||
storyID: ID
|
||||
status: COMMENT_STATUS
|
||||
@@ -2588,7 +2601,7 @@ type Query {
|
||||
stories returns filtered stories that can be paginated.
|
||||
"""
|
||||
stories(
|
||||
first: Int = 10
|
||||
first: Int = 10 @constraint(max: 50)
|
||||
after: Cursor
|
||||
status: STORY_STATUS
|
||||
query: String
|
||||
@@ -2607,7 +2620,7 @@ type Query {
|
||||
TODO: evaluate adding status based filtering.
|
||||
"""
|
||||
users(
|
||||
first: Int = 10
|
||||
first: Int = 10 @constraint(max: 50)
|
||||
after: Cursor
|
||||
role: USER_ROLE
|
||||
query: String
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Db } from "mongodb";
|
||||
import { Db, MongoError } from "mongodb";
|
||||
|
||||
import Migration from "coral-server/services/migrate/migration";
|
||||
import collections from "coral-server/services/mongodb/collections";
|
||||
@@ -331,9 +331,39 @@ export default class extends Migration {
|
||||
}
|
||||
|
||||
public async indexes(mongo: Db) {
|
||||
// Find all the collections that exist already.
|
||||
const results = await mongo
|
||||
.listCollections({}, { nameOnly: true })
|
||||
.toArray();
|
||||
|
||||
const collectionNames = results
|
||||
.filter(({ type }) => type === "collection")
|
||||
.map(({ name }) => name);
|
||||
|
||||
// Drop existing indexes on managed collections so we can re-create them.
|
||||
for (const collection of Object.values(collections)) {
|
||||
await collection(mongo).dropIndexes();
|
||||
for (const collectionName in collections) {
|
||||
if (!collections.hasOwnProperty(collectionName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check to see if this collection exists.
|
||||
if (!collectionNames.includes(collectionName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await mongo.collection(collectionName).dropIndexes();
|
||||
} catch (err) {
|
||||
if (err instanceof MongoError) {
|
||||
// If we're dropping indexes on a collection that doesn't exist, then
|
||||
// don't worry.
|
||||
if (err.code === 26) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-create the indexes for each collection now.
|
||||
|
||||
Reference in New Issue
Block a user