233 lines
8.7 KiB
JavaScript
233 lines
8.7 KiB
JavaScript
'use strict'
|
|
const crs = require('crypto-random-string')
|
|
const appRoot = require('app-root-path')
|
|
const knex = require(`${appRoot}/db/knex`)
|
|
|
|
const Streams = () => knex( 'streams' )
|
|
const Objects = () => knex( 'objects' )
|
|
const Commits = () => knex( 'commits' )
|
|
const Comments = () => knex( 'comments' )
|
|
const CommentLinks = () => knex( 'comment_links' )
|
|
const CommentViews = () => knex( 'comment_views' )
|
|
|
|
const persistResourceLinks = async (commentId, resources) =>
|
|
await Promise.all(resources.map(res => persistResourceLink(commentId, res)))
|
|
|
|
const persistResourceLink = async (commentId, { resourceId, resourceType }) => {
|
|
//should the resource belonging to the stream stuff be validated here?
|
|
let query
|
|
switch (resourceType) {
|
|
case 'stream':
|
|
query = Streams()
|
|
break
|
|
case 'commit':
|
|
query = Commits()
|
|
break
|
|
case 'object':
|
|
query = Objects()
|
|
break
|
|
case 'comment':
|
|
query = Comments()
|
|
break
|
|
default:
|
|
throw Error(`resource type ${resourceType} is not supported as a comment target`)
|
|
}
|
|
//make sure, that the referenced resource exists
|
|
if (!(await query.where({ id: resourceId })).length) throw Error(`${resourceType}: ${resourceId} doesn't exist, you cannot comment on it`)
|
|
await CommentLinks().insert({ commentId, resourceId, resourceType })
|
|
}
|
|
|
|
const getResourcesForComment = async ({ id }) =>
|
|
await CommentLinks().select('resourceId', 'resourceType').where({ commentId: id })
|
|
|
|
const getCommentLinksForResources = async (streamId, resources, archived) => {
|
|
const resourceIds = resources.map(r => r.resourceId)
|
|
let query = CommentLinks()
|
|
.join('comments', 'comment_links.commentId', '=', 'comments.id')
|
|
.whereIn('resourceId', resourceIds)
|
|
.select('commentId', 'resourceId', 'resourceType', 'archived')
|
|
|
|
if (!archived) query = query.where({ archived })
|
|
let commentLinks = await query
|
|
|
|
const objectIds = resources.filter(res => res.resourceType === 'object').map(r => r.resourceId)
|
|
if (objectIds.length) {
|
|
const streamObjectIds = (await Objects().where({ streamId }).whereIn('id', objectIds)).map(o => o.id)
|
|
// if a comment link is of type object, check if the object belongs to the stream, other types do not need filtering
|
|
// since all other types are directly linked to a stream
|
|
commentLinks = commentLinks.filter(link => link.resourceType === 'object' ? streamObjectIds.includes(link.resourceId) : true)
|
|
}
|
|
|
|
// group comment links by comment ids, so that the resources can be filtered below
|
|
let commentGroups = {}
|
|
for (const link of commentLinks) {
|
|
if (!(link.commentId in commentGroups)) commentGroups[link.commentId] = []
|
|
commentGroups[link.commentId].push(link.resourceId)
|
|
}
|
|
|
|
const relevantCommentIds = Object
|
|
.keys(commentGroups)
|
|
.filter(
|
|
// make sure, that the given comment targets exactly the same set of resources, as the input requested
|
|
commentId => commentGroups[commentId].length === resourceIds.length && resourceIds.every(resId => commentGroups[commentId].includes(resId))
|
|
)
|
|
return commentLinks.filter(l => relevantCommentIds.includes(l.commentId))
|
|
}
|
|
|
|
module.exports = {
|
|
async createComment({ userId, input }) {
|
|
if (input.resources.length < 1) throw Error('Must specify at least one resource as the comment target')
|
|
|
|
const streamResources = input.resources.filter(r => r.resourceType === 'stream')
|
|
if (streamResources.length > 1) throw Error('Commenting on multiple streams is not supported')
|
|
|
|
const [stream] = streamResources
|
|
if (stream && stream.resourceId !== input.streamId) throw Error('Input streamId doesn\'t match the stream resource.resourceId')
|
|
|
|
const commentResource = input.resources.find( r => r.resourceType === 'comment' )
|
|
|
|
let comment = { ...input }
|
|
|
|
delete comment.resources
|
|
delete comment.streamId
|
|
|
|
comment.id = crs({ length: 10 })
|
|
comment.authorId = userId
|
|
|
|
await Comments().insert( comment )
|
|
await persistResourceLinks( comment.id, input.resources )
|
|
|
|
if ( commentResource ) {
|
|
await Comments().where( { id: commentResource.resourceId } ).update( { updatedAt: knex.fn.now( ) } )
|
|
}
|
|
|
|
await module.exports.viewComment( { userId, commentId: comment.id } ) // so we don't self mark a comment as unread the moment it's created
|
|
return comment.id
|
|
},
|
|
|
|
async createCommentReply( { authorId, parentCommentId, text, data } ) {
|
|
let comment = { id: crs( { length: 10 } ), authorId, text, data }
|
|
await Comments().insert( comment )
|
|
await persistResourceLink( comment.id, { resourceId: parentCommentId, resourceType: 'comment' } )
|
|
await Comments().where( { id: parentCommentId } ).update( { updatedAt: knex.fn.now( ) } )
|
|
|
|
return comment.id
|
|
},
|
|
|
|
async editComment({ }) {
|
|
// TODO
|
|
},
|
|
|
|
async viewComment( { userId, commentId } ) {
|
|
let query = CommentViews().insert( { commentId: commentId, userId: userId, viewedAt: knex.fn.now() } )
|
|
.onConflict( knex.raw( '("commentId","userId")' ) )
|
|
.merge()
|
|
await query
|
|
},
|
|
|
|
async getComment( { id, userId = null } ) {
|
|
let query = Comments().select( '*' )
|
|
.joinRaw( `
|
|
join(
|
|
select cl."commentId" as id, JSON_AGG(json_build_object('resourceId', cl."resourceId", 'resourceType', cl."resourceType")) as resources
|
|
from comment_links cl
|
|
join comments on comments.id = cl."commentId"
|
|
group by cl."commentId"
|
|
) res using(id)`
|
|
)
|
|
if ( userId ) {
|
|
query.leftOuterJoin( 'comment_views', b => {
|
|
b.on( 'comment_views.commentId', '=', 'comments.id' )
|
|
b.andOn( 'comment_views.userId', '=', knex.raw( '?', userId ) )
|
|
} )
|
|
}
|
|
query.where( { id } ).first()
|
|
let res = await query
|
|
return res
|
|
},
|
|
|
|
async archiveComment({ commentId, archived = true }) {
|
|
let [comments] = await Comments().where({ id: commentId }).count()
|
|
if (parseInt(comments.count) < 1) throw new Error(`No comment ${commentId} exists, cannot change its archival status`)
|
|
return await Comments().where({ id: commentId }).update({ archived })
|
|
},
|
|
|
|
async getCommentsOld({ streamId, resources, limit, cursor, archived = false }) {
|
|
// maybe since we are so streamId limited, asking for a streamId here would make sense
|
|
const commentLinks = await getCommentLinksForResources(streamId, resources, archived)
|
|
const relevantComments = [...new Set(commentLinks.map(l => l.commentId))]
|
|
let query = Comments().whereIn('id', relevantComments).orderBy('createdAt')
|
|
if (cursor) query = query.where('createdAt', '>', cursor.toISOString())
|
|
|
|
if (!archived) query = query.andWhere({ archived })
|
|
const defaultLimit = 100
|
|
let items = await query.limit(limit ?? defaultLimit)
|
|
if (items.length) {
|
|
cursor = items[items.length - 1].createdAt
|
|
} else {
|
|
cursor = null
|
|
}
|
|
items = await Promise.all(items.map(async comment => ({ ...comment, resources: await getResourcesForComment(comment) })))
|
|
return { items, cursor, totalCount: relevantComments.length }
|
|
},
|
|
|
|
async getComments( { resources, limit, cursor, userId = null, replies = false, archived = false } ) {
|
|
let query = knex.with( 'comms', cte => {
|
|
cte.select( ).distinctOn('id').from( 'comments' )
|
|
cte.join( 'comment_links', 'comments.id', '=', 'commentId' )
|
|
|
|
if ( userId ){
|
|
// link viewed At
|
|
cte.leftOuterJoin( 'comment_views', b => {
|
|
b.on( 'comment_views.commentId', '=', 'comments.id' )
|
|
b.andOn( 'comment_views.userId', '=', knex.raw( '?', userId ) )
|
|
} )
|
|
}
|
|
|
|
cte.where( q => {
|
|
// link resources
|
|
for ( let res of resources ) {
|
|
q.orWhere( 'comment_links.resourceId', '=', res.resourceId )
|
|
}
|
|
} )
|
|
if ( !replies ) {
|
|
cte.whereNull( 'parentComment' )
|
|
}
|
|
cte.where( 'archived', '=', archived )
|
|
} )
|
|
|
|
query.select( ).from( 'comms' )
|
|
|
|
// total count coming from our cte
|
|
query.joinRaw( 'right join (select count(*) from comms) c(total_count) on true' )
|
|
|
|
// get comment's all linked resources
|
|
query.joinRaw( `
|
|
join(
|
|
select cl."commentId" as id, JSON_AGG(json_build_object('resourceId', cl."resourceId", 'resourceType', cl."resourceType")) as resources
|
|
from comment_links cl
|
|
join comms on comms.id = cl."commentId"
|
|
group by cl."commentId"
|
|
) res using(id)`
|
|
)
|
|
|
|
if ( cursor ) {
|
|
query.where( 'createdAt', '<', cursor )
|
|
}
|
|
|
|
query.orderBy( 'createdAt', 'desc' )
|
|
query.limit( limit ?? 10 )
|
|
|
|
let rows = await query
|
|
let totalCount = rows && rows.length > 0 ? parseInt( rows[0].total_count ) : 0
|
|
let nextCursor = rows && rows.length > 0 ? rows[rows.length - 1].createdAt : null
|
|
|
|
return {
|
|
items: rows,
|
|
cursor: nextCursor,
|
|
totalCount
|
|
}
|
|
}
|
|
|
|
}
|