feat: added parent/parents edges

This commit is contained in:
Wyatt Johnson
2018-09-22 00:26:09 -06:00
parent 0fbd27cdcb
commit 1702040537
9 changed files with 246 additions and 8 deletions
+5
View File
@@ -11379,6 +11379,11 @@
"source-map-support": "^0.5.1"
}
},
"graphql-fields": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/graphql-fields/-/graphql-fields-1.1.0.tgz",
"integrity": "sha1-okZtHEFFVNq4DA93d9a6mg4PdbQ="
},
"graphql-import": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.4.5.tgz",
+1
View File
@@ -48,6 +48,7 @@
"fs-extra": "^6.0.1",
"graphql": "^0.13.2",
"graphql-config": "^2.0.1",
"graphql-fields": "^1.1.0",
"graphql-playground-middleware-express": "^1.7.2",
"graphql-redis-subscriptions": "^1.5.0",
"graphql-tools": "^3.0.5",
@@ -3,14 +3,35 @@ import DataLoader from "dataloader";
import Context from "talk-server/graph/tenant/context";
import {
AssetToCommentsArgs,
CommentToParentsArgs,
CommentToRepliesArgs,
GQLCOMMENT_SORT,
} from "talk-server/graph/tenant/schema/__generated__/types";
import {
Comment,
retrieveCommentAssetConnection,
retrieveCommentParentsConnection,
retrieveCommentRepliesConnection,
retrieveManyComments,
} from "talk-server/models/comment";
import { Connection } from "talk-server/models/connection";
/**
* primeCommentsFromConnection will prime a given context with the comments
* retrieved via a connection.
*
* @param ctx graph context to use to prime the loaders.
*/
const primeCommentsFromConnection = (ctx: Context) => (
connection: Readonly<Connection<Readonly<Comment>>>
) => {
// For each of the edges, prime the comment loader.
connection.edges.forEach(({ node }) => {
ctx.loaders.Comments.comment.prime(node.id, node);
});
return connection;
};
export default (ctx: Context) => ({
comment: new DataLoader((ids: string[]) =>
@@ -29,7 +50,7 @@ export default (ctx: Context) => ({
first,
orderBy,
after,
}),
}).then(primeCommentsFromConnection(ctx)),
forParent: (
assetID: string,
parentID: string,
@@ -50,5 +71,11 @@ export default (ctx: Context) => ({
orderBy,
after,
}
),
).then(primeCommentsFromConnection(ctx)),
parents: (comment: Comment, { last = 1, before }: CommentToParentsArgs) =>
retrieveCommentParentsConnection(ctx.mongo, ctx.tenant.id, comment, {
last,
// The cursor passed here is always going to be a number.
before: before as number,
}).then(primeCommentsFromConnection(ctx)),
});
@@ -1,4 +1,8 @@
import { GQLCommentTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { getRequestedFields } from "talk-server/graph/tenant/resolvers/util";
import {
GQLComment,
GQLCommentTypeResolver,
} from "talk-server/graph/tenant/schema/__generated__/types";
import { Comment } from "talk-server/models/comment";
const Comment: GQLCommentTypeResolver<Comment> = {
@@ -17,6 +21,30 @@ const Comment: GQLCommentTypeResolver<Comment> = {
ctx.loaders.Users.user.load(comment.author_id),
replies: (comment, input, ctx) =>
ctx.loaders.Comments.forParent(comment.asset_id, comment.id, input),
parentCount: comment =>
comment.parent_id ? comment.grandparent_ids.length + 1 : 0,
replyCount: comment => comment.child_ids.length,
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) =>
ctx.loaders.Comments.parents(comment, input),
};
export default Comment;
@@ -0,0 +1,12 @@
import { GraphQLResolveInfo } from "graphql";
import graphqlFields from "graphql-fields";
import { pull } from "lodash";
/**
* getRequestedFields returns the fields in an array that are being queried for.
*
* @param info query information
*/
export function getRequestedFields<T>(info: GraphQLResolveInfo) {
return pull(Object.keys(graphqlFields<T>(info)), "__typename");
}
@@ -640,6 +640,11 @@ type Comment {
"""
status: COMMENT_STATUS!
"""
parentCount is the number of direct parents for this Comment.
"""
parentCount: Int
"""
replyCount is the number of replies. Only direct replies to this Comment
are counted. Deleted comments are included in this count.
@@ -655,6 +660,17 @@ type Comment {
after: Cursor
): CommentsConnection
"""
parent is the immediate parent of a given comment.
"""
parent: Comment
"""
parents returns a CommentsConnection that allows accessing direct parents of
the given Comment.
"""
parents(last: Int = 1, before: Cursor): CommentsConnection
"""
editing returns details about the edit status of a Comment.
"""
+129 -3
View File
@@ -43,7 +43,8 @@ export interface Comment extends TenantResource {
status: GQLCOMMENT_STATUS;
status_history: StatusHistoryItem[];
action_counts: ActionCounts;
reply_count: number;
grandparent_ids: string[];
child_ids: string[];
created_at: Date;
deleted_at?: Date;
metadata?: Record<string, any>;
@@ -54,7 +55,8 @@ export type CreateCommentInput = Omit<
| "id"
| "tenant_id"
| "created_at"
| "reply_count"
// child_ids are always empty for a new comment.
| "child_ids"
| "body_history"
| "status_history"
>;
@@ -75,7 +77,7 @@ export async function createComment(
id: uuid.v4(),
tenant_id: tenantID,
created_at: now,
reply_count: 0,
child_ids: [],
body_history: [
{
body,
@@ -102,6 +104,32 @@ export async function createComment(
return comment;
}
/**
* pushChildCommentIDOntoParent will push the new child comment's ID onto the
* parent comment so it can reference direct children.
*/
export async function pushChildCommentIDOntoParent(
mongo: Db,
tenantID: string,
parentID: string,
childID: string
) {
// This pushes the new child ID onto the parent comment.
const result = await collection(mongo).findOneAndUpdate(
{
tenant_id: tenantID,
id: parentID,
},
{
$push: {
child_ids: childID,
},
}
);
return result.value;
}
export type EditCommentInput = Pick<
Comment,
"id" | "author_id" | "body" | "status" | "metadata"
@@ -273,6 +301,104 @@ export async function retrieveCommentRepliesConnection(
return retrieveConnection(input, query);
}
/**
* retrieveCommentParentsConnection will return a comment connection used to
* represent the parents of a given comment.
*
* @param mongo the database connection to use when retrieving comments
* @param tenantID the tenant id for where the comment exists
* @param commentID the id of the comment to retrieve parents of
* @param pagination pagination options to paginate the results
*/
export async function retrieveCommentParentsConnection(
mongo: Db,
tenantID: string,
comment: Comment,
{ last: limit, before: skip = -1 }: { last: number; before?: number }
): Promise<Readonly<Connection<Readonly<Comment>>>> {
// // Get the base comment, which stores the references to all it's parents.
// const comment = await retrieveComment(mongo, tenantID, commentID);
// if (!comment) {
// throw new Error("comment not found");
// }
// Return nothing if this comment does not have any parents.
if (!comment.parent_id) {
return {
edges: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
};
}
// TODO: (wyattjoh) maybe throw an error when the limit is zero?
if (limit <= 0) {
return {
edges: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
};
}
// If the last paramter is 1, and the after paramter is either unset or equal
// to zero, then all we have to return is the direct parent.
if (limit === 1 && skip < 0) {
const parent = await retrieveComment(mongo, tenantID, comment.parent_id);
if (!parent) {
throw new Error("parent comment not found");
}
return {
edges: nodesToEdges([parent], () => -1),
pageInfo: {
hasNextPage: false,
hasPreviousPage: comment.grandparent_ids.length > 0,
endCursor: 0,
startCursor: 0,
},
};
}
// Create a list of all the comment parent ids, in reverse order.
const parentIDs = [comment.parent_id, ...comment.grandparent_ids.reverse()];
// Fetch the subset of the comment id's that we are going to query for.
const parentIDSubset = parentIDs.slice(skip + 1, skip + 1 + limit);
// Retrieve the parents via the subset list.
const parents = await retrieveManyComments(mongo, tenantID, parentIDSubset);
// Loop over the list to ensure that none of the entries is null (indicating
// that there was a misplaced parent). We can assert the type here because we
// will throw an error and abort if one of the comments are null.
const nodes = parents.filter(parentComment => {
if (!parentComment) {
// TODO: (wyattjoh) replace with a better error.
throw new Error("parent id specified does not exist");
}
return true;
}) as Array<Readonly<Comment>>;
const edges = nodesToEdges(nodes, (_, index) => index + skip + 1).reverse();
// Return the resolved connection.
return {
edges,
pageInfo: {
hasNextPage: false,
hasPreviousPage: parentIDs.length > limit + skip,
startCursor: edges.length > 0 ? edges[0].cursor : null,
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null,
},
};
}
/**
* retrieveAssetConnection returns a Connection<Comment> for a given Asset's
* comments.
+18 -2
View File
@@ -7,6 +7,7 @@ import {
CreateCommentInput,
editComment,
EditCommentInput,
pushChildCommentIDOntoParent,
retrieveComment,
} from "talk-server/models/comment";
import { Tenant } from "talk-server/models/tenant";
@@ -16,7 +17,7 @@ import { Request } from "talk-server/types/express";
export type CreateComment = Omit<
CreateCommentInput,
"status" | "action_counts" | "metadata"
"status" | "action_counts" | "metadata" | "grandparent_ids"
>;
export async function create(
@@ -35,6 +36,7 @@ export async function create(
// TODO: (wyattjoh) Check that the asset was visible.
const grandparentIDs: string[] = [];
if (input.parent_id) {
// Check to see that the reference parent ID exists.
const parent = await retrieveComment(mongo, tenant.id, input.parent_id);
@@ -44,6 +46,13 @@ export async function create(
}
// TODO: (wyattjoh) Check that the parent comment was visible.
// Push the parent's parent id's into the comment's grandparent id's.
grandparentIDs.push(...parent.grandparent_ids);
if (parent.parent_id) {
// If this parent has a parent, push it down as well.
grandparentIDs.push(parent.parent_id);
}
}
// Run the comment through the moderation phases.
@@ -61,11 +70,18 @@ export async function create(
...input,
status,
action_counts: {},
grandparent_ids: grandparentIDs,
metadata,
});
if (input.parent_id) {
// TODO: update reply count of parent.
// Push the child's ID onto the parent.
await pushChildCommentIDOntoParent(
mongo,
tenant.id,
input.parent_id,
comment.id
);
}
return comment;
+7
View File
@@ -0,0 +1,7 @@
declare module "graphql-fields" {
import { GraphQLResolveInfo } from "graphql";
export default function graphqlFields<T>(
info: GraphQLResolveInfo
): { [P in keyof T]: any };
}