chore(comments): update comments module to typescript (#2513)
* chore(comments): files to .ts * chore(comment): services to .ts * chore(comments): repo to .ts * chore(comments): resolvers to .ts * chore(comments): init to .ts * fix(comments): FIXME for non-null assertions * chore(comments): drop some comments
This commit is contained in:
+100
-78
@@ -1,10 +1,9 @@
|
||||
const { pubsub } = require('@/modules/shared/utils/subscriptions')
|
||||
const { ForbiddenError: ApolloForbiddenError } = require('apollo-server-express')
|
||||
const { ForbiddenError } = require('@/modules/shared/errors')
|
||||
const { getStream } = require('@/modules/core/services/streams')
|
||||
const { Roles } = require('@/modules/core/helpers/mainConstants')
|
||||
|
||||
const {
|
||||
import { pubsub } from '@/modules/shared/utils/subscriptions'
|
||||
import { ForbiddenError as ApolloForbiddenError } from 'apollo-server-express'
|
||||
import { ForbiddenError } from '@/modules/shared/errors'
|
||||
import { getStream } from '@/modules/core/services/streams'
|
||||
import { Roles } from '@/modules/core/helpers/mainConstants'
|
||||
import {
|
||||
getComments,
|
||||
getResourceCommentCount,
|
||||
createComment,
|
||||
@@ -13,39 +12,37 @@ const {
|
||||
archiveComment,
|
||||
editComment,
|
||||
streamResourceCheck
|
||||
} = require('@/modules/comments/services/index')
|
||||
const { getComment } = require('@/modules/comments/repositories/comments')
|
||||
const {
|
||||
ensureCommentSchema
|
||||
} = require('@/modules/comments/services/commentTextService')
|
||||
const { withFilter } = require('graphql-subscriptions')
|
||||
const { has } = require('lodash')
|
||||
const {
|
||||
documentToBasicString
|
||||
} = require('@/modules/core/services/richTextEditorService')
|
||||
const {
|
||||
} from '@/modules/comments/services/index'
|
||||
import { getComment } from '@/modules/comments/repositories/comments'
|
||||
import { ensureCommentSchema } from '@/modules/comments/services/commentTextService'
|
||||
import { has } from 'lodash'
|
||||
import {
|
||||
documentToBasicString,
|
||||
SmartTextEditorValueSchema
|
||||
} from '@/modules/core/services/richTextEditorService'
|
||||
import {
|
||||
getPaginatedCommitComments,
|
||||
getPaginatedBranchComments,
|
||||
getPaginatedProjectComments
|
||||
} = require('@/modules/comments/services/retrieval')
|
||||
const {
|
||||
} from '@/modules/comments/services/retrieval'
|
||||
import {
|
||||
publish,
|
||||
ViewerSubscriptions,
|
||||
CommentSubscriptions,
|
||||
filteredSubscribe,
|
||||
ProjectSubscriptions
|
||||
} = require('@/modules/shared/utils/subscriptions')
|
||||
const {
|
||||
} from '@/modules/shared/utils/subscriptions'
|
||||
import {
|
||||
addCommentCreatedActivity,
|
||||
addCommentArchivedActivity,
|
||||
addReplyAddedActivity
|
||||
} = require('@/modules/activitystream/services/commentActivity')
|
||||
const {
|
||||
} from '@/modules/activitystream/services/commentActivity'
|
||||
import {
|
||||
getViewerResourceItemsUngrouped,
|
||||
getViewerResourcesForComment,
|
||||
doViewerResourcesFit
|
||||
} = require('@/modules/core/services/commit/viewerResources')
|
||||
const {
|
||||
} from '@/modules/core/services/commit/viewerResources'
|
||||
import {
|
||||
authorizeProjectCommentsAccess,
|
||||
authorizeCommentAccess,
|
||||
markViewed,
|
||||
@@ -53,30 +50,40 @@ const {
|
||||
createCommentReplyAndNotify,
|
||||
editCommentAndNotify,
|
||||
archiveCommentAndNotify
|
||||
} = require('@/modules/comments/services/management')
|
||||
const {
|
||||
} from '@/modules/comments/services/management'
|
||||
import {
|
||||
isLegacyData,
|
||||
isDataStruct,
|
||||
formatSerializedViewerState,
|
||||
convertStateToLegacyData,
|
||||
convertLegacyDataToState
|
||||
} = require('@/modules/comments/services/data')
|
||||
} from '@/modules/comments/services/data'
|
||||
import {
|
||||
Resolvers,
|
||||
ResourceIdentifier,
|
||||
ResourceType
|
||||
} from '@/modules/core/graph/generated/graphql'
|
||||
import { GraphQLContext } from '@/modules/shared/helpers/typeHelper'
|
||||
import { CommentRecord } from '@/modules/comments/helpers/types'
|
||||
|
||||
const getStreamComment = async ({ streamId, commentId }, ctx) => {
|
||||
const getStreamComment = async (
|
||||
{ streamId, commentId }: { streamId: string; commentId: string },
|
||||
ctx: GraphQLContext
|
||||
) => {
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId: streamId,
|
||||
authCtx: ctx
|
||||
})
|
||||
|
||||
const comment = await getComment({ id: commentId, userId: ctx.userId })
|
||||
if (comment.streamId !== streamId)
|
||||
if (comment?.streamId !== streamId)
|
||||
throw new ApolloForbiddenError('You do not have access to this comment.')
|
||||
|
||||
return comment
|
||||
}
|
||||
|
||||
/** @type {import('@/modules/core/graph/generated/graphql').Resolvers} */
|
||||
module.exports = {
|
||||
// FIXME: Non-null assertions considered unsafe but are parity with previous .js logic
|
||||
export = {
|
||||
Query: {
|
||||
async comment(_parent, args, context) {
|
||||
return await getStreamComment(
|
||||
@@ -85,12 +92,18 @@ module.exports = {
|
||||
)
|
||||
},
|
||||
|
||||
async comments(parent, args, context) {
|
||||
async comments(_parent, args, context) {
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId: args.streamId,
|
||||
authCtx: context
|
||||
})
|
||||
return { ...(await getComments({ ...args, userId: context.userId })) }
|
||||
return {
|
||||
...(await getComments({
|
||||
...args,
|
||||
resources: args.resources?.filter((res): res is ResourceIdentifier => !!res),
|
||||
userId: context.userId!
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
Comment: {
|
||||
@@ -104,7 +117,7 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
|
||||
const resources = [{ resourceId: parent.id, resourceType: 'comment' }]
|
||||
const resources = [{ resourceId: parent.id, resourceType: ResourceType.Comment }]
|
||||
return await getComments({
|
||||
resources,
|
||||
replies: true,
|
||||
@@ -117,11 +130,13 @@ module.exports = {
|
||||
*/
|
||||
text(parent) {
|
||||
const commentText = parent?.text || ''
|
||||
return ensureCommentSchema(commentText)
|
||||
return ensureCommentSchema(commentText as SmartTextEditorValueSchema)
|
||||
},
|
||||
|
||||
rawText(parent) {
|
||||
const { doc } = ensureCommentSchema(parent.text || '')
|
||||
const { doc } = ensureCommentSchema(
|
||||
(parent.text as SmartTextEditorValueSchema) || ''
|
||||
)
|
||||
return documentToBasicString(doc)
|
||||
},
|
||||
async hasParent(parent) {
|
||||
@@ -134,11 +149,13 @@ module.exports = {
|
||||
* Resolve resources, if they weren't already preloaded
|
||||
*/
|
||||
async resources(parent, _args, ctx) {
|
||||
if (has(parent, 'resources')) return parent.resources
|
||||
if (has(parent, 'resources'))
|
||||
return (parent as CommentRecord & { resources: ResourceIdentifier[] }).resources
|
||||
return await ctx.loaders.comments.getResources.load(parent.id)
|
||||
},
|
||||
async viewedAt(parent, _args, ctx) {
|
||||
if (has(parent, 'viewedAt')) return parent.viewedAt
|
||||
if (has(parent, 'viewedAt'))
|
||||
return (parent as CommentRecord & { viewedAt: Date }).viewedAt
|
||||
return await ctx.loaders.comments.getViewedAt.load(parent.id)
|
||||
},
|
||||
async author(parent, _args, ctx) {
|
||||
@@ -230,14 +247,14 @@ module.exports = {
|
||||
async commentThreads(parent, args, context) {
|
||||
const stream = await context.loaders.commits.getCommitStream.load(parent.id)
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId: stream.id,
|
||||
projectId: stream!.id,
|
||||
authCtx: context
|
||||
})
|
||||
return await getPaginatedCommitComments({
|
||||
...args,
|
||||
commitId: parent.id,
|
||||
filter: {
|
||||
...(args.filter || {}),
|
||||
includeArchived: false,
|
||||
threadsOnly: true
|
||||
}
|
||||
})
|
||||
@@ -253,7 +270,7 @@ module.exports = {
|
||||
...args,
|
||||
branchId: parent.id,
|
||||
filter: {
|
||||
...(args.filter || {}),
|
||||
includeArchived: false,
|
||||
threadsOnly: true
|
||||
}
|
||||
})
|
||||
@@ -262,14 +279,13 @@ module.exports = {
|
||||
ViewerUserActivityMessage: {
|
||||
async user(parent, args, context) {
|
||||
const { userId } = parent
|
||||
return context.loaders.users.getUser.load(userId)
|
||||
return context.loaders.users.getUser.load(userId!)
|
||||
}
|
||||
},
|
||||
Stream: {
|
||||
async commentCount(parent, _args, context) {
|
||||
if (context.role === Roles.Server.ArchivedUser)
|
||||
throw new ApolloForbiddenError('You are not authorized.')
|
||||
|
||||
return await context.loaders.streams.getCommentThreadCount.load(parent.id)
|
||||
}
|
||||
},
|
||||
@@ -293,7 +309,7 @@ module.exports = {
|
||||
authCtx: ctx,
|
||||
commentId: args.commentId
|
||||
})
|
||||
await markViewed(args.commentId, ctx.userId)
|
||||
await markViewed(args.commentId, ctx.userId!)
|
||||
return true
|
||||
},
|
||||
async create(_parent, args, ctx) {
|
||||
@@ -302,7 +318,7 @@ module.exports = {
|
||||
authCtx: ctx,
|
||||
requireProjectRole: true
|
||||
})
|
||||
return await createCommentThreadAndNotify(args.input, ctx.userId)
|
||||
return await createCommentThreadAndNotify(args.input, ctx.userId!)
|
||||
},
|
||||
async reply(_parent, args, ctx) {
|
||||
await authorizeCommentAccess({
|
||||
@@ -310,7 +326,7 @@ module.exports = {
|
||||
authCtx: ctx,
|
||||
requireProjectRole: true
|
||||
})
|
||||
return await createCommentReplyAndNotify(args.input, ctx.userId)
|
||||
return await createCommentReplyAndNotify(args.input, ctx.userId!)
|
||||
},
|
||||
async edit(_parent, args, ctx) {
|
||||
await authorizeCommentAccess({
|
||||
@@ -318,7 +334,7 @@ module.exports = {
|
||||
commentId: args.input.commentId,
|
||||
requireProjectRole: true
|
||||
})
|
||||
return await editCommentAndNotify(args.input, ctx.userId)
|
||||
return await editCommentAndNotify(args.input, ctx.userId!)
|
||||
},
|
||||
async archive(_parent, args, ctx) {
|
||||
await authorizeCommentAccess({
|
||||
@@ -326,7 +342,7 @@ module.exports = {
|
||||
commentId: args.commentId,
|
||||
requireProjectRole: true
|
||||
})
|
||||
await archiveCommentAndNotify(args.commentId, ctx.userId, args.archived)
|
||||
await archiveCommentAndNotify(args.commentId, ctx.userId!, args.archived)
|
||||
return true
|
||||
}
|
||||
},
|
||||
@@ -342,7 +358,7 @@ module.exports = {
|
||||
projectId: args.projectId,
|
||||
resourceItems: await getViewerResourceItemsUngrouped(args),
|
||||
viewerUserActivityBroadcasted: args.message,
|
||||
userId: context.userId
|
||||
userId: context.userId!
|
||||
})
|
||||
return true
|
||||
},
|
||||
@@ -379,7 +395,7 @@ module.exports = {
|
||||
userId: context.userId
|
||||
})
|
||||
|
||||
if (!stream.allowPublicComments && !stream.role)
|
||||
if (!stream?.allowPublicComments && !stream?.role)
|
||||
throw new ApolloForbiddenError('You are not authorized.')
|
||||
|
||||
await pubsub.publish(CommentSubscriptions.CommentThreadActivity, {
|
||||
@@ -399,7 +415,7 @@ module.exports = {
|
||||
userId: context.userId
|
||||
})
|
||||
|
||||
if (!stream.allowPublicComments && !stream.role)
|
||||
if (!stream?.allowPublicComments && !stream?.role)
|
||||
throw new ApolloForbiddenError('You are not authorized.')
|
||||
|
||||
const comment = await createComment({
|
||||
@@ -426,7 +442,7 @@ module.exports = {
|
||||
})
|
||||
const matchUser = !stream.role
|
||||
try {
|
||||
await editComment({ userId: context.userId, input: args.input, matchUser })
|
||||
await editComment({ userId: context.userId!, input: args.input, matchUser })
|
||||
return true
|
||||
} catch (err) {
|
||||
if (err instanceof ForbiddenError) throw new ApolloForbiddenError(err.message)
|
||||
@@ -440,7 +456,7 @@ module.exports = {
|
||||
projectId: args.streamId,
|
||||
authCtx: context
|
||||
})
|
||||
await viewComment({ userId: context.userId, commentId: args.commentId })
|
||||
await viewComment({ userId: context.userId!, commentId: args.commentId })
|
||||
return true
|
||||
},
|
||||
|
||||
@@ -453,7 +469,7 @@ module.exports = {
|
||||
|
||||
let updatedComment
|
||||
try {
|
||||
updatedComment = await archiveComment({ ...args, userId: context.userId }) // NOTE: permissions check inside service
|
||||
updatedComment = await archiveComment({ ...args, userId: context.userId! }) // NOTE: permissions check inside service
|
||||
} catch (err) {
|
||||
if (err instanceof ForbiddenError) throw new ApolloForbiddenError(err.message)
|
||||
throw err
|
||||
@@ -462,7 +478,7 @@ module.exports = {
|
||||
await addCommentArchivedActivity({
|
||||
streamId: args.streamId,
|
||||
commentId: args.commentId,
|
||||
userId: context.userId,
|
||||
userId: context.userId!,
|
||||
input: args,
|
||||
comment: updatedComment
|
||||
})
|
||||
@@ -479,15 +495,15 @@ module.exports = {
|
||||
userId: context.userId
|
||||
})
|
||||
|
||||
if (!stream.allowPublicComments && !stream.role)
|
||||
if (!stream?.allowPublicComments && !stream?.role)
|
||||
throw new ApolloForbiddenError('You are not authorized.')
|
||||
|
||||
const reply = await createCommentReply({
|
||||
authorId: context.userId,
|
||||
parentCommentId: args.input.parentComment,
|
||||
streamId: args.input.streamId,
|
||||
text: args.input.text,
|
||||
data: args.input.data,
|
||||
text: args.input.text as SmartTextEditorValueSchema,
|
||||
data: args.input.data ?? null,
|
||||
blobIds: args.input.blobIds
|
||||
})
|
||||
|
||||
@@ -503,15 +519,15 @@ module.exports = {
|
||||
},
|
||||
Subscription: {
|
||||
userViewerActivity: {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator([CommentSubscriptions.ViewerActivity]),
|
||||
subscribe: filteredSubscribe(
|
||||
CommentSubscriptions.ViewerActivity,
|
||||
async (payload, variables, context) => {
|
||||
const stream = await getStream({
|
||||
streamId: payload.streamId,
|
||||
userId: context.userId
|
||||
})
|
||||
|
||||
if (!stream.allowPublicComments && !stream.role)
|
||||
if (!stream?.allowPublicComments && !stream?.role)
|
||||
throw new ApolloForbiddenError('You are not authorized.')
|
||||
|
||||
// dont report users activity to himself
|
||||
@@ -527,15 +543,15 @@ module.exports = {
|
||||
)
|
||||
},
|
||||
commentActivity: {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator([CommentSubscriptions.CommentActivity]),
|
||||
subscribe: filteredSubscribe(
|
||||
CommentSubscriptions.CommentActivity,
|
||||
async (payload, variables, context) => {
|
||||
const stream = await getStream({
|
||||
streamId: payload.streamId,
|
||||
userId: context.userId
|
||||
})
|
||||
|
||||
if (!stream.allowPublicComments && !stream.role)
|
||||
if (!stream?.allowPublicComments && !stream?.role)
|
||||
throw new ApolloForbiddenError('You are not authorized.')
|
||||
|
||||
// if we're listening for a stream's root comments events
|
||||
@@ -548,14 +564,18 @@ module.exports = {
|
||||
// prevents comment exfiltration by listening in to a auth'ed stream, but different commit ("stream hopping" for subscriptions)
|
||||
await streamResourceCheck({
|
||||
streamId: variables.streamId,
|
||||
resources: variables.resourceIds.map((resId) => {
|
||||
return {
|
||||
resourceId: resId,
|
||||
resourceType: resId.length === 10 ? 'commit' : 'object'
|
||||
}
|
||||
})
|
||||
resources: variables.resourceIds
|
||||
.filter((resId): resId is string => !!resId)
|
||||
.map((resId) => {
|
||||
return {
|
||||
resourceId: resId,
|
||||
resourceType:
|
||||
resId.length === 10 ? ResourceType.Commit : ResourceType.Object
|
||||
}
|
||||
})
|
||||
})
|
||||
for (const res of variables.resourceIds) {
|
||||
if (!res) continue
|
||||
if (
|
||||
payload.resourceIds.includes(res) &&
|
||||
payload.streamId === variables.streamId
|
||||
@@ -566,19 +586,21 @@ module.exports = {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
)
|
||||
},
|
||||
commentThreadActivity: {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator([CommentSubscriptions.CommentThreadActivity]),
|
||||
subscribe: filteredSubscribe(
|
||||
CommentSubscriptions.CommentThreadActivity,
|
||||
async (payload, variables, context) => {
|
||||
const stream = await getStream({
|
||||
streamId: payload.streamId,
|
||||
userId: context.userId
|
||||
})
|
||||
|
||||
if (!stream.allowPublicComments && !stream.role)
|
||||
if (!stream?.allowPublicComments && !stream?.role)
|
||||
throw new ApolloForbiddenError('You are not authorized.')
|
||||
|
||||
return (
|
||||
@@ -607,7 +629,7 @@ module.exports = {
|
||||
getViewerResourceItemsUngrouped(target)
|
||||
])
|
||||
|
||||
if (!stream.isPublic && !stream.role)
|
||||
if (!stream?.isPublic && !stream?.role)
|
||||
throw new ApolloForbiddenError('You are not authorized.')
|
||||
|
||||
// dont report users activity to himself
|
||||
@@ -642,7 +664,7 @@ module.exports = {
|
||||
getViewerResourceItemsUngrouped(target)
|
||||
])
|
||||
|
||||
if (!(stream.isDiscoverable || stream.isPublic) && !stream.role)
|
||||
if (!(stream?.isDiscoverable || stream?.isPublic) && !stream?.role)
|
||||
throw new ApolloForbiddenError('You are not authorized.')
|
||||
|
||||
if (!target.resourceIdString) {
|
||||
@@ -659,4 +681,4 @@ module.exports = {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} as Resolvers
|
||||
@@ -1,4 +1,5 @@
|
||||
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'
|
||||
@@ -9,7 +10,7 @@ export interface CommentRecord {
|
||||
authorId: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
text: Nullable<string>
|
||||
text: Nullable<string | SmartTextEditorValueSchema>
|
||||
screenshot: Nullable<string>
|
||||
data: Nullable<LegacyData | DataStruct>
|
||||
archived: boolean
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
const { moduleLogger } = require('@/logging/logging')
|
||||
const {
|
||||
notifyUsersOnCommentEvents
|
||||
} = require('@/modules/comments/services/notifications')
|
||||
|
||||
let unsubFromEvents
|
||||
|
||||
exports.init = async (_, isInitial) => {
|
||||
moduleLogger.info('🗣 Init comments module')
|
||||
|
||||
if (isInitial) {
|
||||
unsubFromEvents = await notifyUsersOnCommentEvents()
|
||||
}
|
||||
}
|
||||
exports.finalize = async () => {}
|
||||
exports.shutdown = async () => {
|
||||
unsubFromEvents?.()
|
||||
unsubFromEvents = undefined
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { moduleLogger } from '@/logging/logging'
|
||||
import { notifyUsersOnCommentEvents } from '@/modules/comments/services/notifications'
|
||||
import { Optional, SpeckleModule } from '@/modules/shared/helpers/typeHelper'
|
||||
|
||||
let unsubFromEvents: Optional<() => void> = undefined
|
||||
|
||||
const commentsModule: SpeckleModule = {
|
||||
async init(_, isInitial) {
|
||||
moduleLogger.info('🗣 Init comments module')
|
||||
|
||||
if (isInitial) {
|
||||
unsubFromEvents = await notifyUsersOnCommentEvents()
|
||||
}
|
||||
},
|
||||
async finalize() {},
|
||||
async shutdown() {
|
||||
unsubFromEvents?.()
|
||||
unsubFromEvents = undefined
|
||||
}
|
||||
}
|
||||
|
||||
export = commentsModule
|
||||
@@ -466,15 +466,15 @@ export type PaginatedProjectCommentsParams = {
|
||||
cursor?: MaybeNullOrUndefined<string>
|
||||
filter?: MaybeNullOrUndefined<
|
||||
Partial<{
|
||||
threadsOnly: boolean
|
||||
includeArchived: boolean
|
||||
archivedOnly: boolean
|
||||
resourceIdString: string
|
||||
threadsOnly: boolean | null
|
||||
includeArchived: boolean | null
|
||||
archivedOnly: boolean | null
|
||||
resourceIdString: string | null
|
||||
/**
|
||||
* If true, will ignore the version parts of `model@version` identifiers and look for comments of
|
||||
* all versions of any selected comments
|
||||
*/
|
||||
allModelVersions: boolean
|
||||
allModelVersions: boolean | null
|
||||
}>
|
||||
>
|
||||
}
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
'use strict'
|
||||
const crs = require('crypto-random-string')
|
||||
const knex = require('@/db/knex')
|
||||
const { ForbiddenError } = require('@/modules/shared/errors')
|
||||
const {
|
||||
buildCommentTextFromInput,
|
||||
validateInputAttachments
|
||||
} = require('@/modules/comments/services/commentTextService')
|
||||
const { CommentsEmitter, CommentsEvents } = require('@/modules/comments/events/emitter')
|
||||
const {
|
||||
getComment,
|
||||
getStreamCommentCount,
|
||||
markCommentViewed
|
||||
} = require('@/modules/comments/repositories/comments')
|
||||
const { clamp } = require('lodash')
|
||||
const { Roles } = require('@speckle/shared')
|
||||
|
||||
const Comments = () => knex('comments')
|
||||
const CommentLinks = () => knex('comment_links')
|
||||
|
||||
const resourceCheck = 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 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`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
async streamResourceCheck({ streamId, resources }) {
|
||||
// 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
|
||||
*/
|
||||
async createComment({ userId, input }) {
|
||||
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.')
|
||||
|
||||
// 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 comment = {
|
||||
streamId: input.streamId,
|
||||
text: input.text,
|
||||
data: input.data,
|
||||
screenshot: input.screenshot
|
||||
}
|
||||
|
||||
comment.id = crs({ length: 10 })
|
||||
comment.authorId = userId
|
||||
|
||||
await validateInputAttachments(input.streamId, input.blobIds)
|
||||
comment.text = buildCommentTextFromInput({
|
||||
doc: input.text,
|
||||
blobIds: input.blobIds
|
||||
})
|
||||
|
||||
const [newComment] = await Comments().insert(comment, '*')
|
||||
try {
|
||||
await module.exports.streamResourceCheck({
|
||||
streamId: input.streamId,
|
||||
resources: input.resources
|
||||
})
|
||||
for (const res of input.resources) {
|
||||
await CommentLinks().insert({
|
||||
commentId: comment.id,
|
||||
resourceId: res.resourceId,
|
||||
resourceType: res.resourceType
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
await Comments().where({ id: comment.id }).delete() // roll back
|
||||
throw e // pass on to resolver
|
||||
}
|
||||
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
|
||||
*/
|
||||
async createCommentReply({
|
||||
authorId,
|
||||
parentCommentId,
|
||||
streamId,
|
||||
text,
|
||||
data,
|
||||
blobIds
|
||||
}) {
|
||||
await validateInputAttachments(streamId, blobIds)
|
||||
const comment = {
|
||||
id: crs({ length: 10 }),
|
||||
authorId,
|
||||
text: buildCommentTextFromInput({ doc: text, blobIds }),
|
||||
data,
|
||||
streamId,
|
||||
parentComment: parentCommentId
|
||||
}
|
||||
|
||||
const [newComment] = await Comments().insert(comment, '*')
|
||||
try {
|
||||
const commentLink = { resourceId: parentCommentId, resourceType: 'comment' }
|
||||
await module.exports.streamResourceCheck({
|
||||
streamId,
|
||||
resources: [commentLink]
|
||||
})
|
||||
await CommentLinks().insert({ commentId: comment.id, ...commentLink })
|
||||
} catch (e) {
|
||||
await Comments().where({ id: comment.id }).delete() // roll back
|
||||
throw e // pass on to resolver
|
||||
}
|
||||
await Comments().where({ id: parentCommentId }).update({ updatedAt: knex.fn.now() })
|
||||
|
||||
await CommentsEmitter.emit(CommentsEvents.Created, {
|
||||
comment: newComment
|
||||
})
|
||||
|
||||
return newComment
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated Use 'editCommentAndNotify()'
|
||||
*/
|
||||
async editComment({ userId, input, matchUser = false }) {
|
||||
const editedComment = await Comments().where({ id: input.id }).first()
|
||||
if (!editedComment) throw new Error("The comment doesn't exist")
|
||||
|
||||
if (matchUser && editedComment.authorId !== userId)
|
||||
throw new ForbiddenError("You cannot edit someone else's comments")
|
||||
|
||||
await validateInputAttachments(input.streamId, input.blobIds)
|
||||
const newText = buildCommentTextFromInput({
|
||||
doc: input.text,
|
||||
blobIds: input.blobIds
|
||||
})
|
||||
const [updatedComment] = await Comments()
|
||||
.where({ id: input.id })
|
||||
.update({ text: newText }, '*')
|
||||
|
||||
await CommentsEmitter.emit(CommentsEvents.Updated, {
|
||||
previousComment: editedComment,
|
||||
newComment: updatedComment
|
||||
})
|
||||
|
||||
return updatedComment
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated Use 'markCommentViewed()'
|
||||
*/
|
||||
async viewComment({ userId, commentId }) {
|
||||
await markCommentViewed(commentId, userId)
|
||||
},
|
||||
/**
|
||||
* @deprecated Use repository method
|
||||
*/
|
||||
getComment,
|
||||
/**
|
||||
* @deprecated Use 'archiveCommentAndNotify()'
|
||||
*/
|
||||
async archiveComment({ commentId, userId, streamId, archived = true }) {
|
||||
const comment = await Comments().where({ id: commentId }).first()
|
||||
if (!comment)
|
||||
throw new Error(
|
||||
`No comment ${commentId} exists, cannot change its archival status`
|
||||
)
|
||||
|
||||
const aclEntry = await knex('stream_acl')
|
||||
.select()
|
||||
.where({ resourceId: streamId, userId })
|
||||
.first()
|
||||
|
||||
if (comment.authorId !== userId) {
|
||||
if (!aclEntry || aclEntry.role !== Roles.Stream.Owner)
|
||||
throw new ForbiddenError("You don't have permission to archive the comment")
|
||||
}
|
||||
|
||||
const [updatedComment] = await Comments()
|
||||
.where({ id: commentId })
|
||||
.update({ archived }, '*')
|
||||
return updatedComment
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated Use `getPaginatedProjectComments()` instead
|
||||
*/
|
||||
async getComments({
|
||||
resources,
|
||||
limit,
|
||||
cursor,
|
||||
userId = null,
|
||||
replies = false,
|
||||
streamId,
|
||||
archived = false
|
||||
}) {
|
||||
const query = knex.with('comms', (cte) => {
|
||||
cte.select().distinctOn('id').from('comments')
|
||||
cte.join('comment_links', 'comments.id', '=', 'commentId')
|
||||
|
||||
if (userId) {
|
||||
// link viewed At
|
||||
cte.leftOuterJoin('comment_views', (b) => {
|
||||
b.on('comment_views.commentId', '=', 'comments.id')
|
||||
b.andOn('comment_views.userId', '=', knex.raw('?', userId))
|
||||
})
|
||||
}
|
||||
|
||||
if (resources && resources.length !== 0) {
|
||||
cte.where((q) => {
|
||||
// link resources
|
||||
for (const res of resources) {
|
||||
q.orWhere('comment_links.resourceId', '=', res.resourceId)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
cte.where({ streamId })
|
||||
}
|
||||
if (!replies) {
|
||||
cte.whereNull('parentComment')
|
||||
}
|
||||
cte.where('archived', '=', archived)
|
||||
})
|
||||
|
||||
query.select().from('comms')
|
||||
|
||||
// total count coming from our cte
|
||||
query.joinRaw('right join (select count(*) from comms) c(total_count) on true')
|
||||
|
||||
// get comment's all linked resources
|
||||
query.joinRaw(`
|
||||
join(
|
||||
select cl."commentId" as id, JSON_AGG(json_build_object('resourceId', cl."resourceId", 'resourceType', cl."resourceType")) as resources
|
||||
from comment_links cl
|
||||
join comms on comms.id = cl."commentId"
|
||||
group by cl."commentId"
|
||||
) res using(id)`)
|
||||
|
||||
if (cursor) {
|
||||
query.where('createdAt', '<', cursor)
|
||||
}
|
||||
|
||||
limit = clamp(limit ?? 10, 0, 100)
|
||||
query.orderBy('createdAt', 'desc')
|
||||
query.limit(limit || 1) // need at least 1 row to get totalCount
|
||||
|
||||
const rows = await query
|
||||
const totalCount = rows && rows.length > 0 ? parseInt(rows[0].total_count) : 0
|
||||
const nextCursor = rows && rows.length > 0 ? rows[rows.length - 1].createdAt : null
|
||||
|
||||
return {
|
||||
items: !limit ? [] : rows,
|
||||
cursor: nextCursor ? nextCursor.toISOString() : null,
|
||||
totalCount
|
||||
}
|
||||
},
|
||||
|
||||
async getResourceCommentCount({ resourceId }) {
|
||||
const [res] = await CommentLinks()
|
||||
.count('commentId')
|
||||
.where({ resourceId })
|
||||
.join('comments', 'comments.id', '=', 'commentId')
|
||||
.where('comments.archived', '=', false)
|
||||
|
||||
if (res && res.count) {
|
||||
return parseInt(res.count)
|
||||
}
|
||||
return 0
|
||||
},
|
||||
|
||||
async getStreamCommentCount({ streamId }) {
|
||||
return (await getStreamCommentCount(streamId, { threadsOnly: true })) || 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
import crs from 'crypto-random-string'
|
||||
import knex from '@/db/knex'
|
||||
import { ForbiddenError } from '@/modules/shared/errors'
|
||||
import {
|
||||
buildCommentTextFromInput,
|
||||
validateInputAttachments
|
||||
} from '@/modules/comments/services/commentTextService'
|
||||
import { CommentsEmitter, CommentsEvents } from '@/modules/comments/events/emitter'
|
||||
import {
|
||||
getComment as repoGetComment,
|
||||
getStreamCommentCount as repoGetStreamCommentCount,
|
||||
markCommentViewed
|
||||
} from '@/modules/comments/repositories/comments'
|
||||
import { clamp } from 'lodash'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import {
|
||||
ResourceIdentifier,
|
||||
CommentCreateInput,
|
||||
CommentEditInput,
|
||||
SmartTextEditorValue
|
||||
} from '@/modules/core/graph/generated/graphql'
|
||||
import { CommentLinkRecord, CommentRecord } from '@/modules/comments/helpers/types'
|
||||
import { SmartTextEditorValueSchema } from '@/modules/core/services/richTextEditorService'
|
||||
|
||||
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 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')
|
||||
|
||||
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')
|
||||
|
||||
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({
|
||||
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
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
await Comments().where({ id: comment.id }).delete() // roll back
|
||||
throw e // pass on to resolver
|
||||
}
|
||||
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
|
||||
*/
|
||||
export async function createCommentReply({
|
||||
authorId,
|
||||
parentCommentId,
|
||||
streamId,
|
||||
text,
|
||||
data,
|
||||
blobIds
|
||||
}: {
|
||||
authorId: string
|
||||
parentCommentId: string
|
||||
streamId: string
|
||||
text: SmartTextEditorValue
|
||||
data: CommentRecord['data']
|
||||
blobIds: string[]
|
||||
}) {
|
||||
await validateInputAttachments(streamId, blobIds)
|
||||
const comment = {
|
||||
id: crs({ length: 10 }),
|
||||
authorId,
|
||||
text: buildCommentTextFromInput({ doc: text, blobIds }),
|
||||
data,
|
||||
streamId,
|
||||
parentComment: parentCommentId
|
||||
}
|
||||
|
||||
const [newComment] = await Comments().insert(comment, '*')
|
||||
try {
|
||||
const commentLink: Omit<CommentLinkRecord, 'commentId'> = {
|
||||
resourceId: parentCommentId,
|
||||
resourceType: 'comment'
|
||||
}
|
||||
await module.exports.streamResourceCheck({
|
||||
streamId,
|
||||
resources: [commentLink]
|
||||
})
|
||||
await CommentLinks().insert({ commentId: comment.id, ...commentLink })
|
||||
} catch (e) {
|
||||
await Comments().where({ id: comment.id }).delete() // roll back
|
||||
throw e // pass on to resolver
|
||||
}
|
||||
await Comments().where({ id: parentCommentId }).update({ updatedAt: knex.fn.now() })
|
||||
|
||||
await CommentsEmitter.emit(CommentsEvents.Created, {
|
||||
comment: newComment
|
||||
})
|
||||
|
||||
return newComment
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use 'editCommentAndNotify()'
|
||||
*/
|
||||
export async function editComment({
|
||||
userId,
|
||||
input,
|
||||
matchUser = false
|
||||
}: {
|
||||
userId: string
|
||||
input: CommentEditInput
|
||||
matchUser: boolean
|
||||
}) {
|
||||
const editedComment = await Comments().where({ id: input.id }).first()
|
||||
if (!editedComment) throw new Error("The comment doesn't exist")
|
||||
|
||||
if (matchUser && editedComment.authorId !== userId)
|
||||
throw new ForbiddenError("You cannot edit someone else's comments")
|
||||
|
||||
await validateInputAttachments(input.streamId, input.blobIds)
|
||||
const newText = buildCommentTextFromInput({
|
||||
doc: input.text,
|
||||
blobIds: input.blobIds
|
||||
})
|
||||
const [updatedComment] = await Comments()
|
||||
.where({ id: input.id })
|
||||
.update({ text: newText }, '*')
|
||||
|
||||
await CommentsEmitter.emit(CommentsEvents.Updated, {
|
||||
previousComment: editedComment,
|
||||
newComment: updatedComment
|
||||
})
|
||||
|
||||
return updatedComment
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use 'markCommentViewed()'
|
||||
*/
|
||||
export async function viewComment({
|
||||
userId,
|
||||
commentId
|
||||
}: {
|
||||
userId: string
|
||||
commentId: string
|
||||
}) {
|
||||
await markCommentViewed(commentId, userId)
|
||||
}
|
||||
/**
|
||||
* @deprecated Use repository method
|
||||
*/
|
||||
export const getComment = repoGetComment
|
||||
/**
|
||||
* @deprecated Use 'archiveCommentAndNotify()'
|
||||
*/
|
||||
export async function archiveComment({
|
||||
commentId,
|
||||
userId,
|
||||
streamId,
|
||||
archived = true
|
||||
}: {
|
||||
commentId: string
|
||||
userId: string
|
||||
streamId: string
|
||||
archived: boolean
|
||||
}) {
|
||||
const comment = await Comments().where({ id: commentId }).first()
|
||||
if (!comment)
|
||||
throw new Error(`No comment ${commentId} exists, cannot change its archival status`)
|
||||
|
||||
const aclEntry = await knex('stream_acl')
|
||||
.select()
|
||||
.where({ resourceId: streamId, userId })
|
||||
.first()
|
||||
|
||||
if (comment.authorId !== userId) {
|
||||
if (!aclEntry || aclEntry.role !== Roles.Stream.Owner)
|
||||
throw new ForbiddenError("You don't have permission to archive the comment")
|
||||
}
|
||||
|
||||
const [updatedComment] = await Comments()
|
||||
.where({ id: commentId })
|
||||
.update({ archived }, '*')
|
||||
return updatedComment
|
||||
}
|
||||
|
||||
/**
|
||||
* One of `streamId` or `resources` expected. If both are provided, then
|
||||
* `resources` takes precedence.
|
||||
*/
|
||||
type GetCommentsParams = {
|
||||
limit?: number | null
|
||||
cursor?: string | null
|
||||
userId?: string | null
|
||||
replies?: boolean | null
|
||||
archived?: boolean | null
|
||||
} & (
|
||||
| {
|
||||
resources: ResourceIdentifier[]
|
||||
streamId?: null
|
||||
}
|
||||
| {
|
||||
resources?: ResourceIdentifier[] | null
|
||||
streamId: string
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* @deprecated Use `getPaginatedProjectComments()` instead
|
||||
*/
|
||||
export async function getComments({
|
||||
resources,
|
||||
limit,
|
||||
cursor,
|
||||
userId = null,
|
||||
replies = false,
|
||||
streamId,
|
||||
archived = false
|
||||
}: GetCommentsParams) {
|
||||
const query = knex.with('comms', (cte) => {
|
||||
cte.select().distinctOn('id').from('comments')
|
||||
cte.join('comment_links', 'comments.id', '=', 'commentId')
|
||||
|
||||
if (userId) {
|
||||
// link viewed At
|
||||
cte.leftOuterJoin('comment_views', (b) => {
|
||||
b.on('comment_views.commentId', '=', 'comments.id')
|
||||
b.andOn('comment_views.userId', '=', knex.raw('?', userId))
|
||||
})
|
||||
}
|
||||
|
||||
if (resources && resources.length !== 0) {
|
||||
cte.where((q) => {
|
||||
// link resources
|
||||
for (const res of resources) {
|
||||
q.orWhere('comment_links.resourceId', '=', res.resourceId)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
cte.where({ streamId })
|
||||
}
|
||||
if (!replies) {
|
||||
cte.whereNull('parentComment')
|
||||
}
|
||||
cte.where('archived', '=', archived)
|
||||
})
|
||||
|
||||
query.select().from('comms')
|
||||
|
||||
// total count coming from our cte
|
||||
query.joinRaw('right join (select count(*) from comms) c(total_count) on true')
|
||||
|
||||
// get comment's all linked resources
|
||||
query.joinRaw(`
|
||||
join(
|
||||
select cl."commentId" as id, JSON_AGG(json_build_object('resourceId', cl."resourceId", 'resourceType', cl."resourceType")) as resources
|
||||
from comment_links cl
|
||||
join comms on comms.id = cl."commentId"
|
||||
group by cl."commentId"
|
||||
) res using(id)`)
|
||||
|
||||
if (cursor) {
|
||||
query.where('createdAt', '<', cursor)
|
||||
}
|
||||
|
||||
limit = clamp(limit ?? 10, 0, 100)
|
||||
query.orderBy('createdAt', 'desc')
|
||||
query.limit(limit || 1) // need at least 1 row to get totalCount
|
||||
|
||||
const rows = await query
|
||||
const totalCount = rows && rows.length > 0 ? parseInt(rows[0].total_count) : 0
|
||||
const nextCursor = rows && rows.length > 0 ? rows[rows.length - 1].createdAt : null
|
||||
|
||||
return {
|
||||
items: !limit ? [] : rows,
|
||||
cursor: nextCursor ? nextCursor.toISOString() : null,
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
|
||||
export async function getResourceCommentCount({ resourceId }: { resourceId: string }) {
|
||||
const [res] = await CommentLinks()
|
||||
.count('commentId')
|
||||
.where({ resourceId })
|
||||
.join('comments', 'comments.id', '=', 'commentId')
|
||||
.where('comments.archived', '=', false)
|
||||
|
||||
if (res && res.count) {
|
||||
return parseInt(String(res.count))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
export async function getStreamCommentCount({ streamId }: { streamId: string }) {
|
||||
return (await repoGetStreamCommentCount(streamId, { threadsOnly: true })) || 0
|
||||
}
|
||||
@@ -31,6 +31,11 @@ import {
|
||||
GendoAiRender,
|
||||
SubscriptionProjectVersionGendoAiRenderUpdatedArgs,
|
||||
SubscriptionProjectVersionGendoAiRenderCreatedArgs,
|
||||
CommentThreadActivityMessage,
|
||||
SubscriptionCommentThreadActivityArgs,
|
||||
MutationUserViewerActivityBroadcastArgs,
|
||||
SubscriptionUserViewerActivityArgs,
|
||||
SubscriptionCommentActivityArgs,
|
||||
StreamUpdateInput,
|
||||
ProjectUpdateInput,
|
||||
SubscriptionStreamUpdatedArgs,
|
||||
@@ -48,6 +53,7 @@ import {
|
||||
ProjectTriggeredAutomationsStatusUpdatedMessageGraphQLReturn,
|
||||
ProjectAutomationsUpdatedMessageGraphQLReturn
|
||||
} from '@/modules/automate/helpers/graphTypes'
|
||||
import { CommentRecord } from '@/modules/comments/helpers/types'
|
||||
|
||||
/**
|
||||
* GraphQL Subscription PubSub instance
|
||||
@@ -241,6 +247,35 @@ type SubscriptionTypeMap = {
|
||||
}
|
||||
variables: SubscriptionProjectAutomationsUpdatedArgs
|
||||
}
|
||||
[CommentSubscriptions.CommentThreadActivity]: {
|
||||
payload: {
|
||||
commentThreadActivity: Partial<CommentThreadActivityMessage> &
|
||||
Pick<CommentThreadActivityMessage, 'type'>
|
||||
streamId: string
|
||||
commentId: string
|
||||
}
|
||||
variables: SubscriptionCommentThreadActivityArgs
|
||||
}
|
||||
[CommentSubscriptions.ViewerActivity]: {
|
||||
payload: {
|
||||
userViewerActivity: MutationUserViewerActivityBroadcastArgs
|
||||
streamId: string
|
||||
resourceId: string
|
||||
authorId: string
|
||||
}
|
||||
variables: SubscriptionUserViewerActivityArgs
|
||||
}
|
||||
[CommentSubscriptions.CommentActivity]: {
|
||||
payload: {
|
||||
commentActivity: {
|
||||
type: 'comment-added'
|
||||
comment: CommentRecord
|
||||
}
|
||||
streamId: string
|
||||
resourceIds: string[]
|
||||
}
|
||||
variables: SubscriptionCommentActivityArgs
|
||||
}
|
||||
/**
|
||||
* OLD ONES
|
||||
*/
|
||||
@@ -269,11 +304,12 @@ type SubscriptionTypeMap = {
|
||||
} & { [k in SubscriptionEvent]: { payload: unknown; variables: unknown } }
|
||||
|
||||
type SubscriptionEvent =
|
||||
| UserSubscriptions
|
||||
| ProjectSubscriptions
|
||||
| ViewerSubscriptions
|
||||
| CommentSubscriptions
|
||||
| FileImportSubscriptions
|
||||
| ProjectSubscriptions
|
||||
| StreamSubscriptions
|
||||
| UserSubscriptions
|
||||
| ViewerSubscriptions
|
||||
|
||||
/**
|
||||
* Publish a GQL subscription event
|
||||
|
||||
Reference in New Issue
Block a user