From 72334a8f06d0ea27f5d3225594bfdd6fa2bfae1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Jedlicska?= Date: Fri, 25 Feb 2022 14:16:49 +0100 Subject: [PATCH] feat(server): add comment storage and retreive backedn --- .../components/viewer/CommentAddOverlay.vue | 1 + .../comments/graph/resolvers/comments.js | 37 +++++-- .../comments/graph/schemas/comments.gql | 12 ++- .../migrations/20220222173000_comments.js | 1 + .../server/modules/comments/services/index.js | 102 ++++++++++++------ packages/server/package-lock.json | 11 ++ packages/server/package.json | 1 + 7 files changed, 116 insertions(+), 49 deletions(-) diff --git a/packages/frontend/src/main/components/viewer/CommentAddOverlay.vue b/packages/frontend/src/main/components/viewer/CommentAddOverlay.vue index 1c985e230..83878d584 100644 --- a/packages/frontend/src/main/components/viewer/CommentAddOverlay.vue +++ b/packages/frontend/src/main/components/viewer/CommentAddOverlay.vue @@ -66,6 +66,7 @@ export default { methods: { async addComment() { let commentInput = { + streamId: this.$route.params.streamId, resources: [ { type: 'stream', id: this.$route.params.streamId }, { diff --git a/packages/server/modules/comments/graph/resolvers/comments.js b/packages/server/modules/comments/graph/resolvers/comments.js index c925b0d40..6d9d93741 100644 --- a/packages/server/modules/comments/graph/resolvers/comments.js +++ b/packages/server/modules/comments/graph/resolvers/comments.js @@ -3,9 +3,33 @@ const { authorizeResolver, pubsub } = require( `${appRoot}/modules/shared` ) const { ForbiddenError, UserInputError, ApolloError, withFilter } = require( 'apollo-server-express' ) const { getStream } = require( `${appRoot}/modules/core/services/streams` ) -const { createComment } = require( `${appRoot}/modules/comments/services` ) +const { getComment, getComments, createComment } = require( `${appRoot}/modules/comments/services` ) + +const authorizeStreamAccess = async ( { streamId, userId, auth } ) => { + const stream = await getStream( { streamId, userId } ) + if ( !stream ) + throw new ApolloError( 'Stream not found' ) + + if ( !stream.isPublic && auth === false ) + throw new ForbiddenError( 'You are not authorized.' ) + + if ( !stream.isPublic ) { + await authorizeResolver( userId, streamId, 'stream:reviewer' ) + } +} module.exports = { + Query: { + async comment( parent, args, context, info ) { + await authorizeStreamAccess( { streamId: args.streamId, userId: context.userId, auth: context.auth } ) + return await getComment( args.id ) + }, + + async comments( parent, args, context, info ) { + await authorizeStreamAccess( { streamId: args.streamId, userId: context.userId, auth: context.auth } ) + return await getComments( args ) + } + }, Stream: { async comments( parent, args, context, info ) { // TODO @@ -32,16 +56,7 @@ module.exports = { Mutation: { // Used for broadcasting real time chat head bubbles and status. Does not persist anything! async userCommentActivityBroadcast( parent, args, context, info ) { - let stream = await getStream( { streamId: args.streamId, userId: context.userId } ) - if ( !stream ) - throw new ApolloError( 'Stream not found' ) - - if ( !stream.isPublic && context.auth === false ) - throw new ForbiddenError( 'You are not authorized.' ) - - if ( !stream.isPublic ) { - await authorizeResolver( context.userId, args.streamId, 'stream:reviewer' ) - } + await authorizeStreamAccess( { streamId: args.streamId, userId: context.userId, auth: context.auth } ) await pubsub.publish( 'COMMENT_ACTIVITY', { userCommentActivity: args.data, diff --git a/packages/server/modules/comments/graph/schemas/comments.gql b/packages/server/modules/comments/graph/schemas/comments.gql index cc395b31c..d12c3d387 100644 --- a/packages/server/modules/comments/graph/schemas/comments.gql +++ b/packages/server/modules/comments/graph/schemas/comments.gql @@ -1,3 +1,9 @@ +extend type Query { + comment(id: String!, streamId: String!): Comment + + comments(streamId: String!, resources: [ResourceIdentifierInput]!, limit: Int = 25, cursor: String): CommentCollection +} + type Comment { id: String! authorId: String! @@ -13,8 +19,8 @@ type Comment { type CommentCollection { totalCount: Int! - cursor: String - items: [Comment] + cursor: DateTime! + items: [Comment]! } type ResourceIdentifier { @@ -50,7 +56,7 @@ type CommentReplyCollection { } input CommentCreateInput { - # streamId: String! + streamId: String! resources: [ResourceIdentifierInput]! text: String! data: JSONObject! diff --git a/packages/server/modules/comments/migrations/20220222173000_comments.js b/packages/server/modules/comments/migrations/20220222173000_comments.js index 3d8efcf01..f29afe441 100644 --- a/packages/server/modules/comments/migrations/20220222173000_comments.js +++ b/packages/server/modules/comments/migrations/20220222173000_comments.js @@ -7,6 +7,7 @@ exports.up = async ( knex ) => { table.timestamp( 'updatedAt' ).defaultTo( knex.fn.now( ) ) table.string( 'text' ) table.jsonb( 'data' ) + table.boolean( 'archived' ).defaultTo( false ).notNullable() table.string( 'parentComment', 10 ).references( 'id' ).inTable( 'comments' ).defaultTo( null ) } ) diff --git a/packages/server/modules/comments/services/index.js b/packages/server/modules/comments/services/index.js index 98648df73..f3dd7ec44 100644 --- a/packages/server/modules/comments/services/index.js +++ b/packages/server/modules/comments/services/index.js @@ -7,24 +7,45 @@ const Comments = () => knex( 'comments' ) const StreamComments = () => knex( 'stream_comments' ) const CommitComments = () => knex( 'commit_comments' ) const ObjectComments = () => knex( 'object_comments' ) -const CommentReplies = () => knex( 'comment_replies' ) +const intersection = require( 'lodash.intersection' ) const persistResourceLinks = async ( commentId, resources ) => { for ( const resource of resources ) { switch ( resource.type ) { // having the type as a string here, kinda makes having two N mapping tables useless case 'stream': - return await StreamComments().insert( { stream: resource.id, comment: commentId } ) + await StreamComments().insert( { stream: resource.id, comment: commentId } ) + break case 'commit': - return await CommitComments().insert( { commit: resource.id, comment: commentId } ) + await CommitComments().insert( { commit: resource.id, comment: commentId } ) + break case 'object': - return await ObjectComments().insert( { commit: resource.id, comment: commentId } ) + await ObjectComments().insert( { object: resource.id, comment: commentId } ) + break default: throw Error( `resource type ${resource.type} is not supported as a comment target` ) } } } +const getCommentsIdsForResource = async ( { id, type } ) => { + let commentLinks + switch ( type ) { + case 'stream': + commentLinks = await StreamComments().where( { stream: id } ) + break + case 'commit': + commentLinks = await CommitComments().where( { commit: id } ) + break + case 'object': + commentLinks = await ObjectComments().where( { object: id } ) + break + default: + throw Error( `No comments are supported for ${resource.type}` ) + } + return commentLinks.map( l => l.comment ) +} + module.exports = { async createComment( { userId, input } ) { let comment = { ...input } @@ -44,51 +65,62 @@ module.exports = { // TODO }, - // async archiveComment( {} ) { - // // TODO - // }, + async archiveComment( {} ) { + // TODO + }, async createCommentReply( {} ) { // TODO }, - async editCommentReply( {} ) { - // TODO - }, + // async editCommentReply( {} ) { + // // TODO + // }, // async archiveCommentReply( {} ) { // // TODO // }, - - async getComments({ resources }) { - + async getComment( id ) { + const [ comment ] = await Comments().where( { id } ) + return comment }, - async getStreamComments( { streamId, limit, archived, cursor } ) { - // TODO - limit = limit || 25 - let raw = `SELECT * from stream_comments - JOIN comments ON comments.id = stream_comments."comment" - WHERE stream_comments.stream = 'a55537c38f' - ORDER BY comments."createdAt" DESC - LIMIT 25 - ` - let query = Comments() - .columns( [ 'id', 'authorId', 'archived', 'createdAt', 'updatedAt', 'text', 'data' ] ) - .select() - .join( 'stream_comments', 'comment.id', 'stream_comments.commit' ) + async getComments( { resources, limit, cursor } ) { + // maybe since we are so streamId limited, asking for a streamId here would make sense + // and not treat the stream as a resource + const commentIds = await Promise.all( resources.map( r => getCommentsIdsForResource( r ) ) ) + + const relevantComments = intersection( ...commentIds ) + const items = await Comments().whereIn( 'id', relevantComments ).orderBy( 'createdAt' ).limit( limit ) + cursor = items[items.length - 1].createdAt + return { items, cursor, totalCount: relevantComments.length } }, - async getCommitComments( { commitId, limit, archived, cursor } ) { - // TODO - }, + // async getStreamComments( { streamId, limit, archived, cursor } ) { + // // TODO + // limit = limit || 25 + // let raw = `SELECT * from stream_comments + // JOIN comments ON comments.id = stream_comments."comment" + // WHERE stream_comments.stream = 'a55537c38f' + // ORDER BY comments."createdAt" DESC + // LIMIT 25 + // ` + // let query = Comments() + // .columns( [ 'id', 'authorId', 'archived', 'createdAt', 'updatedAt', 'text', 'data' ] ) + // .select() + // .join( 'stream_comments', 'comment.id', 'stream_comments.commit' ) + // }, - async getObjectComments( { objectId, limit, archived, cursor } ) { - // TODO - }, + // async getCommitComments( { commitId, limit, archived, cursor } ) { + // // TODO + // }, - async getCommentReplies( { commentId } ) { - // TODO - } + // async getObjectComments( { objectId, limit, archived, cursor } ) { + // // TODO + // }, + + // async getCommentReplies( { commentId } ) { + // // TODO + // } } diff --git a/packages/server/package-lock.json b/packages/server/package-lock.json index 8da04266f..98bb4c227 100644 --- a/packages/server/package-lock.json +++ b/packages/server/package-lock.json @@ -37,6 +37,7 @@ "lodash.chunk": "^4.2.0", "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", + "lodash.intersection": "^4.4.0", "lodash.merge": "^4.6.2", "lodash.set": "^4.3.2", "lodash.values": "^4.3.0", @@ -15234,6 +15235,11 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, + "node_modules/lodash.intersection": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.intersection/-/lodash.intersection-4.4.0.tgz", + "integrity": "sha1-ChG6Yx0OlcI8fy9Mu5ppLtF45wU=" + }, "node_modules/lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -35100,6 +35106,11 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, + "lodash.intersection": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.intersection/-/lodash.intersection-4.4.0.tgz", + "integrity": "sha1-ChG6Yx0OlcI8fy9Mu5ppLtF45wU=" + }, "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", diff --git a/packages/server/package.json b/packages/server/package.json index 969cd9fc4..c2134e655 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -46,6 +46,7 @@ "lodash.chunk": "^4.2.0", "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", + "lodash.intersection": "^4.4.0", "lodash.merge": "^4.6.2", "lodash.set": "^4.3.2", "lodash.values": "^4.3.0",