chore(server): comments IoC 1 - createComment
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user