diff --git a/packages/server/modules/comments/graph/resolvers/comments.js b/packages/server/modules/comments/graph/resolvers/comments.js index d9a004a3f..93806b34c 100644 --- a/packages/server/modules/comments/graph/resolvers/comments.js +++ b/packages/server/modules/comments/graph/resolvers/comments.js @@ -3,9 +3,27 @@ const { authorizeResolver, pubsub } = require( `${appRoot}/modules/shared` ) const { ForbiddenError, UserInputError, ApolloError, withFilter } = require( 'apollo-server-express' ) const { getStream } = require( '../../../core/services/streams' ) -module.exports = { - Query: {}, - Mutation:{ +module.exports = { + Stream: { + async comments( parent, args, context, info ) { + // TODO + }, + async comment( parent, args, context, info ) { + // TODO + } + }, + Commit: { + async comments( parent, args, context, info ) { + // TODO + } + }, + Object: { + async comments( parent, args, context, info ) { + // TODO + } + }, + Mutation: { + // Used for broadcasting real time chat head bubbles and status. async userCommentActivityBroadcast( parent, args, context, info ) { let stream = await getStream( { streamId: args.streamId, userId: context.userId } ) if ( !stream ) @@ -25,16 +43,33 @@ module.exports = { } ) return true }, - async userCommentCreate( parent, args, context, info ) { + + async commentCreate( parent, args, context, info ) { + // TODO: check perms, persist comment console.log( args ) - // TODO: persist comment + await pubsub.publish( 'COMMENT_CREATED', { - userCommentCreated: args.comment, + commentCreated: args.comment, streamId: args.streamId, resourceId: args.resourceId } ) return true - } + }, + async commentEdit( parent, args, context, info ) { + // TODO + }, + async commentReply( parent, args, context, info ) { + // TODO + await pubsub.publish( 'COMMENT_REPLY_CREATED', { + commentCreated: args.comment, + streamId: args.streamId, + resourceId: args.resourceId + } ) + return true + }, + async commentReplyEdit( parent, args, context, info ) { + // TODO + }, }, Subscription:{ userCommentActivity: { @@ -43,11 +78,17 @@ module.exports = { return payload.streamId === variables.streamId && payload.resourceId === variables.resourceId } ) }, - userCommentCreated: { + commentCreated: { subscribe: withFilter( () => pubsub.asyncIterator( [ 'COMMENT_CREATED' ] ), async( payload, variables, context ) => { await authorizeResolver( context.userId, payload.streamId, 'stream:reviewer' ) return payload.streamId === variables.streamId && payload.resourceId === variables.resourceId } ) + }, + commentReplyCreated: { + subscribe: withFilter( () => pubsub.asyncIterator( [ 'COMMENT_REPLY_CREATED' ] ), async( payload, variables, context ) => { + await authorizeResolver( context.userId, payload.streamId, 'stream:reviewer' ) + return payload.streamId === variables.streamId && payload.resourceId === variables.resourceId + } ) } } } \ No newline at end of file diff --git a/packages/server/modules/comments/graph/schemas/comments.gql b/packages/server/modules/comments/graph/schemas/comments.gql index 6c2b68922..8508f8d0c 100644 --- a/packages/server/modules/comments/graph/schemas/comments.gql +++ b/packages/server/modules/comments/graph/schemas/comments.gql @@ -1,15 +1,97 @@ +type Comment { + id: String! + authorId: String! + archived: Boolean! + createdAt: DateTime! + updatedAt: DateTime! + text: String! + data: JSONObject! + resources: [String]! + replies: CommentReplyCollection +} + +type CommentReply { + id: String! + authorId: String! + archived: Boolean! + createdAt: DateTime! + updatedAt: DateTime! + text: String! +} + +type CommentCollection { + totalCount: Int! + cursor: String + items: [Comment] +} + +type CommentReplyCollection { + totalCount: Int! + cursor: String + items: [CommentReply] +} + +input CommentCreateInput { + streamId: String! + resources: [String]! + text: String! + data: JSONObject! +} + +input CommentEditInput { + streamId: String! + id: String! + text: String! + data: JSONObject! +} + +input ReplyCreateInput { + text: String! + data: JSONObject! +} + +input ReplyEditInput { + id: String! + text: String! + data: JSONObject! +} + +extend type Stream { + comments(limit: Int! = 20, archived: Boolean = false, cursor: String): Boolean + comment(id: String!): Comment +} + +extend type Commit { + comments(limit: Int! = 20, cursor: String): Boolean +} + +extend type Object { + comments(limit: Int! = 20, cursor: String): Boolean +} + extend type Mutation { + # Used for broadcasting real time chat head bubbles and status. userCommentActivityBroadcast( streamId: String! resourceId: String! data: JSONObject ): Boolean! @hasRole(role: "server:user") @hasScope(scope: "streams:read") - userCommentCreate( - streamId: String! - resourceId: String! - comment: JSONObject! - ): Boolean! @hasRole(role: "server:user") @hasScope(scope: "streams:read") + commentCreate(input: CommentCreateInput!): Boolean! + @hasRole(role: "server:user") + @hasScope(scope: "streams:read") + + commentEdit(streamId: String!, comment: JSONObject!): Boolean! + @hasRole(role: "server:user") + @hasScope(scope: "streams:read") + + commentReply(streamId: String!, commentReply: JSONObject!): Boolean! + @hasRole(role: "server:user") + @hasScope(scope: "streams:read") + + commentReplyEdit(streamId: String!, commentReply: JSONObject!): Boolean! + @hasRole(role: "server:user") + @hasScope(scope: "streams:read") } extend type Subscription { @@ -17,14 +99,11 @@ extend type Subscription { @hasRole(role: "server:user") @hasScope(scope: "streams:read") - userCommentCreated(streamId: String!, resourceId: String!): JSONObject + commentCreated(streamId: String!, resourceId: String!): JSONObject + @hasRole(role: "server:user") + @hasScope(scope: "streams:read") + + commentReplyCreated(streamId: String!, commentId: String!): JSONObject @hasRole(role: "server:user") @hasScope(scope: "streams:read") } - -type Comment { - id: String! - text: String! - authorId: String! - # TBD: how do we index on streams, commits, objects, selected objects, etc. -} diff --git a/packages/server/modules/comments/migrations/20220222173000_comments.js b/packages/server/modules/comments/migrations/20220222173000_comments.js new file mode 100644 index 000000000..6d4b7ffff --- /dev/null +++ b/packages/server/modules/comments/migrations/20220222173000_comments.js @@ -0,0 +1,50 @@ +// /* istanbul ignore file */ +exports.up = async ( knex ) => { + await knex.schema.createTable( 'comments', table => { + table.string( 'id', 10 ).primary( ) + table.string( 'authorId', 10 ).references( 'id' ).inTable( 'users' ).notNullable().index( ) + table.boolean( 'archived' ).defaultTo( false ).index( ) + table.timestamp( 'createdAt' ).defaultTo( knex.fn.now( ) ) + table.timestamp( 'updatedAt' ).defaultTo( knex.fn.now( ) ) + table.string( 'text' ) + table.jsonb( 'data' ) + } ) + + // Comments -< Replies + await knex.schema.createTable( 'comment_replies', table => { + table.string( 'id', 10 ).primary( ) + table.string( 'authorId', 10 ).references( 'id' ).inTable( 'users' ).notNullable().index( ) + table.boolean( 'archived' ).defaultTo( false ).index( ) + table.timestamp( 'createdAt' ).defaultTo( knex.fn.now( ) ) + table.timestamp( 'updatedAt' ).defaultTo( knex.fn.now( ) ) + table.string( 'text' ) + } ) + + // Streams >- -< Comments + // Minor futureproofing: a comment can be written "on top of" multiple resources from multiple streams. + await knex.schema.createTable( 'stream_comments', table => { + table.string( 'stream', 10 ).references( 'id' ).inTable( 'streams' ).notNullable() + table.string( 'comment', 10 ).references( 'id' ).inTable( 'comments' ).notNullable() + } ) + + // Commits >- -< Comments + await knex.schema.createTable( 'commit_comments', table => { + table.string( 'commit', 10 ).references( 'id' ).inTable( 'commits' ).notNullable() + table.string( 'comment', 10 ).references( 'id' ).inTable( 'comments' ).notNullable() + } ) + + // Objects >- -< Comments + await knex.schema.createTable( 'object_comments', table => { + table.string( 'object' ) // note: not a FK because we don't enforce uniqeness to speed things up + table.string( 'comment', 10 ).references( 'id' ).inTable( 'comments' ).notNullable() + table.index( [ 'object', 'comment' ] ) + } ) +} + +exports.down = async ( knex ) => { + await knex.schema.dropTableIfExists( 'object_comments' ) + await knex.schema.dropTableIfExists( 'commit_comments' ) + await knex.schema.dropTableIfExists( 'stream_comments' ) + await knex.schema.dropTableIfExists( 'comment_replies' ) + await knex.schema.dropTableIfExists( 'comments' ) +} \ No newline at end of file diff --git a/packages/server/modules/comments/services/index.js b/packages/server/modules/comments/services/index.js new file mode 100644 index 000000000..5eb4f0fc4 --- /dev/null +++ b/packages/server/modules/comments/services/index.js @@ -0,0 +1,50 @@ +'use strict' + +const appRoot = require( 'app-root-path' ) +const knex = require( `${appRoot}/db/knex` ) + +const Comments = () => knex( 'comments' ) +const CommentReplies = () => knex( 'comments' ) + +module.exports = { + async createComment( {} ) { + // TODO + }, + + async editComment( {} ) { + // TODO + }, + + async archiveComment( {} ) { + // TODO + }, + + async createCommentReply( {} ) { + // TODO + }, + + async editCommentReply( {} ) { + // TODO + }, + + async archiveCommentReply( {} ) { + // TODO + }, + + async getStreamComments( {} ) { + // TODO + }, + + async getCommitComments( {} ) { + // TODO + }, + + async getObjectComments( {} ) { + // TODO + }, + + async getCommentReplies( {} ) { + // TODO + } +} + diff --git a/packages/server/modules/core/graph/schemas/streams.graphql b/packages/server/modules/core/graph/schemas/streams.graphql index 06bb16d40..4b5f5cc2b 100644 --- a/packages/server/modules/core/graph/schemas/streams.graphql +++ b/packages/server/modules/core/graph/schemas/streams.graphql @@ -2,16 +2,21 @@ extend type Query { """ Returns a specific stream. """ - stream( id: String! ): Stream + stream(id: String!): Stream """ All the streams of the current user, pass in the `query` parameter to search by name, description or ID. """ - streams( query: String, limit: Int = 25, cursor: String ): StreamCollection + streams(query: String, limit: Int = 25, cursor: String): StreamCollection @hasScope(scope: "streams:read") - adminStreams( offset: Int = 0, query: String, orderBy: String, visibility: String, limit: Int = 25 ): StreamCollection - @hasRole(role: "server:admin") + adminStreams( + offset: Int = 0 + query: String + orderBy: String + visibility: String + limit: Int = 25 + ): StreamCollection @hasRole(role: "server:admin") } type Stream { @@ -25,7 +30,7 @@ type Stream { role: String createdAt: DateTime! updatedAt: DateTime! - collaborators: [ StreamCollaborator ]! + collaborators: [StreamCollaborator]! size: String } @@ -33,7 +38,7 @@ extend type User { """ All the streams that a user has access to. """ - streams( limit: Int! = 25, cursor: String ): StreamCollection + streams(limit: Int! = 25, cursor: String): StreamCollection } type StreamCollaborator { @@ -47,48 +52,45 @@ type StreamCollaborator { type StreamCollection { totalCount: Int! cursor: String - items: [ Stream ] + items: [Stream] } - extend type Mutation { """ Creates a new stream. """ - streamCreate( stream: StreamCreateInput! ): String + streamCreate(stream: StreamCreateInput!): String @hasRole(role: "server:user") @hasScope(scope: "streams:write") """ Updates an existing stream. """ - streamUpdate( stream: StreamUpdateInput! ): Boolean! + streamUpdate(stream: StreamUpdateInput!): Boolean! @hasRole(role: "server:user") @hasScope(scope: "streams:write") """ Deletes an existing stream. """ - streamDelete( id: String! ): Boolean! + streamDelete(id: String!): Boolean! @hasRole(role: "server:user") @hasScope(scope: "streams:write") - streamsDelete( ids: [String!] ): Boolean! - @hasRole(role: "server:admin") + streamsDelete(ids: [String!]): Boolean! @hasRole(role: "server:admin") """ Grants permissions to a user on a given stream. """ - streamGrantPermission( permissionParams: StreamGrantPermissionInput! ): Boolean + streamGrantPermission(permissionParams: StreamGrantPermissionInput!): Boolean @hasRole(role: "server:user") @hasScope(scope: "streams:write") """ Revokes the permissions of a user on a given stream. """ - streamRevokePermission( permissionParams: StreamRevokePermissionInput! ): Boolean - @hasRole(role: "server:user") - @hasScope(scope: "streams:write") + streamRevokePermission( + permissionParams: StreamRevokePermissionInput! + ): Boolean @hasRole(role: "server:user") @hasScope(scope: "streams:write") } extend type Subscription { - # # User bound subscriptions that operate on the stream collection of an user # Example relevant view/usecase: updating the list of streams for a user. @@ -118,17 +120,16 @@ extend type Subscription { """ Subscribes to stream updated event. Use this in clients/components that pertain only to this stream. """ - streamUpdated( streamId: String ): JSONObject + streamUpdated(streamId: String): JSONObject @hasRole(role: "server:user") @hasScope(scope: "streams:read") """ Subscribes to stream deleted event. Use this in clients/components that pertain only to this stream. """ - streamDeleted( streamId: String ): JSONObject + streamDeleted(streamId: String): JSONObject @hasRole(role: "server:user") @hasScope(scope: "streams:read") - } input StreamCreateInput { @@ -145,12 +146,12 @@ input StreamUpdateInput { } input StreamGrantPermissionInput { - streamId: String!, - userId: String!, + streamId: String! + userId: String! role: String! } input StreamRevokePermissionInput { - streamId: String!, + streamId: String! userId: String! }