chore(server): comments IoC 1 - createComment

This commit is contained in:
Kristaps Fabians Geikins
2024-09-23 15:03:45 +03:00
parent 6d4d0c4631
commit 0412deeda3
11 changed files with 398 additions and 212 deletions
@@ -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<void>
export type InsertCommentPayload = MarkNullableOptional<
Omit<CommentRecord, 'id' | 'createdAt' | 'updatedAt' | 'text' | 'archived'> & {
text: SmartTextEditorValueSchema
archived?: boolean
id?: string
}
>
export type InsertComments = (
comments: InsertCommentPayload[],
options?: Partial<{ trx: Knex.Transaction }>
) => Promise<CommentRecord[]>
export type InsertCommentLinks = (
commentLinks: CommentLinkRecord[],
options?: Partial<{ trx: Knex.Transaction }>
) => Promise<CommentLinkRecord[]>
export type DeleteComment = (params: { commentId: string }) => Promise<boolean>
export type MarkCommentViewed = (commentId: string, userId: string) => Promise<boolean>
export type CheckStreamResourcesAccess = (params: {
streamId: string
resources: ResourceIdentifier[]
}) => Promise<void>
export type ValidateInputAttachments = (
streamId: string,
blobIds: string[]
) => Promise<void>
@@ -0,0 +1,11 @@
export type ResourceIdentifier = {
resourceId: string
resourceType: ResourceType
}
export enum ResourceType {
Comment = 'comment',
Commit = 'commit',
Object = 'object',
Stream = 'stream'
}
@@ -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
@@ -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
},
@@ -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
@@ -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<StreamCommitRecord>(StreamCommits.name),
objects: (db: Knex) => db<ObjectRecord>(Objects.name),
comments: (db: Knex) => db<CommentRecord>(Comments.name),
commentLinks: (db: Knex) => db<CommentLinkRecord>(CommentLinks.name),
commentViews: (db: Knex) => db<CommentViewRecord>(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<CommentRecord, 'id' | 'createdAt' | 'updatedAt' | 'text' | 'archived'> & {
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<CommentRecord> {
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())
}
@@ -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
+119 -132
View File
@@ -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<CommentRecord>('comments')
const CommentLinks = () => knex<CommentLinkRecord>('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<CommentRecord> = {
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<CommentLinkRecord, 'commentId'> = {
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
*/
@@ -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,
@@ -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')
@@ -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<UserRecord>
@@ -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 })
}
}