From 0412deeda3afceaad24aeeb0b982cb464edbf12b Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Mon, 23 Sep 2024 15:03:45 +0300 Subject: [PATCH] chore(server): comments IoC 1 - createComment --- .../modules/comments/domain/operations.ts | 42 +++ .../server/modules/comments/domain/types.ts | 11 + .../server/modules/comments/events/emitter.ts | 2 + .../comments/graph/resolvers/comments.ts | 43 ++- .../server/modules/comments/helpers/types.ts | 8 +- .../modules/comments/repositories/comments.ts | 142 +++++++--- .../comments/services/commentTextService.ts | 20 +- .../server/modules/comments/services/index.ts | 251 +++++++++--------- .../modules/comments/services/management.ts | 34 ++- .../modules/comments/tests/comments.spec.js | 39 ++- .../modules/core/services/streams/clone.ts | 18 +- 11 files changed, 398 insertions(+), 212 deletions(-) create mode 100644 packages/server/modules/comments/domain/operations.ts create mode 100644 packages/server/modules/comments/domain/types.ts diff --git a/packages/server/modules/comments/domain/operations.ts b/packages/server/modules/comments/domain/operations.ts new file mode 100644 index 000000000..e2174065f --- /dev/null +++ b/packages/server/modules/comments/domain/operations.ts @@ -0,0 +1,42 @@ +import { ResourceIdentifier } from '@/modules/comments/domain/types' +import { CommentLinkRecord, CommentRecord } from '@/modules/comments/helpers/types' +import { SmartTextEditorValueSchema } from '@/modules/core/services/richTextEditorService' +import { MarkNullableOptional } from '@/modules/shared/helpers/typeHelper' +import { Knex } from 'knex' + +export type CheckStreamResourceAccess = ( + res: ResourceIdentifier, + streamId: string +) => Promise + +export type InsertCommentPayload = MarkNullableOptional< + Omit & { + text: SmartTextEditorValueSchema + archived?: boolean + id?: string + } +> + +export type InsertComments = ( + comments: InsertCommentPayload[], + options?: Partial<{ trx: Knex.Transaction }> +) => Promise + +export type InsertCommentLinks = ( + commentLinks: CommentLinkRecord[], + options?: Partial<{ trx: Knex.Transaction }> +) => Promise + +export type DeleteComment = (params: { commentId: string }) => Promise + +export type MarkCommentViewed = (commentId: string, userId: string) => Promise + +export type CheckStreamResourcesAccess = (params: { + streamId: string + resources: ResourceIdentifier[] +}) => Promise + +export type ValidateInputAttachments = ( + streamId: string, + blobIds: string[] +) => Promise diff --git a/packages/server/modules/comments/domain/types.ts b/packages/server/modules/comments/domain/types.ts new file mode 100644 index 000000000..f173d6d7c --- /dev/null +++ b/packages/server/modules/comments/domain/types.ts @@ -0,0 +1,11 @@ +export type ResourceIdentifier = { + resourceId: string + resourceType: ResourceType +} + +export enum ResourceType { + Comment = 'comment', + Commit = 'commit', + Object = 'object', + Stream = 'stream' +} diff --git a/packages/server/modules/comments/events/emitter.ts b/packages/server/modules/comments/events/emitter.ts index 81fb1b2be..5eaf02637 100644 --- a/packages/server/modules/comments/events/emitter.ts +++ b/packages/server/modules/comments/events/emitter.ts @@ -18,3 +18,5 @@ const { emit, listen } = initializeModuleEventEmitter<{ }) export const CommentsEmitter = { emit, listen, events: CommentsEvents } +export type CommentsEventsEmit = typeof emit +export type CommentsEventsListen = typeof listen diff --git a/packages/server/modules/comments/graph/resolvers/comments.ts b/packages/server/modules/comments/graph/resolvers/comments.ts index 490e13c31..a5daae826 100644 --- a/packages/server/modules/comments/graph/resolvers/comments.ts +++ b/packages/server/modules/comments/graph/resolvers/comments.ts @@ -5,15 +5,24 @@ import { Roles } from '@/modules/core/helpers/mainConstants' import { getComments, getResourceCommentCount, - createComment, createCommentReply, - viewComment, archiveComment, editComment, - streamResourceCheck + streamResourceCheckFactory, + createCommentFactory } from '@/modules/comments/services/index' -import { getComment } from '@/modules/comments/repositories/comments' -import { ensureCommentSchema } from '@/modules/comments/services/commentTextService' +import { + checkStreamResourceAccessFactory, + deleteCommentFactory, + getComment, + insertCommentLinksFactory, + insertCommentsFactory, + markCommentViewedFactory +} from '@/modules/comments/repositories/comments' +import { + ensureCommentSchema, + validateInputAttachmentsFactory +} from '@/modules/comments/services/commentTextService' import { has } from 'lodash' import { documentToBasicString, @@ -44,7 +53,6 @@ import { import { authorizeProjectCommentsAccess, authorizeCommentAccess, - markViewed, createCommentThreadAndNotify, createCommentReplyAndNotify, editCommentAndNotify, @@ -64,6 +72,25 @@ import { } from '@/modules/core/graph/generated/graphql' import { GraphQLContext } from '@/modules/shared/helpers/typeHelper' import { CommentRecord } from '@/modules/comments/helpers/types' +import { db } from '@/db/knex' +import { CommentsEmitter } from '@/modules/comments/events/emitter' +import { getBlobsFactory } from '@/modules/blobstorage/repositories' + +const streamResourceCheck = streamResourceCheckFactory({ + checkStreamResourceAccess: checkStreamResourceAccessFactory({ db }) +}) +const markCommentViewed = markCommentViewedFactory({ db }) +const createComment = createCommentFactory({ + checkStreamResourcesAccess: streamResourceCheck, + validateInputAttachments: validateInputAttachmentsFactory({ + getBlobs: getBlobsFactory({ db }) + }), + insertComments: insertCommentsFactory({ db }), + insertCommentLinks: insertCommentLinksFactory({ db }), + deleteComment: deleteCommentFactory({ db }), + markCommentViewed, + commentsEventsEmit: CommentsEmitter.emit +}) const getStreamComment = async ( { streamId, commentId }: { streamId: string; commentId: string }, @@ -308,7 +335,7 @@ export = { authCtx: ctx, commentId: args.commentId }) - await markViewed(args.commentId, ctx.userId!) + await markCommentViewed(args.commentId, ctx.userId!) return true }, async create(_parent, args, ctx) { @@ -440,7 +467,7 @@ export = { projectId: args.streamId, authCtx: context }) - await viewComment({ userId: context.userId!, commentId: args.commentId }) + await markCommentViewed(args.commentId, context.userId!) return true }, diff --git a/packages/server/modules/comments/helpers/types.ts b/packages/server/modules/comments/helpers/types.ts index 8677fb78c..e3e007d50 100644 --- a/packages/server/modules/comments/helpers/types.ts +++ b/packages/server/modules/comments/helpers/types.ts @@ -1,8 +1,14 @@ +import { ResourceType } from '@/modules/comments/domain/types' import { DataStruct, LegacyData } from '@/modules/comments/services/data' import { SmartTextEditorValueSchema } from '@/modules/core/services/richTextEditorService' import { Nullable } from '@/modules/shared/helpers/typeHelper' -export type CommentLinkResourceType = 'stream' | 'commit' | 'object' | 'comment' +export type CommentLinkResourceType = + | 'stream' + | 'commit' + | 'object' + | 'comment' + | ResourceType export interface CommentRecord { id: string diff --git a/packages/server/modules/comments/repositories/comments.ts b/packages/server/modules/comments/repositories/comments.ts index 200c117d4..d331bf228 100644 --- a/packages/server/modules/comments/repositories/comments.ts +++ b/packages/server/modules/comments/repositories/comments.ts @@ -11,17 +11,15 @@ import { Comments, CommentViews, Commits, - knex + knex, + Objects, + StreamCommits } from '@/modules/core/dbSchema' import { ResourceIdentifier, ResourceType } from '@/modules/core/graph/generated/graphql' -import { - MarkNullableOptional, - MaybeNullOrUndefined, - Optional -} from '@/modules/shared/helpers/typeHelper' +import { MaybeNullOrUndefined, Optional } from '@/modules/shared/helpers/typeHelper' import { clamp, keyBy, reduce } from 'lodash' import crs from 'crypto-random-string' import { @@ -34,6 +32,23 @@ import { isNullOrUndefined, SpeckleViewer } from '@speckle/shared' import { SmartTextEditorValueSchema } from '@/modules/core/services/richTextEditorService' import { Merge } from 'type-fest' import { getBranchLatestCommits } from '@/modules/core/repositories/branches' +import { + CheckStreamResourceAccess, + DeleteComment, + InsertCommentLinks, + InsertCommentPayload, + InsertComments, + MarkCommentViewed +} from '@/modules/comments/domain/operations' +import { ObjectRecord, StreamCommitRecord } from '@/modules/core/helpers/types' + +const tables = { + streamCommits: (db: Knex) => db(StreamCommits.name), + objects: (db: Knex) => db(Objects.name), + comments: (db: Knex) => db(Comments.name), + commentLinks: (db: Knex) => db(CommentLinks.name), + commentViews: (db: Knex) => db(CommentViews.name) +} export const generateCommentId = () => crs({ length: 10 }) @@ -151,23 +166,27 @@ export async function getCommentLinks( return await q } -export async function insertComments( - comments: CommentRecord[], - options?: Partial<{ trx: Knex.Transaction }> -) { - const q = Comments.knex().insert(comments) - if (options?.trx) q.transacting(options.trx) - return await q -} +export const insertCommentsFactory = + (deps: { db: Knex }): InsertComments => + async (comments, options) => { + const q = tables.comments(deps.db).insert( + comments.map((c) => ({ + ...c, + id: c.id || generateCommentId() + })), + '*' + ) + if (options?.trx) q.transacting(options.trx) + return await q + } -export async function insertCommentLinks( - commentLinks: CommentLinkRecord[], - options?: Partial<{ trx: Knex.Transaction }> -) { - const q = CommentLinks.knex().insert(commentLinks) - if (options?.trx) q.transacting(options.trx) - return await q -} +export const insertCommentLinksFactory = + (deps: { db: Knex }): InsertCommentLinks => + async (commentLinks, options) => { + const q = tables.commentLinks(deps.db).insert(commentLinks, '*') + if (options?.trx) q.transacting(options.trx) + return await q + } export async function getStreamCommentCounts( streamIds: string[], @@ -676,26 +695,22 @@ export async function getCommentParents(replyIds: string[]) { return await q } -export async function markCommentViewed(commentId: string, userId: string) { - const query = CommentViews.knex() - .insert({ commentId, userId, viewedAt: knex.fn.now() }) - .onConflict(knex.raw('("commentId","userId")')) - .merge() - return await query -} - -export type InsertCommentPayload = MarkNullableOptional< - Omit & { - text: SmartTextEditorValueSchema - archived?: boolean +export const markCommentViewedFactory = + (deps: { db: Knex }): MarkCommentViewed => + async (commentId: string, userId: string) => { + const query = tables + .commentViews(deps.db) + .insert({ commentId, userId, viewedAt: knex.fn.now() }) + .onConflict(knex.raw('("commentId","userId")')) + .merge() + return !!(await query) } -> export async function insertComment( input: InsertCommentPayload, options?: Partial<{ trx: Knex.Transaction }> ): Promise { - const finalInput = { ...input, id: generateCommentId() } + const finalInput = { ...input, id: input.id || generateCommentId() } const q = Comments.knex().insert(finalInput, '*') if (options?.trx) q.transacting(options.trx) @@ -718,3 +733,58 @@ export async function updateComment( const [res] = await Comments.knex().where(Comments.col.id, id).update(input, '*') return res as CommentRecord } + +export const checkStreamResourceAccessFactory = + (deps: { db: Knex }): CheckStreamResourceAccess => + async (res, streamId) => { + // The switch of doom: if something throws, we're out + switch (res.resourceType) { + case 'stream': + // Stream validity is already checked, so we can just go ahead. + break + case 'commit': { + const linkage = await tables + .streamCommits(deps.db) + .select() + .where({ commitId: res.resourceId, streamId }) + .first() + if (!linkage) throw new Error('Commit not found') + if (linkage.streamId !== streamId) + throw new Error( + 'Stop hacking - that commit id is not part of the specified stream.' + ) + break + } + case 'object': { + const obj = await tables + .objects(deps.db) + .select() + .where({ id: res.resourceId, streamId }) + .first() + if (!obj) throw new Error('Object not found') + break + } + case 'comment': { + const comment = await tables + .comments(deps.db) + .where({ id: res.resourceId }) + .first() + if (!comment) throw new Error('Comment not found') + if (comment.streamId !== streamId) + throw new Error( + 'Stop hacking - that comment is not part of the specified stream.' + ) + break + } + default: + throw Error( + `resource type ${res.resourceType} is not supported as a comment target` + ) + } + } + +export const deleteCommentFactory = + (deps: { db: Knex }): DeleteComment => + async ({ commentId }) => { + return !!(await tables.comments(deps.db).where(Comments.col.id, commentId).del()) + } diff --git a/packages/server/modules/comments/services/commentTextService.ts b/packages/server/modules/comments/services/commentTextService.ts index dfa4ed585..d1683adc3 100644 --- a/packages/server/modules/comments/services/commentTextService.ts +++ b/packages/server/modules/comments/services/commentTextService.ts @@ -10,21 +10,23 @@ import { import { isString, uniq } from 'lodash' import { InvalidAttachmentsError } from '@/modules/comments/errors' import { JSONContent } from '@tiptap/core' -import { getBlobsFactory } from '@/modules/blobstorage/repositories' -import { db } from '@/db/knex' +import { ValidateInputAttachments } from '@/modules/comments/domain/operations' +import { GetBlobs } from '@/modules/blobstorage/domain/operations' const COMMENT_SCHEMA_VERSION = '1.0.0' const COMMENT_SCHEMA_TYPE = 'stream_comment' -export async function validateInputAttachments(streamId: string, blobIds: string[]) { - blobIds = uniq(blobIds || []) - if (!blobIds.length) return +export const validateInputAttachmentsFactory = + (deps: { getBlobs: GetBlobs }): ValidateInputAttachments => + async (streamId: string, blobIds: string[]) => { + blobIds = uniq(blobIds || []) + if (!blobIds.length) return - const blobs = await getBlobsFactory({ db })({ blobIds, streamId }) - if (!blobs || blobs.length !== blobIds.length) { - throw new InvalidAttachmentsError('Attempting to attach invalid blobs to comment') + const blobs = await deps.getBlobs({ blobIds, streamId }) + if (!blobs || blobs.length !== blobIds.length) { + throw new InvalidAttachmentsError('Attempting to attach invalid blobs to comment') + } } -} /** * Build comment.text value from a ProseMirror doc diff --git a/packages/server/modules/comments/services/index.ts b/packages/server/modules/comments/services/index.ts index 835d7e8cf..fb14bfdf4 100644 --- a/packages/server/modules/comments/services/index.ts +++ b/packages/server/modules/comments/services/index.ts @@ -1,18 +1,22 @@ import crs from 'crypto-random-string' -import knex from '@/db/knex' +import knex, { db } from '@/db/knex' import { ForbiddenError } from '@/modules/shared/errors' import { buildCommentTextFromInput, - validateInputAttachments + validateInputAttachmentsFactory } from '@/modules/comments/services/commentTextService' -import { CommentsEmitter, CommentsEvents } from '@/modules/comments/events/emitter' import { + CommentsEmitter, + CommentsEvents, + CommentsEventsEmit +} from '@/modules/comments/events/emitter' +import { + checkStreamResourceAccessFactory, getComment as repoGetComment, - getStreamCommentCount as repoGetStreamCommentCount, - markCommentViewed + getStreamCommentCount as repoGetStreamCommentCount } from '@/modules/comments/repositories/comments' import { clamp } from 'lodash' -import { Roles } from '@speckle/shared' +import { isNonNullable, Roles } from '@speckle/shared' import { ResourceIdentifier, CommentCreateInput, @@ -21,130 +25,116 @@ import { } from '@/modules/core/graph/generated/graphql' import { CommentLinkRecord, CommentRecord } from '@/modules/comments/helpers/types' import { SmartTextEditorValueSchema } from '@/modules/core/services/richTextEditorService' +import { + CheckStreamResourceAccess, + CheckStreamResourcesAccess, + DeleteComment, + InsertCommentLinks, + InsertComments, + MarkCommentViewed, + ValidateInputAttachments +} from '@/modules/comments/domain/operations' +import { getBlobsFactory } from '@/modules/blobstorage/repositories' +import { ResourceType } from '@/modules/comments/domain/types' const Comments = () => knex('comments') const CommentLinks = () => knex('comment_links') -const resourceCheck = async (res: ResourceIdentifier, streamId: string) => { - // The switch of doom: if something throws, we're out - switch (res.resourceType) { - case 'stream': - // Stream validity is already checked, so we can just go ahead. - break - case 'commit': { - const linkage = await knex('stream_commits') - .select() - .where({ commitId: res.resourceId, streamId }) - .first() - if (!linkage) throw new Error('Commit not found') - if (linkage.streamId !== streamId) - throw new Error( - 'Stop hacking - that commit id is not part of the specified stream.' - ) - break - } - case 'object': { - const obj = await knex('objects') - .select() - .where({ id: res.resourceId, streamId }) - .first() - if (!obj) throw new Error('Object not found') - break - } - case 'comment': { - const comment = await Comments().where({ id: res.resourceId }).first() - if (!comment) throw new Error('Comment not found') - if (comment.streamId !== streamId) - throw new Error( - 'Stop hacking - that comment is not part of the specified stream.' - ) - break - } - default: - throw Error( - `resource type ${res.resourceType} is not supported as a comment target` - ) +export const streamResourceCheckFactory = + (deps: { + checkStreamResourceAccess: CheckStreamResourceAccess + }): CheckStreamResourcesAccess => + async ({ + streamId, + resources + }: { + streamId: string + resources: ResourceIdentifier[] + }) => { + // this itches - a for loop with queries... but okay let's hit the road now + await Promise.all( + resources.map((res) => deps.checkStreamResourceAccess(res, streamId)) + ) } -} - -export async function streamResourceCheck({ - streamId, - resources -}: { - streamId: string - resources: ResourceIdentifier[] -}) { - // this itches - a for loop with queries... but okay let's hit the road now - await Promise.all(resources.map((res) => resourceCheck(res, streamId))) -} /** * @deprecated Use 'createCommentThreadAndNotify()' instead */ -export async function createComment({ - userId, - input -}: { - userId: string - input: CommentCreateInput -}) { - if (input.resources.length < 1) - throw Error('Must specify at least one resource as the comment target') +export const createCommentFactory = + (deps: { + checkStreamResourcesAccess: CheckStreamResourcesAccess + validateInputAttachments: ValidateInputAttachments + insertComments: InsertComments + insertCommentLinks: InsertCommentLinks + deleteComment: DeleteComment + markCommentViewed: MarkCommentViewed + commentsEventsEmit: CommentsEventsEmit + }) => + async ({ userId, input }: { userId: string; input: CommentCreateInput }) => { + if (input.resources.length < 1) + throw Error('Must specify at least one resource as the comment target') - const commentResource = input.resources.find((r) => r?.resourceType === 'comment') - if (commentResource) throw new Error('Please use the comment reply mutation.') + const commentResource = input.resources.find((r) => r?.resourceType === 'comment') + if (commentResource) throw new Error('Please use the comment reply mutation.') - // Stream checks - const streamResources = input.resources.filter((r) => r?.resourceType === 'stream') - if (streamResources.length > 1) - throw Error('Commenting on multiple streams is not supported') + // Stream checks + 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 [stream] = streamResources + if (stream && stream.resourceId !== input.streamId) + throw Error("Input streamId doesn't match the stream resource.resourceId") - const comment: Partial = { - streamId: input.streamId, - text: input.text as SmartTextEditorValueSchema, - data: input.data, - screenshot: input.screenshot ?? null - } - - comment.id = crs({ length: 10 }) - comment.authorId = userId - - await validateInputAttachments(input.streamId, input.blobIds) - comment.text = buildCommentTextFromInput({ - doc: input.text, - blobIds: input.blobIds - }) as unknown as string - - const [newComment] = await Comments().insert(comment, '*') - try { - await module.exports.streamResourceCheck({ + const comment = { streamId: input.streamId, - resources: input.resources - }) - for (const res of input.resources) { - if (!res) continue - await CommentLinks().insert({ - commentId: comment.id, - resourceId: res.resourceId, - resourceType: res.resourceType - }) + text: input.text as SmartTextEditorValueSchema, + data: input.data, + screenshot: input.screenshot ?? null } - } catch (e) { - await Comments().where({ id: comment.id }).delete() // roll back - throw e // pass on to resolver + + await deps.validateInputAttachments(input.streamId, input.blobIds) + comment.text = buildCommentTextFromInput({ + doc: input.text, + blobIds: input.blobIds + }) + + const id = crs({ length: 10 }) + const [newComment] = await deps.insertComments([ + { + ...comment, + id, + authorId: userId + } + ]) + try { + await deps.checkStreamResourcesAccess({ + streamId: input.streamId, + resources: input.resources.filter(isNonNullable) + }) + for (const res of input.resources) { + if (!res) continue + await deps.insertCommentLinks([ + { + commentId: id, + resourceId: res.resourceId, + resourceType: res.resourceType + } + ]) + } + } catch (e) { + await deps.deleteComment({ commentId: id }) // roll back + throw e // pass on to resolver + } + + await deps.markCommentViewed(id, userId) // so we don't self mark a comment as unread the moment it's created + + await deps.commentsEventsEmit(CommentsEvents.Created, { + comment: newComment + }) + + return newComment } - await module.exports.viewComment({ userId, commentId: comment.id }) // so we don't self mark a comment as unread the moment it's created - - await CommentsEmitter.emit(CommentsEvents.Created, { - comment: newComment - }) - - return newComment -} /** * @deprecated Use 'createCommentReplyAndNotify()' instead @@ -164,7 +154,10 @@ export async function createCommentReply({ data: CommentRecord['data'] blobIds: string[] }) { - await validateInputAttachments(streamId, blobIds) + await validateInputAttachmentsFactory({ getBlobs: getBlobsFactory({ db }) })( + streamId, + blobIds + ) const comment = { id: crs({ length: 10 }), authorId, @@ -176,15 +169,18 @@ export async function createCommentReply({ const [newComment] = await Comments().insert(comment, '*') try { - const commentLink: Omit = { + const commentLink: CommentLinkRecord & { resourceType: ResourceType } = { resourceId: parentCommentId, - resourceType: 'comment' + resourceType: ResourceType.Comment, + commentId: newComment.id } - await module.exports.streamResourceCheck({ + await streamResourceCheckFactory({ + checkStreamResourceAccess: checkStreamResourceAccessFactory({ db }) + })({ streamId, resources: [commentLink] }) - await CommentLinks().insert({ commentId: comment.id, ...commentLink }) + await CommentLinks().insert({ ...commentLink }) } catch (e) { await Comments().where({ id: comment.id }).delete() // roll back throw e // pass on to resolver @@ -216,7 +212,10 @@ export async function editComment({ if (matchUser && editedComment.authorId !== userId) throw new ForbiddenError("You cannot edit someone else's comments") - await validateInputAttachments(input.streamId, input.blobIds) + await validateInputAttachmentsFactory({ getBlobs: getBlobsFactory({ db }) })( + input.streamId, + input.blobIds + ) const newText = buildCommentTextFromInput({ doc: input.text, blobIds: input.blobIds @@ -233,18 +232,6 @@ export async function editComment({ return updatedComment } -/** - * @deprecated Use 'markCommentViewed()' - */ -export async function viewComment({ - userId, - commentId -}: { - userId: string - commentId: string -}) { - await markCommentViewed(commentId, userId) -} /** * @deprecated Use repository method */ diff --git a/packages/server/modules/comments/services/management.ts b/packages/server/modules/comments/services/management.ts index 1748b0be5..cd1bed787 100644 --- a/packages/server/modules/comments/services/management.ts +++ b/packages/server/modules/comments/services/management.ts @@ -4,12 +4,11 @@ import { ForbiddenError } from '@/modules/shared/errors' import { getStream } from '@/modules/core/repositories/streams' import { StreamInvalidAccessError } from '@/modules/core/errors/stream' import { - InsertCommentPayload, getComment, - markCommentViewed, insertComment, - insertCommentLinks, + insertCommentLinksFactory, markCommentUpdated, + markCommentViewedFactory, updateComment } from '@/modules/comments/repositories/comments' import { @@ -21,7 +20,7 @@ import { getViewerResourceItemsUngrouped } from '@/modules/core/services/commit/ import { CommentCreateError, CommentUpdateError } from '@/modules/comments/errors' import { buildCommentTextFromInput, - validateInputAttachments + validateInputAttachmentsFactory } from '@/modules/comments/services/commentTextService' import { knex } from '@/modules/core/dbSchema' import { @@ -40,6 +39,9 @@ import { inputToDataStruct } from '@/modules/comments/services/data' import { adminOverrideEnabled } from '@/modules/shared/helpers/envHelper' +import { getBlobsFactory } from '@/modules/blobstorage/repositories' +import { db } from '@/db/knex' +import { InsertCommentPayload } from '@/modules/comments/domain/operations' export async function authorizeProjectCommentsAccess(params: { projectId: string @@ -88,17 +90,16 @@ export async function authorizeCommentAccess(params: { }) } -export async function markViewed(commentId: string, userId: string) { - await markCommentViewed(commentId, userId) -} - export async function createCommentThreadAndNotify( input: CreateCommentInput, userId: string ) { const [resources] = await Promise.all([ getViewerResourceItemsUngrouped({ ...input, loadedVersionsOnly: true }), - validateInputAttachments(input.projectId, input.content.blobIds || []) + validateInputAttachmentsFactory({ getBlobs: getBlobsFactory({ db }) })( + input.projectId, + input.content.blobIds || [] + ) ]) if (!resources.length) { throw new CommentCreateError( @@ -141,7 +142,7 @@ export async function createCommentThreadAndNotify( resourceType } }) - await insertCommentLinks(links, { trx }) + await insertCommentLinksFactory({ db })(links, { trx }) return comment }) @@ -151,7 +152,7 @@ export async function createCommentThreadAndNotify( // Mark as viewed and emit events await Promise.all([ - markViewed(comment.id, userId), + markCommentViewedFactory({ db })(comment.id, userId), CommentsEmitter.emit(CommentsEvents.Created, { comment }), @@ -177,7 +178,10 @@ export async function createCommentReplyAndNotify( if (!thread) { throw new CommentCreateError('Reply creation failed due to nonexistant thread') } - await validateInputAttachments(thread.streamId, input.content.blobIds || []) + await validateInputAttachmentsFactory({ getBlobs: getBlobsFactory({ db }) })( + thread.streamId, + input.content.blobIds || [] + ) const commentPayload: InsertCommentPayload = { streamId: thread.streamId, @@ -197,7 +201,7 @@ export async function createCommentReplyAndNotify( const links: CommentLinkRecord[] = [ { resourceType: 'comment', resourceId: thread.id, commentId: reply.id } ] - await insertCommentLinks(links, { trx }) + await insertCommentLinksFactory({ db })(links, { trx }) return reply }) @@ -231,7 +235,9 @@ export async function editCommentAndNotify(input: EditCommentInput, userId: stri throw new CommentUpdateError("You cannot edit someone else's comments") } - await validateInputAttachments(comment.streamId, input.content.blobIds || []) + await validateInputAttachmentsFactory({ + getBlobs: getBlobsFactory({ db }) + })(comment.streamId, input.content.blobIds || []) const updatedComment = await updateComment(comment.id, { text: buildCommentTextFromInput({ doc: input.content.doc, diff --git a/packages/server/modules/comments/tests/comments.spec.js b/packages/server/modules/comments/tests/comments.spec.js index f719a6599..8c2ae262a 100644 --- a/packages/server/modules/comments/tests/comments.spec.js +++ b/packages/server/modules/comments/tests/comments.spec.js @@ -16,22 +16,23 @@ const { createCommitByBranchName } = require('@/modules/core/services/commits') const { createObject } = require('@/modules/core/services/objects') const { - createComment, getComments, getComment, editComment, - viewComment, createCommentReply, archiveComment, getResourceCommentCount, - getStreamCommentCount -} = require('../services/index') + getStreamCommentCount, + streamResourceCheckFactory, + createCommentFactory +} = require('@/modules/comments/services/index') const { convertBasicStringToDocument } = require('@/modules/core/services/richTextEditorService') const { ensureCommentSchema, - buildCommentTextFromInput + buildCommentTextFromInput, + validateInputAttachmentsFactory } = require('@/modules/comments/services/commentTextService') const { range } = require('lodash') const { buildApolloServer } = require('@/app') @@ -47,6 +48,32 @@ const { const { NotificationType } = require('@/modules/notifications/helpers/types') const { EmailSendingServiceMock } = require('@/test/mocks/global') const { createAuthedTestContext } = require('@/test/graphqlHelper') +const { + checkStreamResourceAccessFactory, + markCommentViewedFactory, + insertCommentsFactory, + insertCommentLinksFactory, + deleteCommentFactory +} = require('@/modules/comments/repositories/comments') +const { db } = require('@/db/knex') +const { getBlobsFactory } = require('@/modules/blobstorage/repositories') +const { CommentsEmitter } = require('@/modules/comments/events/emitter') + +const streamResourceCheck = streamResourceCheckFactory({ + checkStreamResourceAccess: checkStreamResourceAccessFactory({ db }) +}) +const markCommentViewed = markCommentViewedFactory({ db }) +const createComment = createCommentFactory({ + checkStreamResourcesAccess: streamResourceCheck, + validateInputAttachments: validateInputAttachmentsFactory({ + getBlobs: getBlobsFactory({ db }) + }), + insertComments: insertCommentsFactory({ db }), + insertCommentLinks: insertCommentLinksFactory({ db }), + deleteComment: deleteCommentFactory({ db }), + markCommentViewed, + commentsEventsEmit: CommentsEmitter.emit +}) function buildCommentInputFromString(textString) { return convertBasicStringToDocument(textString) @@ -410,7 +437,7 @@ describe('Comments @comments', () => { const commentOtherUser = await getComment({ id, userId: otherUser.id }) expect(commentOtherUser.viewedAt).to.be.null - await viewComment({ userId: user.id, commentId: id }) + await markCommentViewed(id, user.id) const viewedCommentOtherUser = await getComment({ id, userId: otherUser.id }) expect(viewedCommentOtherUser).to.haveOwnProperty('viewedAt') diff --git a/packages/server/modules/core/services/streams/clone.ts b/packages/server/modules/core/services/streams/clone.ts index 317bed177..ae812402e 100644 --- a/packages/server/modules/core/services/streams/clone.ts +++ b/packages/server/modules/core/services/streams/clone.ts @@ -32,13 +32,15 @@ import { generateCommentId, getBatchedStreamComments, getCommentLinks, - insertCommentLinks, - insertComments + insertCommentLinksFactory, + insertCommentsFactory } from '@/modules/comments/repositories/comments' import dayjs from 'dayjs' import { addStreamClonedActivity } from '@/modules/activitystream/services/streamActivity' -import knex from '@/db/knex' +import knex, { db } from '@/db/knex' import { Knex } from 'knex' +import { InsertCommentPayload } from '@/modules/comments/domain/operations' +import { SmartTextEditorValueSchema } from '@/modules/core/services/richTextEditorService' type CloneStreamInitialState = { user: UserWithOptionalRole @@ -297,7 +299,7 @@ async function cloneComments( withParentCommentOnly: !threads, trx: state.trx })) { - commentsBatch.forEach((c) => { + const finalBatch = commentsBatch.map((c): InsertCommentPayload => { const oldId = c.id const newDate = getNewDate() @@ -322,9 +324,13 @@ async function cloneComments( } commentIdMap.set(oldId, c.id) + return { + ...c, + text: c.text as SmartTextEditorValueSchema + } }) - await insertComments(commentsBatch, { trx: state.trx }) + await insertCommentsFactory({ db })(finalBatch, { trx: state.trx }) } } @@ -383,7 +389,7 @@ async function cloneCommentLinks( } }) - await insertCommentLinks(commentLinks, { trx }) + await insertCommentLinksFactory({ db })(commentLinks, { trx }) } }