mirror of
https://github.com/wassname/talk.git
synced 2026-07-12 18:50:56 +08:00
283 lines
7.0 KiB
TypeScript
283 lines
7.0 KiB
TypeScript
import { merge } from "lodash";
|
|
import { Db } from "mongodb";
|
|
import uuid from "uuid";
|
|
|
|
import { Omit, Sub } from "talk-common/types";
|
|
import { GQLCOMMENT_SORT } from "talk-server/graph/tenant/schema/__generated__/types";
|
|
import { ActionCounts } from "talk-server/models/actions";
|
|
import { Connection, Cursor } from "talk-server/models/connection";
|
|
import Query from "talk-server/models/query";
|
|
import { TenantResource } from "talk-server/models/tenant";
|
|
|
|
function collection(db: Db) {
|
|
return db.collection<Readonly<Comment>>("comments");
|
|
}
|
|
|
|
export interface BodyHistoryItem {
|
|
body: string;
|
|
created_at: Date;
|
|
}
|
|
|
|
export interface StatusHistoryItem {
|
|
status: CommentStatus; // TODO: migrate field
|
|
assigned_by?: string;
|
|
created_at: Date;
|
|
}
|
|
|
|
export enum CommentStatus {
|
|
ACCEPTED = "ACCEPTED",
|
|
REJECTED = "REJECTED",
|
|
PREMOD = "PREMOD",
|
|
SYSTEM_WITHHELD = "SYSTEM_WITHHELD",
|
|
NONE = "NONE",
|
|
}
|
|
|
|
export interface Comment extends TenantResource {
|
|
readonly id: string;
|
|
parent_id?: string;
|
|
author_id: string;
|
|
asset_id: string;
|
|
body: string;
|
|
body_history: BodyHistoryItem[];
|
|
status: CommentStatus;
|
|
status_history: StatusHistoryItem[];
|
|
action_counts: ActionCounts;
|
|
reply_count: number;
|
|
created_at: Date;
|
|
deleted_at?: Date;
|
|
metadata?: {
|
|
[_: string]: any;
|
|
};
|
|
}
|
|
|
|
export type CreateCommentInput = Omit<
|
|
Comment,
|
|
| "id"
|
|
| "tenant_id"
|
|
| "created_at"
|
|
| "reply_count"
|
|
| "body_history"
|
|
| "status_history"
|
|
>;
|
|
|
|
export async function create(
|
|
db: Db,
|
|
tenantID: string,
|
|
input: CreateCommentInput
|
|
) {
|
|
const now = new Date();
|
|
|
|
// Pull out some useful properties from the input.
|
|
const { body, status } = input;
|
|
|
|
// default are the properties set by the application when a new comment is
|
|
// created.
|
|
const defaults: Sub<Comment, CreateCommentInput> = {
|
|
id: uuid.v4(),
|
|
tenant_id: tenantID,
|
|
created_at: now,
|
|
reply_count: 0,
|
|
body_history: [
|
|
{
|
|
body,
|
|
created_at: now,
|
|
},
|
|
],
|
|
status_history: [
|
|
{
|
|
status,
|
|
created_at: now,
|
|
},
|
|
],
|
|
};
|
|
|
|
// Merge the defaults and the input together.
|
|
const comment: Readonly<Comment> = merge({}, defaults, input);
|
|
|
|
// TODO: Check for existence of the parent ID before we create the comment.
|
|
|
|
// TODO: Check for existence of the asset ID before we create the comment.
|
|
|
|
// Insert it into the database.
|
|
await collection(db).insertOne(comment);
|
|
|
|
// TODO: update reply count of parent if exists.
|
|
|
|
return comment;
|
|
}
|
|
|
|
export async function retrieve(db: Db, tenantID: string, id: string) {
|
|
return collection(db).findOne({ id, tenant_id: tenantID });
|
|
}
|
|
|
|
export async function retrieveMany(db: Db, tenantID: string, ids: string[]) {
|
|
const cursor = await collection(db).find({
|
|
id: {
|
|
$in: ids,
|
|
},
|
|
tenant_id: tenantID,
|
|
});
|
|
|
|
const comments = await cursor.toArray();
|
|
|
|
return ids.map(id => comments.find(comment => comment.id === id) || null);
|
|
}
|
|
|
|
export interface ConnectionInput {
|
|
first: number;
|
|
orderBy: GQLCOMMENT_SORT;
|
|
after?: Cursor;
|
|
}
|
|
|
|
/**
|
|
* nodesToEdge converts a set of nodes and configuration options into a set of
|
|
* edges.
|
|
*
|
|
* @param input connection configuration
|
|
* @param nodes nodes returned from the query
|
|
*/
|
|
function nodesToEdge(input: ConnectionInput, nodes: Comment[]) {
|
|
let getCursor: (comment: Comment, index: number) => Cursor;
|
|
switch (input.orderBy) {
|
|
case GQLCOMMENT_SORT.CREATED_AT_DESC:
|
|
case GQLCOMMENT_SORT.CREATED_AT_ASC:
|
|
getCursor = comment => comment.created_at;
|
|
break;
|
|
case GQLCOMMENT_SORT.REPLIES_DESC:
|
|
case GQLCOMMENT_SORT.RESPECT_DESC:
|
|
getCursor = (_, index) =>
|
|
(input.after ? (input.after as number) : 0) + index + 1;
|
|
break;
|
|
}
|
|
|
|
return nodes.map((comment, index) => ({
|
|
node: comment,
|
|
cursor: getCursor(comment, index),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* retrieveRepliesConnection returns a Connection<Comment> for a given comments
|
|
* replies.
|
|
*
|
|
* @param db database connection
|
|
* @param parentID the parent id for the comment to retrieve
|
|
* @param input connection configuration
|
|
*/
|
|
export async function retrieveRepliesConnection(
|
|
db: Db,
|
|
tenantID: string,
|
|
assetID: string,
|
|
parentID: string,
|
|
input: ConnectionInput
|
|
) {
|
|
// Create the query.
|
|
const query = new Query(collection(db)).where({
|
|
tenant_id: tenantID,
|
|
asset_id: assetID,
|
|
parent_id: parentID,
|
|
});
|
|
|
|
// Return a connection for the comments query.
|
|
return retrieveConnection(input, query);
|
|
}
|
|
|
|
/**
|
|
* retrieveAssetConnection returns a Connection<Comment> for a given Asset's
|
|
* comments.
|
|
*
|
|
* @param db database connection
|
|
* @param assetID the Asset id for the comment to retrieve
|
|
* @param input connection configuration
|
|
*/
|
|
export async function retrieveAssetConnection(
|
|
db: Db,
|
|
tenantID: string,
|
|
assetID: string,
|
|
input: ConnectionInput
|
|
) {
|
|
// Create the query.
|
|
const query = new Query(collection(db)).where({
|
|
tenant_id: tenantID,
|
|
asset_id: assetID,
|
|
parent_id: null,
|
|
});
|
|
|
|
// Return a connection for the comments query.
|
|
return retrieveConnection(input, query);
|
|
}
|
|
|
|
/**
|
|
* retrieveConnection returns a Connection<Comment> for the given input and
|
|
* Query.
|
|
*
|
|
* @param input connection configuration
|
|
* @param query the Query for the set of nodes that should have the connection
|
|
* configuration applied
|
|
*/
|
|
async function retrieveConnection(
|
|
input: ConnectionInput,
|
|
query: Query<Comment>
|
|
) {
|
|
// Apply some sorting options.
|
|
switch (input.orderBy) {
|
|
case GQLCOMMENT_SORT.CREATED_AT_DESC:
|
|
query.orderBy({ created_at: -1 });
|
|
if (input.after) {
|
|
query.where({ created_at: { $lt: input.after as Date } });
|
|
}
|
|
break;
|
|
case GQLCOMMENT_SORT.CREATED_AT_ASC:
|
|
query.orderBy({ created_at: 1 });
|
|
if (input.after) {
|
|
query.where({ created_at: { $gt: input.after as Date } });
|
|
}
|
|
break;
|
|
case GQLCOMMENT_SORT.REPLIES_DESC:
|
|
query.orderBy({ reply_count: -1, created_at: -1 });
|
|
if (input.after) {
|
|
query.after(input.after as number);
|
|
}
|
|
break;
|
|
case GQLCOMMENT_SORT.RESPECT_DESC:
|
|
query.orderBy({ "action_counts.respect": -1, created_at: -1 });
|
|
if (input.after) {
|
|
query.after(input.after as number);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// We load one more than the limit so we can determine if there is
|
|
// another page of entries.
|
|
query.first(input.first + 1);
|
|
|
|
// Get the cursor.
|
|
const cursor = await query.exec();
|
|
|
|
// Get the comments from the cursor.
|
|
const nodes = await cursor.toArray();
|
|
|
|
// The hasNextPage is always handled the same (ask for one more than we need,
|
|
// if there is one more, than there is more).
|
|
let hasNextPage = false;
|
|
if (input.first >= 0 && nodes.length > input.first) {
|
|
// There was one more than we expected! Set hasNextPage = true and remove
|
|
// the last item from the array that we requested.
|
|
hasNextPage = true;
|
|
nodes.splice(input.first, 1);
|
|
}
|
|
|
|
// Convert the nodes to edges.
|
|
const edges = nodesToEdge(input, nodes);
|
|
|
|
// Return the connection.
|
|
const connection: Readonly<Connection<Readonly<Comment>>> = {
|
|
edges,
|
|
pageInfo: {
|
|
hasNextPage,
|
|
},
|
|
};
|
|
|
|
return connection;
|
|
}
|