@wepublish/api-db-mongodb
Version:
We.publish Database adapter for mongoDB
322 lines (277 loc) • 7.86 kB
text/typescript
import {
DBCommentAdapter,
AddPublicCommentArgs,
UpdatePublicCommentArgs,
TakeActionOnCommentArgs,
OptionalComment,
Comment,
GetCommentsArgs,
GetPublicCommentsArgs,
ConnectionResult,
CommentState,
LimitType,
InputCursorType,
CommentSort,
SortOrder,
PublicComment,
OptionalPublicComment
} from '@wepublish/api'
import {Collection, Db, FilterQuery, MongoCountPreferences} from 'mongodb'
import {Cursor} from './cursor'
import {MaxResultsPerPage} from './defaults'
import {CollectionName, DBComment} from './schema'
function commentSortFieldForSort(sort: CommentSort) {
switch (sort) {
case CommentSort.CreatedAt:
return 'createdAt'
case CommentSort.ModifiedAt:
return 'modifiedAt'
}
}
function commentDateForSort(comment: DBComment, sort: CommentSort): Date {
switch (sort) {
case CommentSort.CreatedAt:
return comment.createdAt
case CommentSort.ModifiedAt:
return comment.modifiedAt
}
}
export class MongoDBCommentAdapter implements DBCommentAdapter {
private comments: Collection<DBComment>
private locale: string
constructor(db: Db, locale: string) {
this.comments = db.collection(CollectionName.Comments)
this.locale = locale
}
async addPublicComment({input}: AddPublicCommentArgs): Promise<PublicComment> {
const {text, ...data} = input
const {ops} = await this.comments.insertOne({
...data,
revisions: [
{
text,
createdAt: new Date()
}
],
createdAt: new Date(),
modifiedAt: new Date()
})
const {_id: id, ...comment} = ops[0]
return {
...comment,
id,
text
}
}
async updatePublicComment({
id,
text,
state
}: UpdatePublicCommentArgs): Promise<OptionalPublicComment> {
const {value} = await this.comments.findOneAndUpdate(
{_id: id},
{
$set: {
state,
modifiedAt: new Date()
},
$addToSet: {
revisions: {
text,
createdAt: new Date()
}
}
},
{
returnOriginal: false
}
)
if (!value) return null
const {_id: outID, ...comment} = value
return {
...comment,
id: outID,
text
}
}
async getComments({
filter,
sort,
order,
cursor,
limit
}: GetCommentsArgs): Promise<ConnectionResult<Comment>> {
const metaFilters: FilterQuery<any> = []
if (filter?.state) {
metaFilters.push({state: filter.state})
}
const limitCount = Math.min(limit.count, MaxResultsPerPage)
const sortDirection = limit.type === LimitType.First ? order : -order
const cursorData = cursor.type !== InputCursorType.None ? Cursor.from(cursor.data) : undefined
const expr =
order === SortOrder.Ascending
? cursor.type === InputCursorType.After
? '$gt'
: '$lt'
: cursor.type === InputCursorType.After
? '$lt'
: '$gt'
const sortField = commentSortFieldForSort(sort)
const cursorFilter = cursorData
? {
$or: [
{[sortField]: {[expr]: cursorData.date}},
{_id: {[expr]: cursorData.id}, [sortField]: cursorData.date}
]
}
: {}
const [totalCount, comments] = await Promise.all([
this.comments.countDocuments({}, {
collation: {locale: this.locale, strength: 2}
} as MongoCountPreferences), // MongoCountPreferences doesn't include collation
this.comments
.aggregate(
[
// sort depending on state value
{
$addFields: {
stateSort: {
$indexOfArray: [
[
CommentState.PendingApproval,
CommentState.PendingUserChanges,
CommentState.Approved,
CommentState.Rejected
],
'$state'
]
}
}
},
{
$sort: {
stateSort: 1,
[sortField]: sortDirection
}
}
],
{collation: {locale: this.locale, strength: 2}}
)
.match(metaFilters.length ? {$and: metaFilters} : {})
.match(cursorFilter)
.skip(limit.skip ?? 0)
.limit(limitCount + 1)
.toArray()
])
const nodes = comments.slice(0, limitCount)
if (limit.type === LimitType.Last) {
nodes.reverse()
}
const hasNextPage =
limit.type === LimitType.First
? comments.length > limitCount
: cursor.type === InputCursorType.Before
const hasPreviousPage =
limit.type === LimitType.Last
? comments.length > limitCount
: cursor.type === InputCursorType.After
const firstComment = nodes[0]
const lastComment = nodes[nodes.length - 1]
const startCursor = firstComment
? new Cursor(firstComment._id, commentDateForSort(firstComment, sort)).toString()
: null
const endCursor = lastComment
? new Cursor(lastComment._id, commentDateForSort(lastComment, sort)).toString()
: null
return {
nodes: comments.map<Comment>(({_id: id, ...comment}) => ({id, ...comment})),
pageInfo: {
startCursor,
endCursor,
hasNextPage,
hasPreviousPage
},
totalCount
}
}
async getPublicCommentsForItemByID(args: GetPublicCommentsArgs): Promise<PublicComment[]> {
const {id, userID} = args
let userUnapprovedComments: DBComment[] = []
const comments = await this.comments
.aggregate([{$sort: {modifiedAt: -1}}], {
collation: {locale: this.locale, strength: 2}
})
.match({
$and: [{itemID: id}, {state: CommentState.Approved, parentID: null}]
})
.toArray()
if (userID) {
userUnapprovedComments = await this.comments
.aggregate([{$sort: {'modifiedAt.date': -1}}], {
collation: {locale: this.locale, strength: 2}
})
.match({
$and: [{itemID: id}, {userID}, {state: {$ne: CommentState.Approved}}, {parentID: null}]
})
.toArray()
}
return [...userUnapprovedComments, ...comments].map<PublicComment>(
({_id: id, revisions, ...comment}) => ({
id,
text: revisions[revisions.length - 1].text,
...comment
})
)
}
async getCommentById(id: string): Promise<OptionalComment> {
const value = await this.comments.findOne({_id: id})
if (!value) return null
const {_id: outID, ...comment} = value
return {
id: outID,
...comment
}
}
async getPublicChildrenCommentsByParentId(id: string, userID: string): Promise<PublicComment[]> {
const [childrenComments] = await Promise.all([
this.comments
.aggregate([{$sort: {modifiedAt: -1}}], {
collation: {locale: this.locale, strength: 2}
})
.match({
$and: [{parentID: id}, {$or: [{state: CommentState.Approved}, {userID: userID}]}]
})
.toArray()
])
return childrenComments.map<PublicComment>(({_id: id, revisions, ...comment}) => ({
id,
text: revisions[revisions.length - 1].text,
...comment
}))
}
async takeActionOnComment({
id,
state,
rejectionReason
}: TakeActionOnCommentArgs): Promise<OptionalComment> {
const {value} = await this.comments.findOneAndUpdate(
{_id: id},
{
$set: {
state,
rejectionReason,
modifiedAt: new Date()
}
},
{
returnOriginal: false
}
)
if (!value) return null
const {_id: outID, ...comment} = value
return {
...comment,
id: outID
}
}
}