From b41a98d107b00cae059789ae4aa95635c1b3b97f Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Tue, 14 Jan 2025 18:24:26 +0200 Subject: [PATCH] chore(server): getting rid of module-scoped eventBuses - batch #4 - comments (#3812) --- .../modules/cli/commands/download/commit.ts | 5 +- .../modules/cli/commands/download/project.ts | 5 +- .../server/modules/comments/domain/events.ts | 16 + .../server/modules/comments/events/emitter.ts | 22 - .../comments/graph/resolvers/comments.ts | 14 +- packages/server/modules/comments/index.ts | 4 +- .../comments/services/commentTextService.ts | 12 +- .../server/modules/comments/services/index.ts | 38 +- .../modules/comments/services/management.ts | 32 +- .../comments/services/notifications.ts | 16 +- .../comments/tests/comments.graph.spec.js | 3 +- .../{comments.spec.js => comments.spec.ts} | 750 +++++++++++------- .../comments/tests/projectComments.spec.ts | 138 +++- .../server/modules/cross-server-sync/index.ts | 5 +- .../modules/shared/services/eventBus.ts | 45 +- packages/server/test/blobHelper.js | 28 - packages/server/test/blobHelper.ts | 27 + .../server/test/graphql/generated/graphql.ts | 21 +- .../server/test/graphql/projectComments.ts | 44 +- 19 files changed, 800 insertions(+), 425 deletions(-) create mode 100644 packages/server/modules/comments/domain/events.ts delete mode 100644 packages/server/modules/comments/events/emitter.ts rename packages/server/modules/comments/tests/{comments.spec.js => comments.spec.ts} (68%) delete mode 100644 packages/server/test/blobHelper.js create mode 100644 packages/server/test/blobHelper.ts diff --git a/packages/server/modules/cli/commands/download/commit.ts b/packages/server/modules/cli/commands/download/commit.ts index a5da2f3e72..5e69332200 100644 --- a/packages/server/modules/cli/commands/download/commit.ts +++ b/packages/server/modules/cli/commands/download/commit.ts @@ -46,7 +46,6 @@ import { markCommentUpdatedFactory, markCommentViewedFactory } from '@/modules/comments/repositories/comments' -import { CommentsEmitter } from '@/modules/comments/events/emitter' import { addCommentCreatedActivityFactory, addReplyAddedActivityFactory @@ -144,7 +143,7 @@ const command: CommandModule< insertComments, insertCommentLinks, markCommentViewed, - commentsEventsEmit: CommentsEmitter.emit, + emitEvent: getEventBus().emit, addCommentCreatedActivity: addCommentCreatedActivityFactory({ getViewerResourcesFromLegacyIdentifiers, getViewerResourceItemsUngrouped, @@ -159,7 +158,7 @@ const command: CommandModule< insertComments, insertCommentLinks, markCommentUpdated: markCommentUpdatedFactory({ db: projectDb }), - commentsEventsEmit: CommentsEmitter.emit, + emitEvent: getEventBus().emit, addReplyAddedActivity: addReplyAddedActivityFactory({ getViewerResourcesForComment: getViewerResourcesForCommentFactory({ getCommentsResources: getCommentsResourcesFactory({ db: projectDb }), diff --git a/packages/server/modules/cli/commands/download/project.ts b/packages/server/modules/cli/commands/download/project.ts index b980648ccc..8789100a28 100644 --- a/packages/server/modules/cli/commands/download/project.ts +++ b/packages/server/modules/cli/commands/download/project.ts @@ -27,7 +27,6 @@ import { createCommentThreadAndNotifyFactory } from '@/modules/comments/services/management' import { createBranchAndNotifyFactory } from '@/modules/core/services/branch/management' -import { CommentsEmitter } from '@/modules/comments/events/emitter' import { addCommentCreatedActivityFactory, addReplyAddedActivityFactory @@ -170,7 +169,7 @@ const command: CommandModule< insertComments, insertCommentLinks, markCommentViewed, - commentsEventsEmit: CommentsEmitter.emit, + emitEvent: getEventBus().emit, addCommentCreatedActivity: addCommentCreatedActivityFactory({ getViewerResourcesFromLegacyIdentifiers, getViewerResourceItemsUngrouped, @@ -184,7 +183,7 @@ const command: CommandModule< insertComments, insertCommentLinks, markCommentUpdated: markCommentUpdatedFactory({ db: projectDb }), - commentsEventsEmit: CommentsEmitter.emit, + emitEvent: getEventBus().emit, addReplyAddedActivity: addReplyAddedActivityFactory({ getViewerResourcesForComment: getViewerResourcesForCommentFactory({ getCommentsResources: getCommentsResourcesFactory({ db: projectDb }), diff --git a/packages/server/modules/comments/domain/events.ts b/packages/server/modules/comments/domain/events.ts new file mode 100644 index 0000000000..30f73395ed --- /dev/null +++ b/packages/server/modules/comments/domain/events.ts @@ -0,0 +1,16 @@ +import { CommentRecord } from '@/modules/comments/helpers/types' + +export const commentEventsNamespace = 'comments' as const + +export const CommentEvents = { + Created: `${commentEventsNamespace}.created`, + Updated: `${commentEventsNamespace}.updated` +} as const + +export type CommentEventsPayloads = { + [CommentEvents.Created]: { comment: CommentRecord } + [CommentEvents.Updated]: { + previousComment: CommentRecord + newComment: CommentRecord + } +} diff --git a/packages/server/modules/comments/events/emitter.ts b/packages/server/modules/comments/events/emitter.ts deleted file mode 100644 index 5eaf026378..0000000000 --- a/packages/server/modules/comments/events/emitter.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CommentRecord } from '@/modules/comments/helpers/types' -import { initializeModuleEventEmitter } from '@/modules/shared/services/moduleEventEmitterSetup' - -export enum CommentsEvents { - Created = 'created', - Updated = 'updated' -} - -const { emit, listen } = initializeModuleEventEmitter<{ - // Add mappings between events & payloads here - [CommentsEvents.Created]: { comment: CommentRecord } - [CommentsEvents.Updated]: { - previousComment: CommentRecord - newComment: CommentRecord - } -}>({ - moduleName: 'comments' -}) - -export const CommentsEmitter = { emit, listen, events: CommentsEvents } -export type CommentsEventsEmit = typeof emit -export type CommentsEventsListen = typeof listen diff --git a/packages/server/modules/comments/graph/resolvers/comments.ts b/packages/server/modules/comments/graph/resolvers/comments.ts index 2a9dfd805b..f6f97da06b 100644 --- a/packages/server/modules/comments/graph/resolvers/comments.ts +++ b/packages/server/modules/comments/graph/resolvers/comments.ts @@ -81,7 +81,6 @@ import { Resolvers, ResourceType } from '@/modules/core/graph/generated/graphql' import { GraphQLContext } from '@/modules/shared/helpers/typeHelper' import { CommentRecord } from '@/modules/comments/helpers/types' import { db, mainDb } from '@/db/knex' -import { CommentsEmitter } from '@/modules/comments/events/emitter' import { getBlobsFactory } from '@/modules/blobstorage/repositories' import { ResourceIdentifier } from '@/modules/comments/domain/types' import { @@ -99,6 +98,7 @@ import { getStreamFactory } from '@/modules/core/repositories/streams' import { saveActivityFactory } from '@/modules/activitystream/repositories' import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector' import { Knex } from 'knex' +import { getEventBus } from '@/modules/shared/services/eventBus' // We can use the main DB for these const getStream = getStreamFactory({ db }) @@ -549,7 +549,7 @@ export = { insertComments, insertCommentLinks, markCommentViewed, - commentsEventsEmit: CommentsEmitter.emit, + emitEvent: getEventBus().emit, addCommentCreatedActivity: addCommentCreatedActivityFactory({ getViewerResourcesFromLegacyIdentifiers, getViewerResourceItemsUngrouped, @@ -587,7 +587,7 @@ export = { insertComments, insertCommentLinks, markCommentUpdated: markCommentUpdatedFactory({ db: projectDb }), - commentsEventsEmit: CommentsEmitter.emit, + emitEvent: getEventBus().emit, addReplyAddedActivity: addReplyAddedActivityFactory({ getViewerResourcesForComment: getViewerResourcesForCommentFactory({ getCommentsResources: getCommentsResourcesFactory({ db: projectDb }), @@ -624,7 +624,7 @@ export = { getComment, validateInputAttachments, updateComment, - commentsEventsEmit: CommentsEmitter.emit + emitEvent: getEventBus().emit }) return await editCommentAndNotify(args.input, ctx.userId!) @@ -752,7 +752,7 @@ export = { insertCommentLinks: insertCommentLinksFactory({ db: projectDb }), deleteComment: deleteCommentFactory({ db: projectDb }), markCommentViewed: markCommentViewedFactory({ db: projectDb }), - commentsEventsEmit: CommentsEmitter.emit + emitEvent: getEventBus().emit }) const comment = await createComment({ userId: context.userId, @@ -796,7 +796,7 @@ export = { getBlobs: getBlobsFactory({ db: projectDb }) }), updateComment: updateCommentFactory({ db: projectDb }), - commentsEventsEmit: CommentsEmitter.emit + emitEvent: getEventBus().emit }) await editComment({ userId: context.userId!, input: args.input, matchUser }) @@ -878,7 +878,7 @@ export = { }), deleteComment: deleteCommentFactory({ db: projectDb }), markCommentUpdated: markCommentUpdatedFactory({ db: projectDb }), - commentsEventsEmit: CommentsEmitter.emit + emitEvent: getEventBus().emit }) const reply = await createCommentReply({ authorId: context.userId, diff --git a/packages/server/modules/comments/index.ts b/packages/server/modules/comments/index.ts index 934658cdd8..0f495fa4c4 100644 --- a/packages/server/modules/comments/index.ts +++ b/packages/server/modules/comments/index.ts @@ -2,10 +2,10 @@ import { db } from '@/db/knex' import { moduleLogger } from '@/logging/logging' import { saveActivityFactory } from '@/modules/activitystream/repositories' import { addStreamCommentMentionActivityFactory } from '@/modules/activitystream/services/streamActivity' -import { CommentsEmitter } from '@/modules/comments/events/emitter' import { notifyUsersOnCommentEventsFactory } from '@/modules/comments/services/notifications' import { publishNotification } from '@/modules/notifications/services/publication' import { Optional, SpeckleModule } from '@/modules/shared/helpers/typeHelper' +import { getEventBus } from '@/modules/shared/services/eventBus' let unsubFromEvents: Optional<() => void> = undefined @@ -15,7 +15,7 @@ const commentsModule: SpeckleModule = { if (isInitial) { const notifyUsersOnCommentEvents = notifyUsersOnCommentEventsFactory({ - commentsEventsListen: CommentsEmitter.listen, + eventBus: getEventBus(), publish: publishNotification, addStreamCommentMentionActivity: addStreamCommentMentionActivityFactory({ saveActivity: saveActivityFactory({ db }) diff --git a/packages/server/modules/comments/services/commentTextService.ts b/packages/server/modules/comments/services/commentTextService.ts index d1683adc3c..b7e174d5df 100644 --- a/packages/server/modules/comments/services/commentTextService.ts +++ b/packages/server/modules/comments/services/commentTextService.ts @@ -5,13 +5,15 @@ import { convertBasicStringToDocument, isSerializedTextEditorValueSchema, SmartTextEditorValueSchema, - isDocEmpty + isDocEmpty, + documentToBasicString } from '@/modules/core/services/richTextEditorService' import { isString, uniq } from 'lodash' import { InvalidAttachmentsError } from '@/modules/comments/errors' import { JSONContent } from '@tiptap/core' import { ValidateInputAttachments } from '@/modules/comments/domain/operations' import { GetBlobs } from '@/modules/blobstorage/domain/operations' +import { Nullable } from '@speckle/shared' const COMMENT_SCHEMA_VERSION = '1.0.0' const COMMENT_SCHEMA_TYPE = 'stream_comment' @@ -76,3 +78,11 @@ export function ensureCommentSchema( throw new RichTextParseError('Unexpected comment schema format') } + +export const commentTextToRawString = ( + text: Nullable +) => { + if (!text) return null + const schema = ensureCommentSchema(text) + return documentToBasicString(schema.doc) +} diff --git a/packages/server/modules/comments/services/index.ts b/packages/server/modules/comments/services/index.ts index f90c4aa04e..5c0fc9bd9f 100644 --- a/packages/server/modules/comments/services/index.ts +++ b/packages/server/modules/comments/services/index.ts @@ -1,13 +1,11 @@ import crs from 'crypto-random-string' import { ForbiddenError } from '@/modules/shared/errors' import { buildCommentTextFromInput } from '@/modules/comments/services/commentTextService' -import { CommentsEvents, CommentsEventsEmit } from '@/modules/comments/events/emitter' import { isNonNullable, Roles } from '@speckle/shared' import { ResourceIdentifier, CommentCreateInput, - CommentEditInput, - SmartTextEditorValue + CommentEditInput } from '@/modules/core/graph/generated/graphql' import { CommentLinkRecord, CommentRecord } from '@/modules/comments/helpers/types' import { SmartTextEditorValueSchema } from '@/modules/core/services/richTextEditorService' @@ -25,6 +23,9 @@ import { } from '@/modules/comments/domain/operations' import { ResourceType } from '@/modules/comments/domain/types' import { GetStream } from '@/modules/core/domain/streams/operations' +import { EventBusEmit } from '@/modules/shared/services/eventBus' +import { CommentEvents } from '@/modules/comments/domain/events' +import { JSONContent } from '@tiptap/core' export const streamResourceCheckFactory = (deps: { @@ -54,7 +55,7 @@ export const createCommentFactory = insertCommentLinks: InsertCommentLinks deleteComment: DeleteComment markCommentViewed: MarkCommentViewed - commentsEventsEmit: CommentsEventsEmit + emitEvent: EventBusEmit }) => async ({ userId, input }: { userId: string; input: CommentCreateInput }) => { if (input.resources.length < 1) @@ -115,8 +116,11 @@ export const createCommentFactory = 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 + await deps.emitEvent({ + eventName: CommentEvents.Created, + payload: { + comment: newComment + } }) return newComment @@ -133,7 +137,7 @@ export const createCommentReplyFactory = checkStreamResourcesAccess: CheckStreamResourcesAccess deleteComment: DeleteComment markCommentUpdated: MarkCommentUpdated - commentsEventsEmit: CommentsEventsEmit + emitEvent: EventBusEmit }) => async ({ authorId, @@ -146,7 +150,7 @@ export const createCommentReplyFactory = authorId: string parentCommentId: string streamId: string - text: SmartTextEditorValue + text: JSONContent data: CommentRecord['data'] blobIds: string[] }) => { @@ -184,8 +188,11 @@ export const createCommentReplyFactory = await deps.markCommentUpdated(parentCommentId) - await deps.commentsEventsEmit(CommentsEvents.Created, { - comment: newComment + await deps.emitEvent({ + eventName: CommentEvents.Created, + payload: { + comment: newComment + } }) return newComment @@ -199,7 +206,7 @@ export const editCommentFactory = getComment: GetComment validateInputAttachments: ValidateInputAttachments updateComment: UpdateComment - commentsEventsEmit: CommentsEventsEmit + emitEvent: EventBusEmit }) => async ({ userId, @@ -223,9 +230,12 @@ export const editCommentFactory = }) const updatedComment = await deps.updateComment(input.id, { text: newText }) - await deps.commentsEventsEmit(CommentsEvents.Updated, { - previousComment: editedComment, - newComment: updatedComment! + await deps.emitEvent({ + eventName: CommentEvents.Updated, + payload: { + previousComment: editedComment, + newComment: updatedComment! + } }) return updatedComment diff --git a/packages/server/modules/comments/services/management.ts b/packages/server/modules/comments/services/management.ts index 97d3a7b3c5..0836579b47 100644 --- a/packages/server/modules/comments/services/management.ts +++ b/packages/server/modules/comments/services/management.ts @@ -14,7 +14,6 @@ import { CommentLinkResourceType, CommentRecord } from '@/modules/comments/helpers/types' -import { CommentsEvents, CommentsEventsEmit } from '@/modules/comments/events/emitter' import { formatSerializedViewerState, inputToDataStruct @@ -41,6 +40,8 @@ import { AddCommentCreatedActivity, AddReplyAddedActivity } from '@/modules/activitystream/domain/operations' +import { EventBusEmit } from '@/modules/shared/services/eventBus' +import { CommentEvents } from '@/modules/comments/domain/events' type AuthorizeProjectCommentsAccessDeps = { getStream: GetStream @@ -116,7 +117,7 @@ export const createCommentThreadAndNotifyFactory = insertComments: InsertComments insertCommentLinks: InsertCommentLinks markCommentViewed: MarkCommentViewed - commentsEventsEmit: CommentsEventsEmit + emitEvent: EventBusEmit addCommentCreatedActivity: AddCommentCreatedActivity }): CreateCommentThreadAndNotify => async (input: CreateCommentInput, userId: string) => { @@ -176,8 +177,11 @@ export const createCommentThreadAndNotifyFactory = // Mark as viewed and emit events await Promise.all([ deps.markCommentViewed(comment.id, userId), - deps.commentsEventsEmit(CommentsEvents.Created, { - comment + deps.emitEvent({ + eventName: CommentEvents.Created, + payload: { + comment + } }), deps.addCommentCreatedActivity({ streamId: input.projectId, @@ -200,7 +204,7 @@ export const createCommentReplyAndNotifyFactory = insertComments: InsertComments insertCommentLinks: InsertCommentLinks markCommentUpdated: MarkCommentUpdated - commentsEventsEmit: CommentsEventsEmit + emitEvent: EventBusEmit addReplyAddedActivity: AddReplyAddedActivity }): CreateCommentReplyAndNotify => async (input: CreateCommentReplyInput, userId: string) => { @@ -237,8 +241,11 @@ export const createCommentReplyAndNotifyFactory = // Mark parent comment updated and emit events await Promise.all([ deps.markCommentUpdated(thread.id), - deps.commentsEventsEmit(CommentsEvents.Created, { - comment: reply + deps.emitEvent({ + eventName: CommentEvents.Created, + payload: { + comment: reply + } }), deps.addReplyAddedActivity({ streamId: thread.streamId, @@ -256,7 +263,7 @@ export const editCommentAndNotifyFactory = getComment: GetComment validateInputAttachments: ValidateInputAttachments updateComment: UpdateComment - commentsEventsEmit: CommentsEventsEmit + emitEvent: EventBusEmit }): EditCommentAndNotify => async (input: EditCommentInput, userId: string) => { const comment = await deps.getComment({ id: input.commentId, userId }) @@ -275,9 +282,12 @@ export const editCommentAndNotifyFactory = }) }) - await deps.commentsEventsEmit(CommentsEvents.Updated, { - previousComment: comment, - newComment: updatedComment! + await deps.emitEvent({ + eventName: CommentEvents.Updated, + payload: { + previousComment: comment, + newComment: updatedComment! + } }) return updatedComment diff --git a/packages/server/modules/comments/services/notifications.ts b/packages/server/modules/comments/services/notifications.ts index 52b105ffaa..9199755f5a 100644 --- a/packages/server/modules/comments/services/notifications.ts +++ b/packages/server/modules/comments/services/notifications.ts @@ -1,5 +1,4 @@ import { CommentRecord } from '@/modules/comments/helpers/types' -import { CommentsEvents, CommentsEventsListen } from '@/modules/comments/events/emitter' import { ensureCommentSchema } from '@/modules/comments/services/commentTextService' import type { JSONContent } from '@tiptap/core' import { iterateContentNodes } from '@/modules/core/services/richTextEditorService' @@ -9,6 +8,8 @@ import { NotificationType } from '@/modules/notifications/helpers/types' import { AddStreamCommentMentionActivity } from '@/modules/activitystream/domain/operations' +import { EventBus } from '@/modules/shared/services/eventBus' +import { CommentEvents } from '@/modules/comments/domain/events' function findMentionedUserIds(doc: JSONContent) { const mentionedUserIds = new Set() @@ -92,19 +93,16 @@ const processCommentMentionsFactory = * @returns Callback to invoke when you wish to stop listening for comments events */ export const notifyUsersOnCommentEventsFactory = - ( - deps: { commentsEventsListen: CommentsEventsListen } & SendNotificationsForUsersDeps - ) => - async () => { + (deps: { eventBus: EventBus } & SendNotificationsForUsersDeps) => async () => { const processCommentMentions = processCommentMentionsFactory(deps) const exitCbs = [ - deps.commentsEventsListen(CommentsEvents.Created, async ({ comment }) => { + deps.eventBus.listen(CommentEvents.Created, async ({ payload: { comment } }) => { await processCommentMentions(comment) }), - deps.commentsEventsListen( - CommentsEvents.Updated, - async ({ newComment, previousComment }) => { + deps.eventBus.listen( + CommentEvents.Updated, + async ({ payload: { newComment, previousComment } }) => { await processCommentMentions(newComment, previousComment) } ) diff --git a/packages/server/modules/comments/tests/comments.graph.spec.js b/packages/server/modules/comments/tests/comments.graph.spec.js index dc197d2329..6c2bc26291 100644 --- a/packages/server/modules/comments/tests/comments.graph.spec.js +++ b/packages/server/modules/comments/tests/comments.graph.spec.js @@ -29,7 +29,6 @@ const { validateInputAttachmentsFactory } = require('@/modules/comments/services/commentTextService') const { getBlobsFactory } = require('@/modules/blobstorage/repositories') -const { CommentsEmitter } = require('@/modules/comments/events/emitter') const { createCommitByBranchIdFactory, createCommitByBranchNameFactory @@ -133,7 +132,7 @@ const createComment = createCommentFactory({ insertCommentLinks: insertCommentLinksFactory({ db }), deleteComment: deleteCommentFactory({ db }), markCommentViewed, - commentsEventsEmit: CommentsEmitter.emit + emitEvent: getEventBus().emit }) const getObject = getObjectFactory({ db }) diff --git a/packages/server/modules/comments/tests/comments.spec.js b/packages/server/modules/comments/tests/comments.spec.ts similarity index 68% rename from packages/server/modules/comments/tests/comments.spec.js rename to packages/server/modules/comments/tests/comments.spec.ts index 271ddbb4bb..aa62f4dfe5 100644 --- a/packages/server/modules/comments/tests/comments.spec.js +++ b/packages/server/modules/comments/tests/comments.spec.ts @@ -1,42 +1,38 @@ -const path = require('path') -const { packageRoot } = require('@/bootstrap') -const expect = require('chai').expect -const crs = require('crypto-random-string') -const { beforeEachContext, truncateTables } = require('@/test/hooks') +import path from 'path' +import { packageRoot } from '@/bootstrap' +import { expect } from 'chai' +import crs from 'crypto-random-string' +import { beforeEachContext, truncateTables } from '@/test/hooks' -const { +import { streamResourceCheckFactory, createCommentFactory, createCommentReplyFactory, editCommentFactory, archiveCommentFactory -} = require('@/modules/comments/services/index') -const { - convertBasicStringToDocument -} = require('@/modules/core/services/richTextEditorService') -const { +} from '@/modules/comments/services/index' +import { convertBasicStringToDocument } from '@/modules/core/services/richTextEditorService' +import { ensureCommentSchema, buildCommentTextFromInput, validateInputAttachmentsFactory -} = require('@/modules/comments/services/commentTextService') -const { range } = require('lodash') -const { buildApolloServer } = require('@/app') -const { AllScopes } = require('@/modules/core/helpers/mainConstants') -const { createAuthTokenForUser } = require('@/test/authHelper') -const { uploadBlob } = require('@/test/blobHelper') -const { Comments } = require('@/modules/core/dbSchema') -const CommentsGraphQLClient = require('@/test/graphql/comments') -const { +} from '@/modules/comments/services/commentTextService' +import { get, range } from 'lodash' +import { buildApolloServer } from '@/app' +import { AllScopes } from '@/modules/core/helpers/mainConstants' +import { createAuthTokenForUser } from '@/test/authHelper' +import { uploadBlob, UploadedBlob } from '@/test/blobHelper' +import { Comments } from '@/modules/core/dbSchema' +import * as CommentsGraphQLClient from '@/test/graphql/comments' +import { buildNotificationsStateTracker, + NotificationsStateManager, purgeNotifications -} = require('@/test/notificationsHelper') -const { NotificationType } = require('@/modules/notifications/helpers/types') -const { - EmailSendingServiceMock, - CommentsRepositoryMock -} = require('@/test/mocks/global') -const { createAuthedTestContext } = require('@/test/graphqlHelper') -const { +} from '@/test/notificationsHelper' +import { NotificationType } from '@/modules/notifications/helpers/types' +import { EmailSendingServiceMock, CommentsRepositoryMock } from '@/test/mocks/global' +import { createAuthedTestContext, ServerAndContext } from '@/test/graphqlHelper' +import { checkStreamResourceAccessFactory, markCommentViewedFactory, insertCommentsFactory, @@ -48,92 +44,88 @@ const { getCommentsLegacyFactory, getResourceCommentCountFactory, getStreamCommentCountFactory -} = require('@/modules/comments/repositories/comments') -const { db } = require('@/db/knex') -const { getBlobsFactory } = require('@/modules/blobstorage/repositories') -const { CommentsEmitter } = require('@/modules/comments/events/emitter') -const { +} from '@/modules/comments/repositories/comments' +import { db } from '@/db/knex' +import { getBlobsFactory } from '@/modules/blobstorage/repositories' +import { getStreamFactory, createStreamFactory, markCommitStreamUpdatedFactory -} = require('@/modules/core/repositories/streams') -const { +} from '@/modules/core/repositories/streams' +import { createCommitByBranchIdFactory, createCommitByBranchNameFactory -} = require('@/modules/core/services/commit/management') -const { +} from '@/modules/core/services/commit/management' +import { createCommitFactory, insertStreamCommitsFactory, insertBranchCommitsFactory -} = require('@/modules/core/repositories/commits') -const { +} from '@/modules/core/repositories/commits' +import { getBranchByIdFactory, markCommitBranchUpdatedFactory, getStreamBranchByNameFactory, createBranchFactory -} = require('@/modules/core/repositories/branches') -const { +} from '@/modules/core/repositories/branches' +import { getObjectFactory, storeSingleObjectIfNotFoundFactory, storeClosuresIfNotFoundFactory -} = require('@/modules/core/repositories/objects') -const { +} from '@/modules/core/repositories/objects' +import { legacyCreateStreamFactory, createStreamReturnRecordFactory -} = require('@/modules/core/services/streams/management') -const { - inviteUsersToProjectFactory -} = require('@/modules/serverinvites/services/projectInviteManagement') -const { - createAndSendInviteFactory -} = require('@/modules/serverinvites/services/creation') -const { +} from '@/modules/core/services/streams/management' +import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' +import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' +import { findUserByTargetFactory, insertInviteAndDeleteOldFactory, deleteServerOnlyInvitesFactory, updateAllInviteTargetsFactory -} = require('@/modules/serverinvites/repositories/serverInvites') -const { - collectAndValidateCoreTargetsFactory -} = require('@/modules/serverinvites/services/coreResourceCollection') -const { - buildCoreInviteEmailContentsFactory -} = require('@/modules/serverinvites/services/coreEmailContents') -const { getEventBus } = require('@/modules/shared/services/eventBus') -const { saveActivityFactory } = require('@/modules/activitystream/repositories') -const { publish } = require('@/modules/shared/utils/subscriptions') -const { - addCommitCreatedActivityFactory -} = require('@/modules/activitystream/services/commitActivity') -const { +} from '@/modules/serverinvites/repositories/serverInvites' +import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' +import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' +import { getEventBus } from '@/modules/shared/services/eventBus' +import { saveActivityFactory } from '@/modules/activitystream/repositories' +import { publish } from '@/modules/shared/utils/subscriptions' +import { addCommitCreatedActivityFactory } from '@/modules/activitystream/services/commitActivity' +import { getUsersFactory, getUserFactory, storeUserFactory, countAdminUsersFactory, storeUserAclFactory -} = require('@/modules/core/repositories/users') -const { +} from '@/modules/core/repositories/users' +import { findEmailFactory, createUserEmailFactory, ensureNoPrimaryEmailForUserFactory -} = require('@/modules/core/repositories/userEmails') -const { - requestNewEmailVerificationFactory -} = require('@/modules/emails/services/verification/request') -const { - deleteOldAndInsertNewVerificationFactory -} = require('@/modules/emails/repositories') -const { renderEmail } = require('@/modules/emails/services/emailRendering') -const { sendEmail } = require('@/modules/emails/services/sending') -const { createUserFactory } = require('@/modules/core/services/users/management') -const { - validateAndCreateUserEmailFactory -} = require('@/modules/core/services/userEmails') -const { - finalizeInvitedServerRegistrationFactory -} = require('@/modules/serverinvites/services/processing') -const { getServerInfoFactory } = require('@/modules/core/repositories/server') -const { createObjectFactory } = require('@/modules/core/services/objects/management') +} from '@/modules/core/repositories/userEmails' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' +import { createUserFactory } from '@/modules/core/services/users/management' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing' +import { getServerInfoFactory } from '@/modules/core/repositories/server' +import { createObjectFactory } from '@/modules/core/services/objects/management' +import type express from 'express' +import { ResourceType } from '@/modules/comments/domain/types' +import { + CommentCreateInput, + LegacyCommentViewerData, + ReplyCreateInput +} from '@/modules/core/graph/generated/graphql' +import { CommentRecord } from '@/modules/comments/helpers/types' +import { MaybeNullOrUndefined } from '@speckle/shared' +import { CommentEvents } from '@/modules/comments/domain/events' + +type LegacyCommentRecord = CommentRecord & { + total_count: string + resources: Array<{ resourceId: string; resourceType: string }> +} const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) @@ -156,7 +148,7 @@ const createComment = createCommentFactory({ insertCommentLinks, deleteComment, markCommentViewed, - commentsEventsEmit: CommentsEmitter.emit + emitEvent: getEventBus().emit }) const createCommentReply = createCommentReplyFactory({ validateInputAttachments, @@ -165,7 +157,7 @@ const createCommentReply = createCommentReplyFactory({ checkStreamResourcesAccess: streamResourceCheck, deleteComment, markCommentUpdated: markCommentUpdatedFactory({ db }), - commentsEventsEmit: CommentsEmitter.emit + emitEvent: getEventBus().emit }) const getComment = getCommentFactory({ db }) const updateComment = updateCommentFactory({ db }) @@ -173,7 +165,7 @@ const editComment = editCommentFactory({ getComment, validateInputAttachments, updateComment: updateCommentFactory({ db }), - commentsEventsEmit: CommentsEmitter.emit + emitEvent: getEventBus().emit }) const archiveComment = archiveCommentFactory({ getComment, @@ -267,8 +259,8 @@ const createObject = createObjectFactory({ storeClosuresIfNotFound: storeClosuresIfNotFoundFactory({ db }) }) -function buildCommentInputFromString(textString) { - return convertBasicStringToDocument(textString) +function buildCommentInputFromString(textString?: string) { + return convertBasicStringToDocument(textString || crs({ length: 10 })) } function generateRandomCommentText() { @@ -279,39 +271,42 @@ const mailerMock = EmailSendingServiceMock const commentRepoMock = CommentsRepositoryMock describe('Comments @comments', () => { - /** @type {import('express').Express} */ - let app + let app: express.Express - /** @type {import('@/test/notificationsHelper').NotificationsStateManager} */ - let notificationsState + let notificationsState: NotificationsStateManager const user = { name: 'The comment wizard', email: 'comment@wizard.ry', - password: 'i did not like Rivendel wine :(' + password: 'i did not like Rivendel wine :(', + id: '' } const otherUser = { name: 'Fondalf The Brey', email: 'totalnotfakegandalf87@mordor.com', - password: 'what gandalf puts in his pipe stays in his pipe' + password: 'what gandalf puts in his pipe stays in his pipe', + id: '' } const stream = { name: 'Commented stream', - description: 'Chit chats over here' + description: 'Chit chats over here', + id: '' } const testObject1 = { - foo: 'bar' + foo: 'bar', + id: '' } const testObject2 = { foo: 'barbar', - baz: 123 + baz: 123, + id: '' } - let commitId1, commitId2 + let commitId1: string, commitId2: string before(async () => { await purgeNotifications() @@ -360,14 +355,68 @@ describe('Comments @comments', () => { commentRepoMock.resetMockedFunctions() }) + it('Should be able to create a comment and a reply', async () => { + let threadEventFired = false + const threadFakeData = { justSome: crs({ length: 10 }) } + + getEventBus().listenOnce( + CommentEvents.Created, + async (payload) => { + expect(payload.payload.comment.data).to.deep.equal(threadFakeData) + threadEventFired = true + }, + { timeout: 1000 } + ) + + const thread = await createComment({ + userId: user.id, + input: { + streamId: stream.id, + resources: [{ resourceId: commitId1, resourceType: ResourceType.Commit }], + text: convertBasicStringToDocument('fakeout'), + data: threadFakeData, + blobIds: [] + } + }) + + expect(thread).to.be.ok + expect(threadEventFired).to.be.ok + + let replyEventFired = false + const replyFakeData = { justSome: crs({ length: 10 }) } + getEventBus().listenOnce( + CommentEvents.Created, + async (payload) => { + expect(payload.payload.comment.data).to.deep.equal(replyFakeData) + expect(payload.payload.comment.parentComment).to.equal(thread.id) + replyEventFired = true + }, + { timeout: 1000 } + ) + + const reply = await createCommentReply({ + authorId: user.id, + parentCommentId: thread.id, + streamId: stream.id, + text: buildCommentInputFromString('I am an 3l1t3 hack0r; - drop tables;'), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: replyFakeData as any, + blobIds: [] + }) + + expect(reply).to.be.ok + expect(replyEventFired).to.be.ok + }) + it('Should not be allowed to comment without specifying at least one target resource', async () => { await createComment({ userId: user.id, input: { streamId: stream.id, resources: [], - text: crs({ length: 10 }), - data: { justSome: crs({ length: 10 }) } + text: convertBasicStringToDocument('fakeout'), + data: { justSome: crs({ length: 10 }) }, + blobIds: [] } }) .then(() => { @@ -384,11 +433,11 @@ describe('Comments @comments', () => { const throwawayCommentText = buildCommentInputFromString('whatever') // Stream A belongs to user - const streamA = { name: 'Stream A' } + const streamA = { name: 'Stream A', id: '' } streamA.id = await createStream({ ...streamA, ownerId: user.id }) - const objA = { foo: 'bar' } + const objA = { foo: 'bar', id: '' } objA.id = await createObject({ streamId: streamA.id, object: objA }) - const commA = {} + const commA = { id: '' } commA.id = ( await createCommitByBranchName({ streamId: streamA.id, @@ -400,11 +449,11 @@ describe('Comments @comments', () => { ).id // Stream B belongs to otherUser - const streamB = { name: 'Stream B' } + const streamB = { name: 'Stream B', id: '' } streamB.id = await createStream({ ...streamB, ownerId: otherUser.id }) - const objB = { qux: 'mux' } + const objB = { qux: 'mux', id: '' } objB.id = await createObject({ streamId: streamB.id, object: objB }) - const commB = {} + const commB = { id: '' } commB.id = ( await createCommitByBranchName({ streamId: streamB.id, @@ -420,8 +469,10 @@ describe('Comments @comments', () => { userId: user.id, input: { streamId: streamA.id, - resources: [{ resourceId: objB.id, resourceType: 'object' }], - text: throwawayCommentText + resources: [{ resourceId: objB.id, resourceType: ResourceType.Object }], + text: throwawayCommentText, + blobIds: [], + data: {} } }) .then(() => { @@ -434,8 +485,10 @@ describe('Comments @comments', () => { userId: user.id, input: { streamId: streamA.id, - resources: [{ resourceId: commB.id, resourceType: 'commit' }], - text: throwawayCommentText + resources: [{ resourceId: commB.id, resourceType: ResourceType.Commit }], + text: throwawayCommentText, + blobIds: [], + data: {} } }) .then(() => { @@ -449,10 +502,12 @@ describe('Comments @comments', () => { input: { streamId: streamA.id, resources: [ - { resourceId: commA.id, resourceType: 'commit' }, - { resourceId: commB.id, resourceType: 'commit' } + { resourceId: commA.id, resourceType: ResourceType.Commit }, + { resourceId: commB.id, resourceType: ResourceType.Commit } ], - text: throwawayCommentText + text: throwawayCommentText, + blobIds: [], + data: {} } }) .then(() => { @@ -466,12 +521,13 @@ describe('Comments @comments', () => { input: { streamId: streamA.id, resources: [ - { resourceId: commA.id, resourceType: 'commit' }, - { resourceId: commA.id, resourceType: 'commit' } + { resourceId: commA.id, resourceType: ResourceType.Commit }, + { resourceId: commA.id, resourceType: ResourceType.Commit } ], - text: throwawayCommentText - }, - text: throwawayCommentText + text: throwawayCommentText, + blobIds: [], + data: {} + } }) // replies should also not be swappable @@ -479,7 +535,9 @@ describe('Comments @comments', () => { authorId: user.id, parentCommentId: correctCommentId, streamId: streamB.id, - text: buildCommentInputFromString('I am an 3l1t3 hack0r; - drop tables;') + text: buildCommentInputFromString('I am an 3l1t3 hack0r; - drop tables;'), + data: {}, + blobIds: [] }) .then(() => { throw new Error('This should have been rejected') @@ -492,11 +550,11 @@ describe('Comments @comments', () => { }) it('Should return comment counts for streams, commits and objects', async () => { - const stream = { name: 'Bean Counter' } + const stream = { name: 'Bean Counter', id: '' } stream.id = await createStream({ ...stream, ownerId: user.id }) - const obj = { foo: 'bar' } + const obj = { foo: 'bar', id: '' } obj.id = await createObject({ streamId: stream.id, object: obj }) - const commit = {} + const commit = { id: '' } commit.id = ( await createCommitByBranchName({ streamId: stream.id, @@ -518,9 +576,11 @@ describe('Comments @comments', () => { text: buildCommentInputFromString('bar'), streamId: stream.id, resources: [ - { resourceId: commit.id, resourceType: 'commit' }, - { resourceId: obj.id, resourceType: 'object' } - ] + { resourceId: commit.id, resourceType: ResourceType.Commit }, + { resourceId: obj.id, resourceType: ResourceType.Object } + ], + blobIds: [], + data: {} } }).then((c) => c.id) ) @@ -531,7 +591,9 @@ describe('Comments @comments', () => { input: { text: buildCommentInputFromString('baz'), streamId: stream.id, - resources: [{ resourceId: commit.id, resourceType: 'commit' }] + resources: [{ resourceId: commit.id, resourceType: ResourceType.Commit }], + blobIds: [], + data: {} } }).then((c) => c.id) ) @@ -542,7 +604,9 @@ describe('Comments @comments', () => { input: { text: buildCommentInputFromString('qux'), streamId: stream.id, - resources: [{ resourceId: obj.id, resourceType: 'object' }] + resources: [{ resourceId: obj.id, resourceType: ResourceType.Object }], + blobIds: [], + data: {} } }).then((c) => c.id) ) @@ -553,19 +617,25 @@ describe('Comments @comments', () => { authorId: user.id, parentCommentId: commentIds[0], streamId: stream.id, - text: generateRandomCommentText() + text: buildCommentInputFromString(), + data: {}, + blobIds: [] }) await createCommentReply({ authorId: user.id, parentCommentId: commentIds[1], streamId: stream.id, - text: generateRandomCommentText() + text: buildCommentInputFromString(), + data: {}, + blobIds: [] }) await createCommentReply({ authorId: user.id, parentCommentId: commentIds[2], streamId: stream.id, - text: generateRandomCommentText() + text: buildCommentInputFromString(), + data: {}, + blobIds: [] }) // we archive one of the object only comments for fun and profit @@ -585,11 +655,11 @@ describe('Comments @comments', () => { const commitCount = await getResourceCommentCount({ resourceId: commit.id }) expect(commitCount).to.equal(commCount * 2) - const streamOther = { name: 'Bean Counter' } + const streamOther = { name: 'Bean Counter', id: '' } streamOther.id = await createStream({ ...streamOther, ownerId: user.id }) - const objOther = { 'are you bored': 'yes' } + const objOther = { 'are you bored': 'yes', id: '' } objOther.id = await createObject({ streamId: streamOther.id, object: objOther }) - const commitOther = {} + const commitOther = { id: '' } commitOther.id = ( await createCommitByBranchName({ streamId: streamOther.id, @@ -606,13 +676,11 @@ describe('Comments @comments', () => { expect(countOther).to.equal(0) const objCountOther = await getResourceCommentCount({ - streamId: streamOther.id, resourceId: objOther.id }) expect(objCountOther).to.equal(0) const commitCountOther = await getResourceCommentCount({ - streamId: streamOther.id, resourceId: commitOther.id }) expect(commitCountOther).to.equal(0) @@ -623,14 +691,15 @@ describe('Comments @comments', () => { userId: user.id, input: { streamId: stream.id, - resources: [{ resourceId: commitId1, resourceType: 'commit' }], + resources: [{ resourceId: commitId1, resourceType: ResourceType.Commit }], text: buildCommentInputFromString( 'https://tenor.com/view/gandalf-smoking-gif-21189890' ), // possibly NSFW data: { someMore: 'https://tenor.com/view/gandalf-old-man-naked-take-robe-off-funny-gif-17224126' - } // possibly NSFW + }, // possibly NSFW + blobIds: [] } }) @@ -642,7 +711,7 @@ describe('Comments @comments', () => { expect(commentNoUser).to.not.haveOwnProperty('viewedAt') const commentOtherUser = await getComment({ id, userId: otherUser.id }) - expect(commentOtherUser.viewedAt).to.be.null + expect(commentOtherUser!.viewedAt).to.be.null await markCommentViewed(id, user.id) @@ -656,13 +725,14 @@ describe('Comments @comments', () => { input: { streamId: stream.id, resources: [ - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: commitId1, resourceType: 'commit' }, - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: testObject1.id, resourceType: 'object' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: commitId1, resourceType: ResourceType.Commit }, + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: testObject1.id, resourceType: ResourceType.Object } ], - text: crs({ length: 10 }), - data: { justSome: crs({ length: 10 }) } + text: buildCommentInputFromString(), + data: { justSome: crs({ length: 10 }) }, + blobIds: [] } }) .then(() => { @@ -679,15 +749,17 @@ describe('Comments @comments', () => { const nonExistentResources = [ { streamId: 'this doesnt exist dummy', - resources: [{ resourceId: 'this doesnt exist dummy', resourceType: 'stream' }], + resources: [ + { resourceId: 'this doesnt exist dummy', resourceType: ResourceType.Stream } + ], text: null, data: null }, { streamId: stream.id, resources: [ - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: 'this doesnt exist dummy', resourceType: 'commit' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: 'this doesnt exist dummy', resourceType: ResourceType.Commit } ], text: null, data: null @@ -695,8 +767,8 @@ describe('Comments @comments', () => { { streamId: stream.id, resources: [ - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: 'this doesnt exist dummy', resourceType: 'object' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: 'this doesnt exist dummy', resourceType: ResourceType.Object } ], text: null, data: null @@ -704,15 +776,18 @@ describe('Comments @comments', () => { { streamId: stream.id, resources: [ - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: 'this doesnt exist dummy', resourceType: 'comment' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: 'this doesnt exist dummy', resourceType: ResourceType.Comment } ], text: null, data: null } ] for (const input of nonExistentResources) { - await createComment({ userId: user.id, input }) + await createComment({ + userId: user.id, + input: input as unknown as CommentCreateInput + }) .then(() => { throw new Error('This should have been rejected') }) @@ -726,11 +801,15 @@ describe('Comments @comments', () => { input: { streamId: stream.id, resources: [ - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: 'jubbjabb', resourceType: 'flux capacitor' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { + resourceId: 'jubbjabb', + resourceType: 'flux capacitor' as unknown as ResourceType + } ], - text: crs({ length: 10 }), - data: { justSome: crs({ length: 10 }) } + text: buildCommentInputFromString(), + data: { justSome: crs({ length: 10 }) }, + blobIds: [] } }) .then(() => { @@ -741,40 +820,40 @@ describe('Comments @comments', () => { it('Should be able to comment on valid resources in any permutation', async () => { const resourceCombinations = [ - [{ resourceId: stream.id, resourceType: 'stream' }], + [{ resourceId: stream.id, resourceType: ResourceType.Stream }], [ - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: commitId1, resourceType: 'commit' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: commitId1, resourceType: ResourceType.Commit } ], [ - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: commitId1, resourceType: 'commit' }, - { resourceId: testObject1.id, resourceType: 'object' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: commitId1, resourceType: ResourceType.Commit }, + { resourceId: testObject1.id, resourceType: ResourceType.Object } ], [ // object overlay on object - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: testObject1.id, resourceType: 'object' }, - { resourceId: testObject2.id, resourceType: 'object' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: testObject1.id, resourceType: ResourceType.Object }, + { resourceId: testObject2.id, resourceType: ResourceType.Object } ], [ // an object overlayed on a commit - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: commitId1, resourceType: 'commit' }, - { resourceId: testObject2.id, resourceType: 'object' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: commitId1, resourceType: ResourceType.Commit }, + { resourceId: testObject2.id, resourceType: ResourceType.Object } ], [ // an object overlayed on a commit - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: commitId1, resourceType: 'commit' }, - { resourceId: testObject1.id, resourceType: 'object' }, - { resourceId: testObject2.id, resourceType: 'object' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: commitId1, resourceType: ResourceType.Commit }, + { resourceId: testObject1.id, resourceType: ResourceType.Object }, + { resourceId: testObject2.id, resourceType: ResourceType.Object } ], [ - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: commitId1, resourceType: 'commit' }, - { resourceId: commitId2, resourceType: 'commit' }, - { resourceId: testObject1.id, resourceType: 'object' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: commitId1, resourceType: ResourceType.Commit }, + { resourceId: commitId2, resourceType: ResourceType.Commit }, + { resourceId: testObject1.id, resourceType: ResourceType.Object } ] ] @@ -786,7 +865,8 @@ describe('Comments @comments', () => { streamId: stream.id, resources, text: generateRandomCommentText(), - data: { justSome: crs({ length: 10 }) } + data: { justSome: crs({ length: 10 }) }, + blobIds: [] } }) expect(commentId).to.exist @@ -806,12 +886,13 @@ describe('Comments @comments', () => { input: { streamId: stream.id, resources: [ - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: commitId1, resourceType: 'commit' }, - { resourceId: localObjectId, resourceType: 'object' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: commitId1, resourceType: ResourceType.Commit }, + { resourceId: localObjectId, resourceType: ResourceType.Object } ], text: generateRandomCommentText(), - data: { justSome: 'distinct test' + crs({ length: 10 }) } + data: { justSome: 'distinct test' + crs({ length: 10 }) }, + blobIds: [] } }) } @@ -819,8 +900,8 @@ describe('Comments @comments', () => { const comments = await getComments({ streamId: stream.id, resources: [ - { resourceId: commitId1, resourceType: 'commit' }, - { resourceId: localObjectId, resourceType: 'object' } + { resourceId: commitId1, resourceType: ResourceType.Commit }, + { resourceId: localObjectId, resourceType: ResourceType.Object } ] }) @@ -853,12 +934,13 @@ describe('Comments @comments', () => { input: { streamId: stream.id, resources: [ - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: commitId1, resourceType: 'commit' }, - { resourceId: localObjectId, resourceType: 'object' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: commitId1, resourceType: ResourceType.Commit }, + { resourceId: localObjectId, resourceType: ResourceType.Object } ], text: generateRandomCommentText(), - data: { justSome: crs({ length: 10 }) } + data: { justSome: crs({ length: 10 }) }, + blobIds: [] } }).then((c) => c.id) ) @@ -868,13 +950,13 @@ describe('Comments @comments', () => { let comments = await getComments({ streamId: stream.id, resources: [ - { resourceId: commitId1, resourceType: 'commit' }, - { resourceId: localObjectId, resourceType: 'object' } + { resourceId: commitId1, resourceType: ResourceType.Commit }, + { resourceId: localObjectId, resourceType: ResourceType.Object } ], limit: 2 }) expect(comments.items).to.have.lengthOf(2) - expect(createdComments.reverse().slice(0, 2)).deep.to.equal( + expect(createdComments.reverse().slice(0, 2)).to.deep.equal( comments.items.map((c) => c.id) ) // note: reversing as default order is newest first now @@ -882,14 +964,14 @@ describe('Comments @comments', () => { comments = await getComments({ streamId: stream.id, resources: [ - { resourceId: commitId1, resourceType: 'commit' }, - { resourceId: localObjectId, resourceType: 'object' } + { resourceId: commitId1, resourceType: ResourceType.Commit }, + { resourceId: localObjectId, resourceType: ResourceType.Object } ], limit: 2, - cursor + cursor: cursor.toISOString() }) expect(comments.items).to.have.lengthOf(2) - expect(createdComments.slice(2, 4)).deep.to.equal(comments.items.map((c) => c.id)) + expect(createdComments.slice(2, 4)).to.deep.equal(comments.items.map((c) => c.id)) }) it('Should properly return replies for a comment', async () => { @@ -897,9 +979,10 @@ describe('Comments @comments', () => { userId: user.id, input: { streamId: stream.id, - resources: [{ resourceId: stream.id, resourceType: 'stream' }], + resources: [{ resourceId: stream.id, resourceType: ResourceType.Stream }], text: generateRandomCommentText(), - data: { justSome: crs({ length: 10 }) } + data: { justSome: crs({ length: 10 }) }, + blobIds: [] } }) @@ -907,24 +990,26 @@ describe('Comments @comments', () => { authorId: user.id, parentCommentId: streamCommentId1, streamId: stream.id, - text: generateRandomCommentText(), - data: { justSome: crs({ length: 10 }) } + text: buildCommentInputFromString(), + data: { justSome: crs({ length: 10 }) } as unknown as LegacyCommentViewerData, + blobIds: [] }) const { id: commentId2 } = await createCommentReply({ authorId: user.id, parentCommentId: streamCommentId1, streamId: stream.id, - text: generateRandomCommentText(), - data: { justSome: crs({ length: 10 }) } + text: buildCommentInputFromString(), + data: { justSome: crs({ length: 10 }) } as unknown as LegacyCommentViewerData, + blobIds: [] }) const replies = await getComments({ streamId: stream.id, replies: true, - resources: [{ resourceId: streamCommentId1, resourceType: 'comment' }] + resources: [{ resourceId: streamCommentId1, resourceType: ResourceType.Comment }] }) expect(replies.items).to.have.lengthOf(2) - expect(replies.items.reverse().map((i) => i.id)).deep.to.equal([ + expect(replies.items.reverse().map((i) => i.id)).to.deep.equal([ commentId1, commentId2 ]) @@ -936,14 +1021,14 @@ describe('Comments @comments', () => { object: { anotherTestObject: 1 } }) const inputResources = [ - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: commitId1, resourceType: 'commit' }, - { resourceId: localObjectId, resourceType: 'object' }, - { resourceId: testObject2.id, resourceType: 'object' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: commitId1, resourceType: ResourceType.Commit }, + { resourceId: localObjectId, resourceType: ResourceType.Object }, + { resourceId: testObject2.id, resourceType: ResourceType.Object } ] const queryResources = [ - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: localObjectId, resourceType: 'object' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: localObjectId, resourceType: ResourceType.Object } ] await createComment({ userId: user.id, @@ -951,7 +1036,8 @@ describe('Comments @comments', () => { streamId: stream.id, resources: inputResources, text: generateRandomCommentText(), - data: { justSome: crs({ length: 10 }) } + data: { justSome: crs({ length: 10 }) }, + blobIds: [] } }) @@ -974,19 +1060,22 @@ describe('Comments @comments', () => { input: { streamId: stream.id, resources: [ - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: localObjectId, resourceType: 'object' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: localObjectId, resourceType: ResourceType.Object } ], text: generateRandomCommentText(), - data: { justSome: crs({ length: 10 }) } + data: { justSome: crs({ length: 10 }) }, + blobIds: [] } }) const comments = await getComments({ streamId: stream.id, - resources: [{ resourceId: localObjectId, resourceType: 'object' }] + resources: [{ resourceId: localObjectId, resourceType: ResourceType.Object }] }) expect(comments.items).to.have.lengthOf(1) - const [firstComment] = comments.items + const firstComment: Record & { id: string } = { + ...comments.items[0] + } const comment = await getComment({ id: firstComment.id }) // the getComments query brings along some extra garbage i'm lazy to clean up @@ -995,7 +1084,7 @@ describe('Comments @comments', () => { delete firstComment.resourceId delete firstComment.commentId - expect(comment).deep.to.equal(firstComment) + expect(comment).to.deep.equal(firstComment) }) it('Should be able to edit a comment text', async () => { @@ -1005,31 +1094,58 @@ describe('Comments @comments', () => { anotherTestObject: crs({ length: 10 }) } }) + + const baseText = generateRandomCommentText() const { id: commentId } = await createComment({ userId: user.id, input: { streamId: stream.id, resources: [ - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: localObjectId, resourceType: 'object' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: localObjectId, resourceType: ResourceType.Object } ], - text: generateRandomCommentText(), - data: { justSome: crs({ length: 10 }) } + text: baseText, + data: { justSome: crs({ length: 10 }) }, + blobIds: [] } }) - const properText = buildCommentInputFromString('now thats what im talking about') - await editComment({ userId: user.id, input: { id: commentId, text: properText } }) + let editEventFired = true + const newText = buildCommentInputFromString('now thats what im talking about') + getEventBus().listenOnce( + CommentEvents.Updated, + async (payload) => { + expect(payload.payload.newComment.id).to.eq(commentId) + expect(payload.payload.previousComment.id).to.eq(commentId) + + expect( + ensureCommentSchema(payload.payload.previousComment.text!).doc + ).to.deep.equal(baseText) + expect(ensureCommentSchema(payload.payload.newComment.text!).doc).to.deep.equal( + newText + ) + editEventFired = true + }, + { timeout: 1000 } + ) + + await editComment({ + userId: user.id, + input: { id: commentId, text: newText, blobIds: [], streamId: stream.id }, + matchUser: true + }) const comment = await getComment({ id: commentId }) - const commentTextSchema = ensureCommentSchema(comment.text) + const commentTextSchema = ensureCommentSchema(comment!.text || '') - expect(commentTextSchema.doc).to.deep.equal(properText) + expect(commentTextSchema.doc).to.deep.equal(newText) + expect(editEventFired).to.be.true }) it('Should not be allowed to edit a not existing comment', async () => { await editComment({ userId: user.id, - input: { id: 'this is not going to be found' } + input: { id: 'this is not going to be found', blobIds: [], streamId: 'a' }, + matchUser: true }) .then(() => { throw new Error('This should have been rejected') @@ -1049,17 +1165,23 @@ describe('Comments @comments', () => { input: { streamId: stream.id, resources: [ - { resourceId: stream.id, resourceType: 'stream' }, - { resourceId: localObjectId, resourceType: 'object' } + { resourceId: stream.id, resourceType: ResourceType.Stream }, + { resourceId: localObjectId, resourceType: ResourceType.Object } ], text: generateRandomCommentText(), - data: { justSome: crs({ length: 10 }) } + data: { justSome: crs({ length: 10 }) }, + blobIds: [] } }) await editComment({ userId: otherUser.id, - input: { id: commentId, text: generateRandomCommentText() }, + input: { + id: commentId, + text: generateRandomCommentText(), + blobIds: [], + streamId: stream.id + }, matchUser: true }) .then(() => { @@ -1069,9 +1191,13 @@ describe('Comments @comments', () => { expect(error.message).to.be.equal("You cannot edit someone else's comments") ) const properText = buildCommentInputFromString('fooood') - await editComment({ userId: user.id, input: { id: commentId, text: properText } }) + await editComment({ + userId: user.id, + input: { id: commentId, text: properText, blobIds: [], streamId: '' }, + matchUser: true + }) const comment = await getComment({ id: commentId }) - const commentText = ensureCommentSchema(comment.text) + const commentText = ensureCommentSchema(comment!.text || '') expect(commentText.doc).to.deep.equalInAnyOrder(properText) }) @@ -1080,19 +1206,25 @@ describe('Comments @comments', () => { userId: user.id, input: { streamId: stream.id, - resources: [{ resourceId: stream.id, resourceType: 'stream' }], + resources: [{ resourceId: stream.id, resourceType: ResourceType.Stream }], text: generateRandomCommentText(), - data: { justSome: crs({ length: 10 }) } + data: { justSome: crs({ length: 10 }) }, + blobIds: [] } }) let comment = await getComment({ id: commentId }) - expect(comment.archived).to.equal(false) + expect(comment!.archived).to.equal(false) - await archiveComment({ streamId: stream.id, commentId, userId: user.id }) + await archiveComment({ + streamId: stream.id, + commentId, + userId: user.id, + archived: true + }) comment = await getComment({ id: commentId }) - expect(comment.archived).to.equal(true) + expect(comment!.archived).to.equal(true) await archiveComment({ streamId: stream.id, @@ -1102,14 +1234,15 @@ describe('Comments @comments', () => { }) comment = await getComment({ id: commentId }) - expect(comment.archived).to.equal(false) + expect(comment!.archived).to.equal(false) }) it('Should not be allowed to archive a not existing comment', async () => { await archiveComment({ commentId: 'badabumm', streamId: stream.id, - userId: user.id + userId: user.id, + archived: true }) .then(() => { throw new Error('This should have been rejected') @@ -1126,13 +1259,19 @@ describe('Comments @comments', () => { userId: user.id, input: { streamId: stream.id, - resources: [{ resourceId: stream.id, resourceType: 'stream' }], + resources: [{ resourceId: stream.id, resourceType: ResourceType.Stream }], text: generateRandomCommentText(), - data: { justSome: crs({ length: 10 }) } + data: { justSome: crs({ length: 10 }) }, + blobIds: [] } }) - await archiveComment({ commentId, streamId: stream.id, userId: otherUser.id }) + await archiveComment({ + commentId, + streamId: stream.id, + userId: otherUser.id, + archived: true + }) .then(() => { throw new Error('This should have been rejected') }) @@ -1146,15 +1285,17 @@ describe('Comments @comments', () => { userId: otherUser.id, input: { streamId: stream.id, - resources: [{ resourceId: stream.id, resourceType: 'stream' }], + resources: [{ resourceId: stream.id, resourceType: ResourceType.Stream }], text: generateRandomCommentText(), - data: { justSome: crs({ length: 10 }) } + data: { justSome: crs({ length: 10 }) }, + blobIds: [] } }) const archiveResult = await archiveComment({ commentId: otherUsersCommentId, userId: user.id, - streamId: stream.id + streamId: stream.id, + archived: true }) expect(archiveResult).to.be.ok }) @@ -1174,9 +1315,12 @@ describe('Comments @comments', () => { userId: user.id, input: { streamId: stream.id, - resources: [{ resourceId: localObjectId, resourceType: 'object' }], + resources: [ + { resourceId: localObjectId, resourceType: ResourceType.Object } + ], text: generateRandomCommentText(), - data: { justSome: crs({ length: 10 }) } + data: { justSome: crs({ length: 10 }) }, + blobIds: [] } }) ) @@ -1185,20 +1329,25 @@ describe('Comments @comments', () => { const archiveCount = 3 let comments = await getComments({ streamId: stream.id, - resources: [{ resourceId: localObjectId, resourceType: 'object' }], + resources: [{ resourceId: localObjectId, resourceType: ResourceType.Object }], limit: archiveCount }) expect(comments.totalCount).to.be.equal(commentCount) await Promise.all( comments.items.map((comment) => - archiveComment({ commentId: comment.id, streamId: stream.id, userId: user.id }) + archiveComment({ + commentId: comment.id, + streamId: stream.id, + userId: user.id, + archived: true + }) ) ) comments = await getComments({ streamId: stream.id, - resources: [{ resourceId: localObjectId, resourceType: 'object' }], + resources: [{ resourceId: localObjectId, resourceType: ResourceType.Object }], limit: 100 }) expect(comments.totalCount).to.be.equal(commentCount - archiveCount) @@ -1206,7 +1355,7 @@ describe('Comments @comments', () => { comments = await getComments({ streamId: stream.id, - resources: [{ resourceId: localObjectId, resourceType: 'object' }], + resources: [{ resourceId: localObjectId, resourceType: ResourceType.Object }], limit: 100, archived: true }) @@ -1220,23 +1369,23 @@ describe('Comments @comments', () => { userId: user.id, input: { streamId: stream.id, - resources: [{ resourceId: stream.id, resourceType: 'stream' }], + resources: [{ resourceId: stream.id, resourceType: ResourceType.Stream }], text: novelValue, - data: { justSome: crs({ length: 10 }) } + data: { justSome: crs({ length: 10 }) }, + blobIds: [] } }) const comment = await getComment({ id: commentId }) - const commentText = ensureCommentSchema(comment.text) + const commentText = ensureCommentSchema(comment!.text || '') expect(commentText.doc).to.deep.equal(novelValue) }) describe('when authenticated', () => { - /** @type {import('@/test/graphqlHelper').ServerAndContext} */ - let apollo - let userToken - let blob1 + let apollo: ServerAndContext + let userToken: string + let blob1: UploadedBlob before(async () => { const scopes = AllScopes @@ -1265,25 +1414,26 @@ describe('Comments @comments', () => { CommentsGraphQLClient.createComment(apollo, { input: { streamId: stream.id, - resources: [{ resourceId: commitId1, resourceType: 'commit' }], + resources: [{ resourceId: commitId1, resourceType: ResourceType.Commit }], data: {}, blobIds: [], ...input } }) - const createReply = (input = {}) => + const createReply = (input?: ReplyCreateInput) => CommentsGraphQLClient.createReply(apollo, { input: { streamId: stream.id, blobIds: [], + parentComment: '', ...input } }) describe('when reading comments', () => { - let parentCommentId - let emptyCommentId + let parentCommentId: string + let emptyCommentId: string before(async () => { // Truncate comments @@ -1294,29 +1444,31 @@ describe('Comments @comments', () => { text: generateRandomCommentText(), blobIds: [blob1.blobId] }) - parentCommentId = createCommentResult.data.commentCreate + parentCommentId = createCommentResult.data!.commentCreate if (!parentCommentId) throw new Error('Comment creation failed!') // Create a reply with a blob await createReply({ text: generateRandomCommentText(), blobIds: [blob1.blobId], - parentComment: parentCommentId + parentComment: parentCommentId, + streamId: stream.id }) // Create a reply with a blob, but no text const emptyCommentResult = await createReply({ blobIds: [blob1.blobId], - parentComment: parentCommentId + parentComment: parentCommentId, + streamId: stream.id }) - emptyCommentId = emptyCommentResult.data.commentReply + emptyCommentId = emptyCommentResult.data!.commentReply if (!emptyCommentId) throw new Error('Comment creation failed!') }) - const readComment = (input = {}) => + const readComment = (input?: { id: string }) => CommentsGraphQLClient.getComment(apollo, { streamId: stream.id, - ...input + ...(input || { id: '' }) }) const readComments = (input = {}) => @@ -1329,7 +1481,7 @@ describe('Comments @comments', () => { it('both legacy (string) comments and new (ProseMirror) documents are formatted as SmartTextEditorValue values', async () => { commentRepoMock.enable() commentRepoMock.mockFunction('getCommentsLegacyFactory', () => { - return () => ({ + return async () => ({ items: [ // Legacy { @@ -1355,7 +1507,7 @@ describe('Comments @comments', () => { }), streamId: stream.id } - ], + ] as unknown as Array, cursor: new Date().toISOString(), totalCount: 3 }) @@ -1372,10 +1524,10 @@ describe('Comments @comments', () => { id: '1', text: 'https://aaa.com:3000/h3ll0-world/_?a=1&b=2#aaa', streamId: stream.id - } + } as unknown as LegacyCommentRecord commentRepoMock.enable() - commentRepoMock.mockFunction('getCommentsLegacyFactory', () => () => ({ + commentRepoMock.mockFunction('getCommentsLegacyFactory', () => async () => ({ items: [item], cursor: new Date().toISOString(), totalCount: 1 @@ -1386,7 +1538,7 @@ describe('Comments @comments', () => { expect(data?.comments?.items?.length || 0).to.eq(1) expect(errors?.length || 0).to.eq(0) - const textNode = data.comments.items[0].text.doc.content[0].content[0] + const textNode = get(data, 'comments.items[0].text.doc.content[0].content[0]') expect(textNode.text).to.eq(item.text) expect(textNode.marks).to.deep.equalInAnyOrder([ { @@ -1412,10 +1564,10 @@ describe('Comments @comments', () => { id: '1', text: textParts.join(''), streamId: stream.id - } + } as unknown as LegacyCommentRecord commentRepoMock.enable() - commentRepoMock.mockFunction('getCommentsLegacyFactory', () => () => ({ + commentRepoMock.mockFunction('getCommentsLegacyFactory', () => async () => ({ items: [item], cursor: new Date().toISOString(), totalCount: 1 @@ -1423,7 +1575,7 @@ describe('Comments @comments', () => { const { data, errors } = await readComments() - const runExpectationsOnTextNode = (idx, shouldBeLink) => { + const runExpectationsOnTextNode = (idx: number, shouldBeLink: boolean) => { expect(textNodes[idx].text).to.eq(textParts[idx]) if (shouldBeLink) { @@ -1439,7 +1591,7 @@ describe('Comments @comments', () => { expect(data?.comments?.items?.length || 0).to.eq(1) expect(errors?.length || 0).to.eq(0) - const textNodes = data.comments.items[0].text.doc.content[0].content + const textNodes = get(data, 'comments.items[0].text.doc.content[0].content') expect(textNodes.length).to.eq(textParts.length) range(textParts.length).forEach((i) => { @@ -1460,33 +1612,31 @@ describe('Comments @comments', () => { // Check first comment expect(data?.comments?.items?.length || 0).to.eq(1) - expect(data.comments.items[0].text?.attachments?.length || 0).to.eq(1) - expect(data.comments.items[0].text.attachments[0]).to.deep.equalInAnyOrder( + expect(data?.comments?.items[0].text?.attachments?.length || 0).to.eq(1) + expect(data?.comments?.items[0].text?.attachments?.[0]).to.deep.equalInAnyOrder( expectedMetadata ) // Check first reply - expect(data.comments.items[0].replies?.items?.length || 0).to.eq(2) + expect(data?.comments?.items[0].replies?.items?.length || 0).to.eq(2) expect( - data.comments.items[0].replies.items[0].text?.attachments?.length || 0 + data?.comments?.items[0].replies.items[0].text?.attachments?.length || 0 ).to.eq(1) expect( - data.comments.items[0].replies.items[0].text?.attachments[0] + data?.comments?.items[0].replies.items[0].text?.attachments?.[0] ).to.deep.equalInAnyOrder(expectedMetadata) // Check 2nd reply expect( - data.comments.items[0].replies.items[1].text?.attachments?.length || 0 + data?.comments?.items[0].replies.items[1].text?.attachments?.length || 0 ).to.eq(1) expect( - data.comments.items[0].replies.items[1].text?.attachments[0] + data?.comments?.items[0].replies.items[1].text?.attachments?.[0] ).to.deep.equalInAnyOrder(expectedMetadata) }) it('returns raw text correctly', async () => { - const { - data: { commentReply: commentId } - } = await createReply({ + const { data } = await createReply({ text: { type: 'doc', content: [ @@ -1501,8 +1651,10 @@ describe('Comments @comments', () => { ] }, blobIds: [], - parentComment: parentCommentId + parentComment: parentCommentId, + streamId: stream.id }) + const commentId = data?.commentReply || '' const results = await readComment({ id: commentId @@ -1517,9 +1669,9 @@ describe('Comments @comments', () => { }) expect(errors?.length || 0).to.eq(0) - expect(data.comment).to.be.ok - expect(data.comment.text.doc).to.be.null - expect(data.comment.text.attachments.length).to.be.greaterThan(0) + expect(data?.comment).to.be.ok + expect(data?.comment?.text.doc).to.be.null + expect(data?.comment?.text.attachments?.length).to.be.greaterThan(0) }) const unexpectedValDataset = [ @@ -1531,10 +1683,10 @@ describe('Comments @comments', () => { const item = { id: '1', text: value - } + } as unknown as LegacyCommentRecord commentRepoMock.enable() - commentRepoMock.mockFunction('getCommentsLegacyFactory', () => () => ({ + commentRepoMock.mockFunction('getCommentsLegacyFactory', () => async () => ({ items: [item], cursor: new Date().toISOString(), totalCount: 1 @@ -1555,17 +1707,21 @@ describe('Comments @comments', () => { { creating: true, display: 'creating a new comment thread' } ] creatingOrReplyingDataSet.forEach(({ replying, creating, display }) => { - let parentCommentId + let parentCommentId: string const createOrReplyComment = (input = {}) => creating ? createComment(input) : createReply({ parentComment: parentCommentId, + blobIds: [], + streamId: stream.id, ...input }) - const getResult = (data) => (creating ? data?.commentCreate : data?.commentReply) + const getResult = ( + data: MaybeNullOrUndefined<{ commentCreate?: unknown; commentReply?: unknown }> + ) => (creating ? data?.commentCreate : data?.commentReply) describe(`when ${display}`, () => { before(async () => { @@ -1575,7 +1731,7 @@ describe('Comments @comments', () => { text: generateRandomCommentText() }) - parentCommentId = data.commentCreate + parentCommentId = data!.commentCreate if (!parentCommentId) { throw new Error("Couldn't successfully create comment for tests!") } @@ -1666,7 +1822,7 @@ describe('Comments @comments', () => { }) describe('and mentioning a user', () => { - const createOrReplyCommentWithMention = (targetUserId, input = {}) => + const createOrReplyCommentWithMention = (targetUserId: string, input = {}) => createOrReplyComment({ text: { type: 'doc', diff --git a/packages/server/modules/comments/tests/projectComments.spec.ts b/packages/server/modules/comments/tests/projectComments.spec.ts index 44382bfd79..a7a8dfff29 100644 --- a/packages/server/modules/comments/tests/projectComments.spec.ts +++ b/packages/server/modules/comments/tests/projectComments.spec.ts @@ -1,9 +1,20 @@ +import { CommentEvents } from '@/modules/comments/domain/events' +import { commentTextToRawString } from '@/modules/comments/services/commentTextService' +import { getEventBus } from '@/modules/shared/services/eventBus' import { BasicTestUser, createTestUsers } from '@/test/authHelper' import { CreateCommentInput, - CreateProjectCommentDocument + CreateCommentReplyInput, + CreateProjectCommentDocument, + CreateProjectCommentReplyDocument, + EditCommentInput, + EditProjectCommentDocument } from '@/test/graphql/generated/graphql' -import { testApolloServer, TestApolloServer } from '@/test/graphqlHelper' +import { + ExecuteOperationOptions, + testApolloServer, + TestApolloServer +} from '@/test/graphqlHelper' import { beforeEachContext } from '@/test/hooks' import { BasicTestBranch, @@ -61,31 +72,128 @@ describe('Project Comments', () => { }) }) - const createProjectComment = async (input: CreateCommentInput) => - await apollo.execute(CreateProjectCommentDocument, { input }) + const createProjectComment = async ( + input: CreateCommentInput, + options?: ExecuteOperationOptions + ) => await apollo.execute(CreateProjectCommentDocument, { input }, options) + + const createProjectCommentReply = async (input: CreateCommentReplyInput) => + await apollo.execute(CreateProjectCommentReplyDocument, { input }) + + const editProjectComment = async (input: EditCommentInput) => + await apollo.execute(EditProjectCommentDocument, { input }) + + it('can be created and replied to', async () => { + const parentText = 'hello world111' + + let createEventFired = false + getEventBus().listenOnce( + CommentEvents.Created, + ({ payload }) => { + expect(commentTextToRawString(payload.comment.text)).to.equal(parentText) + createEventFired = true + }, + { timeout: 1000 } + ) - it('can be created', async () => { - const input: CreateCommentInput = { + const threadInput: CreateCommentInput = { projectId: myStream.id, resourceIdString: resourceUrlBuilder() .addModel(myBranch.id, myCommit.id) .toString(), content: { - doc: RichTextEditor.convertBasicStringToDocument('hello world') + doc: RichTextEditor.convertBasicStringToDocument(parentText) } } - const res = await createProjectComment(input) + const res1 = await createProjectComment(threadInput) + const threadId = res1.data?.commentMutations.create.id - expect(res).to.not.haveGraphQLErrors() - expect(res.data?.commentMutations.create.id).to.be.ok - expect(res.data?.commentMutations.create.rawText).to.equal('hello world') - expect(res.data?.commentMutations.create.text.doc).to.be.ok - expect(res.data?.commentMutations.create.authorId).to.equal(me.id) + expect(res1).to.not.haveGraphQLErrors() + expect(threadId).to.be.ok + expect(res1.data?.commentMutations.create.rawText).to.equal(parentText) + expect(res1.data?.commentMutations.create.text.doc).to.be.ok + expect(res1.data?.commentMutations.create.authorId).to.equal(me.id) + expect(createEventFired).to.be.true }) - describe('after creation', () => { - it.skip('can be retrieved through Project.comment') + describe('after creation', async () => { + let threadId: string + + before(async () => { + const res = await createProjectComment( + { + projectId: myStream.id, + resourceIdString: resourceUrlBuilder() + .addModel(myBranch.id, myCommit.id) + .toString(), + content: { + doc: RichTextEditor.convertBasicStringToDocument('some rando text lol') + } + }, + { assertNoErrors: true } + ) + expect(res.data?.commentMutations.create.id).to.be.ok + threadId = res.data!.commentMutations.create.id + }) + + it('can be replied to', async () => { + const replyText = 'hello again bozo222' + let replyEventFired = false + getEventBus().listenOnce( + CommentEvents.Created, + ({ payload }) => { + expect(commentTextToRawString(payload.comment.text)).to.equal(replyText) + expect(payload.comment.parentComment).to.equal(threadId) + replyEventFired = true + }, + { timeout: 1000 } + ) + + const replyInput: CreateCommentReplyInput = { + projectId: myStream.id, + threadId: threadId!, + content: { + doc: RichTextEditor.convertBasicStringToDocument(replyText) + } + } + const res2 = await createProjectCommentReply(replyInput) + + expect(res2).to.not.haveGraphQLErrors() + expect(res2.data?.commentMutations.reply.rawText).to.equal(replyText) + expect(res2.data?.commentMutations.reply.text.doc).to.be.ok + expect(res2.data?.commentMutations.reply.authorId).to.equal(me.id) + expect(replyEventFired).to.be.true + }) + + it('can be edited', async () => { + const newText = 'new text here!!!' + let editEventFired = false + + getEventBus().listenOnce( + CommentEvents.Updated, + ({ payload }) => { + expect(commentTextToRawString(payload.newComment.text)).to.equal(newText) + expect(payload.newComment.id).to.equal(threadId) + editEventFired = true + }, + { timeout: 1000 } + ) + + const res = await editProjectComment({ + commentId: threadId, + content: { + doc: RichTextEditor.convertBasicStringToDocument(newText) + }, + projectId: myStream.id + }) + + expect(res).to.not.haveGraphQLErrors() + expect(res.data?.commentMutations.edit.rawText).to.equal(newText) + expect(res.data?.commentMutations.edit.text.doc).to.be.ok + expect(res.data?.commentMutations.edit.authorId).to.equal(me.id) + expect(editEventFired).to.be.true + }) }) }) }) diff --git a/packages/server/modules/cross-server-sync/index.ts b/packages/server/modules/cross-server-sync/index.ts index c7d09c00cb..99c428765c 100644 --- a/packages/server/modules/cross-server-sync/index.ts +++ b/packages/server/modules/cross-server-sync/index.ts @@ -8,7 +8,6 @@ import { } from '@/modules/activitystream/services/commentActivity' import { addCommitCreatedActivityFactory } from '@/modules/activitystream/services/commitActivity' import { getBlobsFactory } from '@/modules/blobstorage/repositories' -import { CommentsEmitter } from '@/modules/comments/events/emitter' import { getCommentFactory, getCommentsResourcesFactory, @@ -122,7 +121,7 @@ const crossServerSyncModule: SpeckleModule = { insertComments, insertCommentLinks, markCommentViewed, - commentsEventsEmit: CommentsEmitter.emit, + emitEvent: getEventBus().emit, addCommentCreatedActivity: addCommentCreatedActivityFactory({ getViewerResourcesFromLegacyIdentifiers, getViewerResourceItemsUngrouped, @@ -136,7 +135,7 @@ const crossServerSyncModule: SpeckleModule = { insertComments, insertCommentLinks, markCommentUpdated: markCommentUpdatedFactory({ db }), - commentsEventsEmit: CommentsEmitter.emit, + emitEvent: getEventBus().emit, addReplyAddedActivity: addReplyAddedActivityFactory({ getViewerResourcesForComment: getViewerResourcesForCommentFactory({ getCommentsResources: getCommentsResourcesFactory({ db }), diff --git a/packages/server/modules/shared/services/eventBus.ts b/packages/server/modules/shared/services/eventBus.ts index e4e4d3d0f5..d2ac819383 100644 --- a/packages/server/modules/shared/services/eventBus.ts +++ b/packages/server/modules/shared/services/eventBus.ts @@ -35,6 +35,10 @@ import { accessRequestEventsNamespace, AccessRequestEventsPayloads } from '@/modules/accessrequests/domain/events' +import { + commentEventsNamespace, + CommentEventsPayloads +} from '@/modules/comments/domain/events' type AllEventsWildcard = '**' type EventWildcard = '*' @@ -60,6 +64,7 @@ type EventsByNamespace = { [userEventsNamespace]: UserEventsPayloads [versionEventsNamespace]: VersionEventsPayloads [accessRequestEventsNamespace]: AccessRequestEventsPayloads + [commentEventsNamespace]: CommentEventsPayloads } type EventTypes = UnionToIntersection @@ -114,7 +119,7 @@ export type EventPayload = T extends AllEventsWi export function initializeEventBus() { const emitter = new EventEmitter({ wildcard: true }) - return { + const core = { /** * Emit a module event. This function must be awaited to ensure all listeners * execute. Any errors thrown in the listeners will bubble up and throw from @@ -156,6 +161,44 @@ export function initializeEventBus() { emitter.removeAllListeners() } } + + // Extra utils + const listenOnce = ( + eventName: K, + handler: (event: EventPayload) => MaybeAsync, + options?: Partial<{ + /** + * Timeout in milliseconds after which the listener will be removed even if it never fires + * (useful in tests for cleanup) + */ + timeout: number + }> + ) => { + const removeListener = core.listen(eventName, async (event) => { + try { + await handler(event) + } finally { + removeListener() + } + }) + + if (options?.timeout) { + setTimeout(removeListener, options.timeout) + } + + return removeListener + } + + return { + ...core, + /** + * Listen for module events only once. Any errors thrown here will bubble out of where + * emit() was invoked. + * + * @returns Callback for stopping listening + */ + listenOnce + } } export type EventBus = ReturnType diff --git a/packages/server/test/blobHelper.js b/packages/server/test/blobHelper.js deleted file mode 100644 index e9c2c60673..0000000000 --- a/packages/server/test/blobHelper.js +++ /dev/null @@ -1,28 +0,0 @@ -const request = require('supertest') - -/** - * Upload a blob from a test runner - * @param {import('express').Express} app Test runner express instance - * @param {string} filePath Absolute path to file on the server - * @param {string} streamId - * @param {string} authToken Token for authenticating with server, if any - * @returns {Promise} Response result structure - */ -async function uploadBlob(app, filePath, streamId, { authToken }) { - const response = await request(app) - .post(`/api/stream/${streamId}/blob`) - .attach('file', filePath) - .set('Accept', 'application/json') - .set('Authorization', authToken ? `Bearer ${authToken}` : undefined) - - const uploadResults = response.body.uploadResults || [] - if (!uploadResults.length) { - throw new Error('Test runner blob upload received unexpected results!') - } - - return uploadResults[0] -} - -module.exports = { - uploadBlob -} diff --git a/packages/server/test/blobHelper.ts b/packages/server/test/blobHelper.ts new file mode 100644 index 0000000000..9e10ffecb4 --- /dev/null +++ b/packages/server/test/blobHelper.ts @@ -0,0 +1,27 @@ +import request from 'supertest' +import { Express } from 'express' + +/** + * Upload a blob from a test runner + */ +export async function uploadBlob( + app: Express, + filePath: string, + streamId: string, + auth: { authToken: string } +) { + const response = await request(app) + .post(`/api/stream/${streamId}/blob`) + .attach('file', filePath) + .set('Accept', 'application/json') + .set('Authorization', auth.authToken ? `Bearer ${auth.authToken}` : '') + + const uploadResults = response.body.uploadResults || [] + if (!uploadResults.length) { + throw new Error('Test runner blob upload received unexpected results!') + } + + return uploadResults[0] as Record & { blobId: string } +} + +export type UploadedBlob = Awaited> diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index c31fe7bdfe..a78e5a42a5 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -5147,6 +5147,8 @@ export type UseProjectAccessRequestMutationVariables = Exact<{ export type UseProjectAccessRequestMutation = { __typename?: 'Mutation', projectMutations: { __typename?: 'ProjectMutations', accessRequestMutations: { __typename?: 'ProjectAccessRequestMutations', use: { __typename?: 'Project', id: string } } } }; +export type BasicProjectCommentFragment = { __typename?: 'Comment', id: string, rawText: string, authorId: string, text: { __typename?: 'SmartTextEditorValue', doc?: Record | null } }; + export type CreateProjectCommentMutationVariables = Exact<{ input: CreateCommentInput; }>; @@ -5154,6 +5156,20 @@ export type CreateProjectCommentMutationVariables = Exact<{ export type CreateProjectCommentMutation = { __typename?: 'Mutation', commentMutations: { __typename?: 'CommentMutations', create: { __typename?: 'Comment', id: string, rawText: string, authorId: string, text: { __typename?: 'SmartTextEditorValue', doc?: Record | null } } } }; +export type CreateProjectCommentReplyMutationVariables = Exact<{ + input: CreateCommentReplyInput; +}>; + + +export type CreateProjectCommentReplyMutation = { __typename?: 'Mutation', commentMutations: { __typename?: 'CommentMutations', reply: { __typename?: 'Comment', id: string, rawText: string, authorId: string, text: { __typename?: 'SmartTextEditorValue', doc?: Record | null } } } }; + +export type EditProjectCommentMutationVariables = Exact<{ + input: EditCommentInput; +}>; + + +export type EditProjectCommentMutation = { __typename?: 'Mutation', commentMutations: { __typename?: 'CommentMutations', edit: { __typename?: 'Comment', id: string, rawText: string, authorId: string, text: { __typename?: 'SmartTextEditorValue', doc?: Record | null } } } }; + export type BasicProjectFieldsFragment = { __typename?: 'Project', id: string, name: string, description?: string | null, visibility: ProjectVisibility, allowPublicComments: boolean, role?: string | null, createdAt: string, updatedAt: string }; export type AdminProjectListQueryVariables = Exact<{ @@ -5558,6 +5574,7 @@ export const CommentWithRepliesFragmentDoc = {"kind":"Document","definitions":[{ export const BaseCommitFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BaseCommitFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Commit"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"authorName"}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}},{"kind":"Field","name":{"kind":"Name","value":"authorAvatar"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"streamName"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApplication"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"commentCount"}}]}}]} as unknown as DocumentNode; export const MainRegionMetadataFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MainRegionMetadata"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerRegionItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]} as unknown as DocumentNode; export const BasicProjectAccessRequestFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; +export const BasicProjectCommentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectComment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}}]}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}}]}}]} as unknown as DocumentNode; export const BasicProjectFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"allowPublicComments"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const StreamInviteDataFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"StreamInviteData"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingStreamCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}}]}}]}}]} as unknown as DocumentNode; export const BasicStreamFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicStreamFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Stream"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"isPublic"}},{"kind":"Field","name":{"kind":"Name","value":"isDiscoverable"}},{"kind":"Field","name":{"kind":"Name","value":"allowPublicComments"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; @@ -5635,7 +5652,9 @@ export const GetActiveUserProjectAccessRequestDocument = {"kind":"Document","def export const GetActiveUserFullProjectAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetActiveUserFullProjectAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectAccessRequest"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectAccessRequestFields"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; export const GetPendingProjectAccessRequestsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPendingProjectAccessRequests"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"pendingAccessRequests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectAccessRequestFields"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; export const UseProjectAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UseProjectAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"requestId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"accept"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"role"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"StreamRole"}}},"defaultValue":{"kind":"EnumValue","value":"STREAM_CONTRIBUTOR"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"accessRequestMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"use"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"requestId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"requestId"}}},{"kind":"Argument","name":{"kind":"Name","value":"accept"},"value":{"kind":"Variable","name":{"kind":"Name","value":"accept"}}},{"kind":"Argument","name":{"kind":"Name","value":"role"},"value":{"kind":"Variable","name":{"kind":"Name","value":"role"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const CreateProjectCommentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateProjectComment"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateCommentInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commentMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}}]}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateProjectCommentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateProjectComment"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateCommentInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commentMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectComment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectComment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}}]}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}}]}}]} as unknown as DocumentNode; +export const CreateProjectCommentReplyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateProjectCommentReply"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateCommentReplyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commentMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reply"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectComment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectComment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}}]}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}}]}}]} as unknown as DocumentNode; +export const EditProjectCommentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"EditProjectComment"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EditCommentInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commentMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edit"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectComment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectComment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}}]}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}}]}}]} as unknown as DocumentNode; export const AdminProjectListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminProjectList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"query"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"visibility"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"25"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}},"defaultValue":{"kind":"NullValue"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"admin"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"Variable","name":{"kind":"Name","value":"query"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}},{"kind":"Argument","name":{"kind":"Name","value":"visibility"},"value":{"kind":"Variable","name":{"kind":"Name","value":"visibility"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectFields"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"allowPublicComments"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const GetProjectObjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectObject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"objectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"object"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"objectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetProjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/server/test/graphql/projectComments.ts b/packages/server/test/graphql/projectComments.ts index 97e868f9fc..a3e1155098 100644 --- a/packages/server/test/graphql/projectComments.ts +++ b/packages/server/test/graphql/projectComments.ts @@ -1,16 +1,48 @@ import gql from 'graphql-tag' +export const basicProjectCommentFragment = gql` + fragment BasicProjectComment on Comment { + id + rawText + text { + doc + } + authorId + } +` + export const createProjectCommentMutation = gql` mutation CreateProjectComment($input: CreateCommentInput!) { commentMutations { create(input: $input) { - id - rawText - text { - doc - } - authorId + ...BasicProjectComment } } } + + ${basicProjectCommentFragment} +` + +export const createProjectCommentReplyMutation = gql` + mutation CreateProjectCommentReply($input: CreateCommentReplyInput!) { + commentMutations { + reply(input: $input) { + ...BasicProjectComment + } + } + + ${basicProjectCommentFragment} + } +` + +export const editProjectCommentMutation = gql` + mutation EditProjectComment($input: EditCommentInput!) { + commentMutations { + edit(input: $input) { + ...BasicProjectComment + } + } + + ${basicProjectCommentFragment} + } `