From bbd3d12efefd4f378bb6be576faf6680e8aa1378 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 27 Aug 2024 12:52:54 -0700 Subject: [PATCH 01/16] chore: write to PG Signed-off-by: Matt Krick --- codegen.json | 1 + .../components/DiscussionThreadInput.tsx | 4 +- .../helpers/publishSimilarRetroTopics.ts | 13 ++ .../server/dataloader/customLoaderMakers.ts | 24 +-- .../dataloader/foreignKeyLoaderMakers.ts | 10 + .../dataloader/primaryKeyLoaderMakers.ts | 5 + .../server/graphql/mutations/addComment.ts | 183 ------------------ .../server/graphql/mutations/deleteComment.ts | 10 +- .../server/graphql/mutations/endCheckIn.ts | 11 +- .../graphql/mutations/endSprintPoker.ts | 12 +- .../helpers/addAIGeneratedContentToThreads.ts | 11 +- .../mutations/helpers/safeEndRetrospective.ts | 18 +- .../resetRetroMeetingToGroupStage.ts | 1 + .../graphql/mutations/updateCommentContent.ts | 13 +- .../graphql/public/mutations/addComment.ts | 170 ++++++++++++++++ .../public/mutations/addReactjiToReactable.ts | 1 - .../public/typeDefs/AddCommentInput.graphql | 2 +- .../graphql/public/typeDefs/Comment.graphql | 2 +- .../public/typeDefs/CreatePollInput.graphql | 2 +- .../public/typeDefs/CreateTaskInput.graphql | 2 +- .../graphql/public/typeDefs/Poll.graphql | 2 +- .../graphql/public/typeDefs/Task.graphql | 2 +- .../public/typeDefs/Threadable.graphql | 2 +- .../graphql/public/types/AddCommentSuccess.ts | 14 ++ .../graphql/public/types/TeamPromptMeeting.ts | 14 +- .../server/graphql/public/types/Threadable.ts | 3 +- packages/server/graphql/rootMutation.ts | 2 - .../server/graphql/types/AddCommentInput.ts | 34 ---- .../server/graphql/types/AddCommentPayload.ts | 25 --- .../server/graphql/types/CreatePollInput.ts | 4 +- .../server/graphql/types/CreateTaskInput.ts | 3 +- packages/server/graphql/types/Poll.ts | 3 - packages/server/graphql/types/Task.ts | 3 - packages/server/graphql/types/Threadable.ts | 89 --------- .../1724780116674_Comment-phase1.ts | 48 +++++ packages/server/postgres/select.ts | 18 ++ 36 files changed, 354 insertions(+), 407 deletions(-) delete mode 100644 packages/server/graphql/mutations/addComment.ts create mode 100644 packages/server/graphql/public/mutations/addComment.ts create mode 100644 packages/server/graphql/public/types/AddCommentSuccess.ts delete mode 100644 packages/server/graphql/types/AddCommentInput.ts delete mode 100644 packages/server/graphql/types/AddCommentPayload.ts delete mode 100644 packages/server/graphql/types/Threadable.ts create mode 100644 packages/server/postgres/migrations/1724780116674_Comment-phase1.ts diff --git a/codegen.json b/codegen.json index f8e66b6e79c..8ea9beb81b1 100644 --- a/codegen.json +++ b/codegen.json @@ -48,6 +48,7 @@ "mappers": { "SetSlackNotificationPayload": "./types/SetSlackNotificationPayload#SetSlackNotificationPayloadSource", "SetDefaultSlackChannelSuccess": "./types/SetDefaultSlackChannelSuccess#SetDefaultSlackChannelSuccessSource", + "AddCommentSuccess": "./types/AddCommentSuccess#AddCommentSuccessSource", "AddSlackAuthPayload": "./types/AddSlackAuthPayload#AddSlackAuthPayloadSource", "RemoveAgendaItemPayload": "./types/RemoveAgendaItemPayload#RemoveAgendaItemPayloadSource", "AddAgendaItemPayload": "./types/AddAgendaItemPayload#AddAgendaItemPayloadSource", diff --git a/packages/client/components/DiscussionThreadInput.tsx b/packages/client/components/DiscussionThreadInput.tsx index 5d2e1f0373a..c9ed94f2e90 100644 --- a/packages/client/components/DiscussionThreadInput.tsx +++ b/packages/client/components/DiscussionThreadInput.tsx @@ -196,7 +196,7 @@ const DiscussionThreadInput = forwardRef((props: Props, ref: any) => { isAnonymous: isAnonymousComment, discussionId, threadParentId, - threadSortOrder: getMaxSortOrder() + SORT_STEP + dndNoise() + threadSortOrder: getMaxSortOrder() + SORT_STEP } AddCommentMutation(atmosphere, {comment}, {onError, onCompleted}) // move focus to end is very important! otherwise ghost chars appear @@ -263,7 +263,7 @@ const DiscussionThreadInput = forwardRef((props: Props, ref: any) => { discussionId, meetingId, threadParentId, - threadSortOrder: getMaxSortOrder() + SORT_STEP + dndNoise(), + threadSortOrder: getMaxSortOrder() + SORT_STEP, userId: viewerId, teamId } as const diff --git a/packages/embedder/workflows/helpers/publishSimilarRetroTopics.ts b/packages/embedder/workflows/helpers/publishSimilarRetroTopics.ts index 0f881ee4621..8490667c5eb 100644 --- a/packages/embedder/workflows/helpers/publishSimilarRetroTopics.ts +++ b/packages/embedder/workflows/helpers/publishSimilarRetroTopics.ts @@ -8,6 +8,7 @@ import { buildCommentContentBlock, createAIComment } from '../../../server/graphql/mutations/helpers/addAIGeneratedContentToThreads' +import getKysely from '../../../server/postgres/getKysely' import getPhase from '../../../server/utils/getPhase' import publish from '../../../server/utils/publish' @@ -54,6 +55,7 @@ export const publishSimilarRetroTopics = async ( dataLoader: DataLoaderInstance ) => { const r = await getRethink() + const pg = getKysely() const links = await Promise.all( similarEmbeddings.map((se) => makeSimilarDiscussionLink(se, dataLoader)) ) @@ -68,5 +70,16 @@ export const publishSimilarRetroTopics = async ( 2 ) await r.table('Comment').insert(relatedDiscussionsComment).run() + await pg + .insertInto('Comment') + .values({ + id: relatedDiscussionsComment.id, + content: relatedDiscussionsComment.content, + plaintextContent: relatedDiscussionsComment.plaintextContent, + createdBy: relatedDiscussionsComment.createdBy, + threadSortOrder: relatedDiscussionsComment.threadSortOrder, + discussionId: relatedDiscussionsComment.discussionId + }) + .execute() publishComment(meetingId, relatedDiscussionsComment.id) } diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts index 8a5b883dce3..00ef82e4031 100644 --- a/packages/server/dataloader/customLoaderMakers.ts +++ b/packages/server/dataloader/customLoaderMakers.ts @@ -83,31 +83,21 @@ export const commentCountByDiscussionId = ( dependsOn('comments') return new DataLoader( async (discussionIds) => { - const r = await getRethink() - const groups = (await ( - r - .table('Comment') - .getAll(r.args(discussionIds as string[]), {index: 'discussionId'}) - .filter((row: RDatum) => - row('isActive').eq(true).and(row('createdBy').ne(PARABOL_AI_USER_ID)) - ) - .group('discussionId') as any + const commentsByDiscussionId = await Promise.all( + discussionIds.map((discussionId) => parent.get('commentsByDiscussionId').load(discussionId)) ) - .count() - .ungroup() - .run()) as {group: string; reduction: number}[] - const lookup: Record = {} - groups.forEach(({group, reduction}) => { - lookup[group] = reduction + return commentsByDiscussionId.map((commentArr) => { + const activeHumanComments = commentArr.filter( + (comment) => comment.isActive && comment.createdBy !== PARABOL_AI_USER_ID + ) + return activeHumanComments.length }) - return discussionIds.map((discussionId) => lookup[discussionId] || 0) }, { ...parent.dataLoaderOptions } ) } - export const latestTaskEstimates = (parent: RootDataLoader) => { return new DataLoader( async (taskIds) => { diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 2ff2918b551..d65ab6b9385 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -2,6 +2,7 @@ import getKysely from '../postgres/getKysely' import {getTeamPromptResponsesByMeetingIds} from '../postgres/queries/getTeamPromptResponsesByMeetingIds' import { selectAgendaItems, + selectComments, selectOrganizations, selectRetroReflections, selectSlackAuths, @@ -205,3 +206,12 @@ export const slackNotificationsByTeamId = foreignKeyLoaderMaker( return selectSlackNotifications().where('teamId', 'in', teamIds).execute() } ) + +export const _pgcommentsByDiscussionId = foreignKeyLoaderMaker( + '_pgcomments', + 'discussionId', + async (discussionIds) => { + // include deleted comments so we can replace them with tombstones + return selectComments().where('discussionId', 'in', discussionIds).execute() + } +) diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index d514172dde0..c57d6b9adab 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -7,6 +7,7 @@ import getTemplateRefsByIds from '../postgres/queries/getTemplateRefsByIds' import {getUsersByIds} from '../postgres/queries/getUsersByIds' import { selectAgendaItems, + selectComments, selectMeetingSettings, selectOrganizations, selectRetroReflections, @@ -105,3 +106,7 @@ export const slackAuths = primaryKeyLoaderMaker((ids: readonly string[]) => { export const slackNotifications = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectSlackNotifications().where('id', 'in', ids).execute() }) + +export const _pgcomments = primaryKeyLoaderMaker((ids: readonly string[]) => { + return selectComments().where('id', 'in', ids).execute() +}) diff --git a/packages/server/graphql/mutations/addComment.ts b/packages/server/graphql/mutations/addComment.ts deleted file mode 100644 index 1a117aeb780..00000000000 --- a/packages/server/graphql/mutations/addComment.ts +++ /dev/null @@ -1,183 +0,0 @@ -import {GraphQLNonNull} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' -import MeetingMemberId from '../../../client/shared/gqlIds/MeetingMemberId' -import TeamMemberId from '../../../client/shared/gqlIds/TeamMemberId' -import getTypeFromEntityMap from '../../../client/utils/draftjs/getTypeFromEntityMap' -import getRethink from '../../database/rethinkDriver' -import Comment from '../../database/types/Comment' -import GenericMeetingPhase, { - NewMeetingPhaseTypeEnum -} from '../../database/types/GenericMeetingPhase' -import GenericMeetingStage from '../../database/types/GenericMeetingStage' -import NotificationDiscussionMentioned from '../../database/types/NotificationDiscussionMentioned' -import NotificationResponseReplied from '../../database/types/NotificationResponseReplied' -import {IGetDiscussionsByIdsQueryResult} from '../../postgres/queries/generated/getDiscussionsByIdsQuery' -import {analytics} from '../../utils/analytics/analytics' -import {getUserId} from '../../utils/authorization' -import publish from '../../utils/publish' -import {GQLContext} from '../graphql' -import publishNotification from '../public/mutations/helpers/publishNotification' -import AddCommentInput from '../types/AddCommentInput' -import AddCommentPayload from '../types/AddCommentPayload' -import {IntegrationNotifier} from './helpers/notifications/IntegrationNotifier' - -type AddCommentMutationVariables = { - comment: { - discussionId: string - content: string - threadSortOrder: number - } -} - -const getMentionNotifications = ( - content: string, - viewerId: string, - discussion: IGetDiscussionsByIdsQueryResult, - commentId: string, - meetingId: string -) => { - let parsedContent: any - try { - parsedContent = JSON.parse(content) - } catch { - // If we can't parse the content, assume no new notifications. - return [] - } - - const {entityMap} = parsedContent - return getTypeFromEntityMap('MENTION', entityMap) - .filter((mentionedUserId) => { - if (mentionedUserId === viewerId) { - return false - } - - if (discussion.discussionTopicType === 'teamPromptResponse') { - const {userId: responseUserId} = TeamMemberId.split(discussion.discussionTopicId) - if (responseUserId === mentionedUserId) { - // The mentioned user will already receive a 'RESPONSE_REPLIED' notification for this - // comment - return false - } - } - - // :TODO: (jmtaber129): Consider limiting these to when the mentionee is *not* on the - // relevant page. - return true - }) - .map( - (mentioneeUserId) => - new NotificationDiscussionMentioned({ - userId: mentioneeUserId, - meetingId: meetingId, - authorId: viewerId, - commentId, - discussionId: discussion.id - }) - ) -} - -const addComment = { - type: new GraphQLNonNull(AddCommentPayload), - description: `Add a comment to a discussion`, - args: { - comment: { - type: new GraphQLNonNull(AddCommentInput), - description: 'A partial new comment' - } - }, - resolve: async ( - _source: unknown, - {comment}: AddCommentMutationVariables, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) => { - const r = await getRethink() - const viewerId = getUserId(authToken) - const operationId = dataLoader.share() - const subOptions = {mutatorId, operationId} - - //AUTH - const {discussionId} = comment - const discussion = await dataLoader.get('discussions').load(discussionId) - if (!discussion) { - return {error: {message: 'Invalid discussion thread'}} - } - const {meetingId} = discussion - if (!meetingId) { - return {error: {message: 'Discussion does not take place in a meeting'}} - } - const meetingMemberId = MeetingMemberId.join(meetingId, viewerId) - const [meeting, viewerMeetingMember, viewer] = await Promise.all([ - dataLoader.get('newMeetings').load(meetingId), - dataLoader.get('meetingMembers').load(meetingMemberId), - dataLoader.get('users').loadNonNull(viewerId) - ]) - - if (!viewerMeetingMember) { - return {error: {message: 'Not a member of the meeting'}} - } - - // VALIDATION - const content = normalizeRawDraftJS(comment.content) - - const dbComment = new Comment({...comment, content, createdBy: viewerId}) - const {id: commentId, isAnonymous, threadParentId} = dbComment - await r.table('Comment').insert(dbComment).run() - - if (discussion.discussionTopicType === 'teamPromptResponse') { - const {userId: responseUserId} = TeamMemberId.split(discussion.discussionTopicId) - - if (responseUserId !== viewerId) { - const notification = new NotificationResponseReplied({ - userId: responseUserId, - meetingId: meetingId, - authorId: viewerId, - commentId - }) - - await r.table('Notification').insert(notification).run() - - IntegrationNotifier.sendNotificationToUser?.( - dataLoader, - notification.id, - notification.userId - ) - publishNotification(notification, subOptions) - } - } - - const notificationsToAdd = getMentionNotifications( - content, - viewerId, - discussion, - commentId, - meetingId - ) - - if (notificationsToAdd.length) { - await r.table('Notification').insert(notificationsToAdd).run() - notificationsToAdd.forEach((notification) => { - publishNotification(notification, subOptions) - }) - } - - const data = {commentId, meetingId} - const {phases} = meeting! - const threadablePhases = [ - 'discuss', - 'agendaitems', - 'ESTIMATE', - 'RESPONSES' - ] as NewMeetingPhaseTypeEnum[] - const containsThreadablePhase = phases.find(({phaseType}: GenericMeetingPhase) => - threadablePhases.includes(phaseType) - )! - const {stages} = containsThreadablePhase - const isAsync = stages.some((stage: GenericMeetingStage) => stage.isAsync) - analytics.commentAdded(viewer, meeting, isAnonymous, isAsync, !!threadParentId) - publish(SubscriptionChannel.MEETING, meetingId, 'AddCommentSuccess', data, subOptions) - return data - } -} - -export default addComment diff --git a/packages/server/graphql/mutations/deleteComment.ts b/packages/server/graphql/mutations/deleteComment.ts index a3910576a4f..e2a3ee08276 100644 --- a/packages/server/graphql/mutations/deleteComment.ts +++ b/packages/server/graphql/mutations/deleteComment.ts @@ -3,6 +3,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' import {PARABOL_AI_USER_ID} from '../../../client/utils/constants' import getRethink from '../../database/rethinkDriver' +import getKysely from '../../postgres/getKysely' import {getUserId} from '../../utils/authorization' import publish from '../../utils/publish' import {GQLContext} from '../graphql' @@ -38,7 +39,7 @@ const deleteComment = { //AUTH const meetingMemberId = toTeamMemberId(meetingId, viewerId) const [comment, viewerMeetingMember] = await Promise.all([ - r.table('Comment').get(commentId).run(), + dataLoader.get('comments').load(commentId), dataLoader.get('meetingMembers').load(meetingMemberId), dataLoader.get('newMeetings').load(meetingId) ]) @@ -58,7 +59,12 @@ const deleteComment = { } await r.table('Comment').get(commentId).update({isActive: false, updatedAt: now}).run() - + await getKysely() + .updateTable('Comment') + .set({updatedAt: now}) + .where('id', '=', commentId) + .execute() + dataLoader.clearAll('comments') const data = {commentId} if (meetingId) { diff --git a/packages/server/graphql/mutations/endCheckIn.ts b/packages/server/graphql/mutations/endCheckIn.ts index a3e0c2573da..4ad865c40b0 100644 --- a/packages/server/graphql/mutations/endCheckIn.ts +++ b/packages/server/graphql/mutations/endCheckIn.ts @@ -23,6 +23,7 @@ import getPhase from '../../utils/getPhase' import publish from '../../utils/publish' import standardError from '../../utils/standardError' import {DataLoaderWorker, GQLContext} from '../graphql' +import isValid from '../isValid' import EndCheckInPayload from '../types/EndCheckInPayload' import sendNewMeetingSummary from './helpers/endMeeting/sendNewMeetingSummary' import gatherInsights from './helpers/gatherInsights' @@ -133,6 +134,10 @@ const summarizeCheckInMeeting = async (meeting: MeetingAction, dataLoader: DataL const pinnedAgendaItems = await getPinnedAgendaItems(teamId, dataLoader) const isKill = !!(meetingPhase && ![AGENDA_ITEMS, LAST_CALL].includes(meetingPhase.phaseType)) if (!isKill) await clearAgendaItems(teamId, dataLoader) + const commentCounts = ( + await dataLoader.get('commentCountByDiscussionId').loadMany(discussionIds) + ).filter(isValid) + const commentCount = commentCounts.reduce((cumSum, count) => cumSum + count, 0) await Promise.all([ isKill ? undefined : archiveTasksForDB(doneTasks, meetingId), isKill ? undefined : clonePinnedAgendaItems(pinnedAgendaItems, dataLoader), @@ -143,11 +148,7 @@ const summarizeCheckInMeeting = async (meeting: MeetingAction, dataLoader: DataL .update( { agendaItemCount: activeAgendaItems.length, - commentCount: r - .table('Comment') - .getAll(r.args(discussionIds), {index: 'discussionId'}) - .count() - .default(0) as unknown as number, + commentCount, taskCount: tasks.length }, {nonAtomic: true} diff --git a/packages/server/graphql/mutations/endSprintPoker.ts b/packages/server/graphql/mutations/endSprintPoker.ts index 267aa3eb4cf..00db314a2ab 100644 --- a/packages/server/graphql/mutations/endSprintPoker.ts +++ b/packages/server/graphql/mutations/endSprintPoker.ts @@ -16,6 +16,7 @@ import getRedis from '../../utils/getRedis' import publish from '../../utils/publish' import standardError from '../../utils/standardError' import {GQLContext} from '../graphql' +import isValid from '../isValid' import EndSprintPokerPayload from '../types/EndSprintPokerPayload' import sendNewMeetingSummary from './helpers/endMeeting/sendNewMeetingSummary' import gatherInsights from './helpers/gatherInsights' @@ -77,7 +78,10 @@ export default { ).size const discussionIds = estimateStages.map((stage) => stage.discussionId) const insights = await gatherInsights(meeting, dataLoader) - + const commentCounts = ( + await dataLoader.get('commentCountByDiscussionId').loadMany(discussionIds) + ).filter(isValid) + const commentCount = commentCounts.reduce((cumSum, count) => cumSum + count, 0) const completedMeeting = (await r .table('NewMeeting') .get(meetingId) @@ -85,11 +89,7 @@ export default { { endedAt: now, phases, - commentCount: r - .table('Comment') - .getAll(r.args(discussionIds), {index: 'discussionId'}) - .count() - .default(0) as unknown as number, + commentCount, storyCount, ...insights }, diff --git a/packages/server/graphql/mutations/helpers/addAIGeneratedContentToThreads.ts b/packages/server/graphql/mutations/helpers/addAIGeneratedContentToThreads.ts index fa513232f98..5eae10abdb7 100644 --- a/packages/server/graphql/mutations/helpers/addAIGeneratedContentToThreads.ts +++ b/packages/server/graphql/mutations/helpers/addAIGeneratedContentToThreads.ts @@ -2,6 +2,7 @@ import {PARABOL_AI_USER_ID} from '../../../../client/utils/constants' import getRethink from '../../../database/rethinkDriver' import Comment from '../../../database/types/Comment' import DiscussStage from '../../../database/types/DiscussStage' +import getKysely from '../../../postgres/getKysely' import {convertHtmlToTaskContent} from '../../../utils/draftjs/convertHtmlToTaskContent' import {DataLoaderWorker} from '../../graphql' @@ -45,7 +46,15 @@ const addAIGeneratedContentToThreads = async ( ) comments.push(topicSummaryComment) } - + const pgComments = comments.map((comment) => ({ + id: comment.id, + content: comment.content, + plaintextContent: comment.plaintextContent, + createdBy: comment.createdBy, + threadSortOrder: comment.threadSortOrder, + discussionId: comment.discussionId + })) + await getKysely().insertInto('Comment').values(pgComments).execute() return r.table('Comment').insert(comments).run() }) await Promise.all(commentPromises) diff --git a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts index 3f3f0b4be4f..1af261de253 100644 --- a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts +++ b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts @@ -1,10 +1,9 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import {DISCUSS, PARABOL_AI_USER_ID} from 'parabol-client/utils/constants' +import {DISCUSS} from 'parabol-client/utils/constants' import getMeetingPhase from 'parabol-client/utils/getMeetingPhase' import findStageById from 'parabol-client/utils/meetings/findStageById' import {checkTeamsLimit} from '../../../billing/helpers/teamLimitsCheck' import getRethink from '../../../database/rethinkDriver' -import {RDatum} from '../../../database/stricterR' import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import TimelineEventRetroComplete from '../../../database/types/TimelineEventRetroComplete' import getKysely from '../../../postgres/getKysely' @@ -17,6 +16,7 @@ import getPhase from '../../../utils/getPhase' import publish from '../../../utils/publish' import standardError from '../../../utils/standardError' import {InternalContext} from '../../graphql' +import isValid from '../../isValid' import sendNewMeetingSummary from './endMeeting/sendNewMeetingSummary' import gatherInsights from './gatherInsights' import generateWholeMeetingSentimentScore from './generateWholeMeetingSentimentScore' @@ -51,20 +51,16 @@ const summarizeRetroMeeting = async (meeting: MeetingRetrospective, context: Int generateWholeMeetingSummary(discussionIds, meetingId, teamId, facilitatorUserId, dataLoader), getTranscription(recallBotId) ]) - + const commentCounts = ( + await dataLoader.get('commentCountByDiscussionId').loadMany(discussionIds) + ).filter(isValid) + const commentCount = commentCounts.reduce((cumSum, count) => cumSum + count, 0) await r .table('NewMeeting') .get(meetingId) .update( { - commentCount: r - .table('Comment') - .getAll(r.args(discussionIds), {index: 'discussionId'}) - .filter((row: RDatum) => - row('isActive').eq(true).and(row('createdBy').ne(PARABOL_AI_USER_ID)) - ) - .count() - .default(0) as unknown as number, + commentCount, taskCount: r .table('Task') .getAll(r.args(discussionIds), {index: 'discussionId'}) diff --git a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts index 5e1238fedb9..0e775bc2dc2 100644 --- a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts +++ b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts @@ -109,6 +109,7 @@ const resetRetroMeetingToGroupStage = { .getAll(r.args(discussionIdsToDelete), {index: 'discussionId'}) .delete() .run(), + pg.deleteFrom('Comment').where('discussionId', 'in', discussionIdsToDelete).execute(), r.table('Task').getAll(r.args(discussionIdsToDelete), {index: 'discussionId'}).delete().run(), pg .updateTable('RetroReflectionGroup') diff --git a/packages/server/graphql/mutations/updateCommentContent.ts b/packages/server/graphql/mutations/updateCommentContent.ts index 362301e9019..307a82044af 100644 --- a/packages/server/graphql/mutations/updateCommentContent.ts +++ b/packages/server/graphql/mutations/updateCommentContent.ts @@ -5,6 +5,7 @@ import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' import {PARABOL_AI_USER_ID} from '../../../client/utils/constants' import getRethink from '../../database/rethinkDriver' +import getKysely from '../../postgres/getKysely' import {getUserId} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -40,9 +41,8 @@ export default { const viewerId = getUserId(authToken) const meetingMemberId = toTeamMemberId(meetingId, viewerId) const [comment, viewerMeetingMember] = await Promise.all([ - r.table('Comment').get(commentId).run(), - dataLoader.get('meetingMembers').load(meetingMemberId), - dataLoader.get('newMeetings').load(meetingId) + dataLoader.get('comments').load(commentId), + dataLoader.get('meetingMembers').load(meetingMemberId) ]) if (!comment || !comment.isActive) { return standardError(new Error('comment not found'), {userId: viewerId}) @@ -69,7 +69,12 @@ export default { .get(commentId) .update({content: normalizedContent, plaintextContent, updatedAt: now}) .run() - + await getKysely() + .updateTable('Comment') + .set({content: normalizedContent, plaintextContent}) + .where('id', '=', commentId) + .execute() + dataLoader.clearAll('comments') // :TODO: (jmtaber129): diff new and old comment content for mentions and handle notifications // appropriately. diff --git a/packages/server/graphql/public/mutations/addComment.ts b/packages/server/graphql/public/mutations/addComment.ts new file mode 100644 index 00000000000..2e0392c062e --- /dev/null +++ b/packages/server/graphql/public/mutations/addComment.ts @@ -0,0 +1,170 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' +import MeetingMemberId from '../../../../client/shared/gqlIds/MeetingMemberId' +import TeamMemberId from '../../../../client/shared/gqlIds/TeamMemberId' +import getTypeFromEntityMap from '../../../../client/utils/draftjs/getTypeFromEntityMap' +import getRethink from '../../../database/rethinkDriver' +import Comment from '../../../database/types/Comment' +import GenericMeetingPhase, { + NewMeetingPhaseTypeEnum +} from '../../../database/types/GenericMeetingPhase' +import GenericMeetingStage from '../../../database/types/GenericMeetingStage' +import NotificationDiscussionMentioned from '../../../database/types/NotificationDiscussionMentioned' +import NotificationResponseReplied from '../../../database/types/NotificationResponseReplied' +import getKysely from '../../../postgres/getKysely' +import {IGetDiscussionsByIdsQueryResult} from '../../../postgres/queries/generated/getDiscussionsByIdsQuery' +import {analytics} from '../../../utils/analytics/analytics' +import {getUserId} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import {IntegrationNotifier} from '../../mutations/helpers/notifications/IntegrationNotifier' +import {MutationResolvers} from '../resolverTypes' +import publishNotification from './helpers/publishNotification' + +const getMentionNotifications = ( + content: string, + viewerId: string, + discussion: IGetDiscussionsByIdsQueryResult, + commentId: string, + meetingId: string +) => { + let parsedContent: any + try { + parsedContent = JSON.parse(content) + } catch { + // If we can't parse the content, assume no new notifications. + return [] + } + + const {entityMap} = parsedContent + return getTypeFromEntityMap('MENTION', entityMap) + .filter((mentionedUserId) => { + if (mentionedUserId === viewerId) { + return false + } + + if (discussion.discussionTopicType === 'teamPromptResponse') { + const {userId: responseUserId} = TeamMemberId.split(discussion.discussionTopicId) + if (responseUserId === mentionedUserId) { + // The mentioned user will already receive a 'RESPONSE_REPLIED' notification for this + // comment + return false + } + } + + // :TODO: (jmtaber129): Consider limiting these to when the mentionee is *not* on the + // relevant page. + return true + }) + .map( + (mentioneeUserId) => + new NotificationDiscussionMentioned({ + userId: mentioneeUserId, + meetingId: meetingId, + authorId: viewerId, + commentId, + discussionId: discussion.id + }) + ) +} + +const addComment: MutationResolvers['addComment'] = async ( + _source, + {comment}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const r = await getRethink() + const viewerId = getUserId(authToken) + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + + //AUTH + const {discussionId} = comment + const discussion = await dataLoader.get('discussions').load(discussionId) + if (!discussion) { + return {error: {message: 'Invalid discussion thread'}} + } + const {meetingId} = discussion + if (!meetingId) { + return {error: {message: 'Discussion does not take place in a meeting'}} + } + const meetingMemberId = MeetingMemberId.join(meetingId, viewerId) + const [meeting, viewerMeetingMember, viewer] = await Promise.all([ + dataLoader.get('newMeetings').load(meetingId), + dataLoader.get('meetingMembers').load(meetingMemberId), + dataLoader.get('users').loadNonNull(viewerId) + ]) + + if (!viewerMeetingMember) { + return {error: {message: 'Not a member of the meeting'}} + } + + // VALIDATION + const content = normalizeRawDraftJS(comment.content) + + const dbComment = new Comment({...comment, content, createdBy: viewerId}) + const {id: commentId, isAnonymous, threadParentId} = dbComment + await r.table('Comment').insert(dbComment).run() + await getKysely() + .insertInto('Comment') + .values({ + id: dbComment.id, + content: dbComment.content, + plaintextContent: dbComment.plaintextContent, + createdBy: dbComment.createdBy, + threadSortOrder: dbComment.threadSortOrder, + discussionId: dbComment.discussionId + }) + .execute() + + if (discussion.discussionTopicType === 'teamPromptResponse') { + const {userId: responseUserId} = TeamMemberId.split(discussion.discussionTopicId) + + if (responseUserId !== viewerId) { + const notification = new NotificationResponseReplied({ + userId: responseUserId, + meetingId: meetingId, + authorId: viewerId, + commentId + }) + + await r.table('Notification').insert(notification).run() + + IntegrationNotifier.sendNotificationToUser?.(dataLoader, notification.id, notification.userId) + publishNotification(notification, subOptions) + } + } + + const notificationsToAdd = getMentionNotifications( + content, + viewerId, + discussion, + commentId, + meetingId + ) + + if (notificationsToAdd.length) { + await r.table('Notification').insert(notificationsToAdd).run() + notificationsToAdd.forEach((notification) => { + publishNotification(notification, subOptions) + }) + } + + const data = {commentId, meetingId} + const {phases} = meeting! + const threadablePhases = [ + 'discuss', + 'agendaitems', + 'ESTIMATE', + 'RESPONSES' + ] as NewMeetingPhaseTypeEnum[] + const containsThreadablePhase = phases.find(({phaseType}: GenericMeetingPhase) => + threadablePhases.includes(phaseType) + )! + const {stages} = containsThreadablePhase + const isAsync = stages.some((stage: GenericMeetingStage) => stage.isAsync) + analytics.commentAdded(viewer, meeting, isAnonymous, isAsync, !!threadParentId) + publish(SubscriptionChannel.MEETING, meetingId, 'AddCommentSuccess', data, subOptions) + return data +} + +export default addComment diff --git a/packages/server/graphql/public/mutations/addReactjiToReactable.ts b/packages/server/graphql/public/mutations/addReactjiToReactable.ts index b60543c50ad..93225e8837e 100644 --- a/packages/server/graphql/public/mutations/addReactjiToReactable.ts +++ b/packages/server/graphql/public/mutations/addReactjiToReactable.ts @@ -93,7 +93,6 @@ const addReactjiToReactable: MutationResolvers['addReactjiToReactable'] = async tableName === 'TeamPromptResponse' ? TeamPromptResponseId.split(reactableId) : reactableId const updatePG = async (pgTable: ValueOf) => { - if (pgTable === 'Comment') return if (isRemove) { await pg .updateTable(pgTable) diff --git a/packages/server/graphql/public/typeDefs/AddCommentInput.graphql b/packages/server/graphql/public/typeDefs/AddCommentInput.graphql index d9c2ef2b1ed..9fb75071f5e 100644 --- a/packages/server/graphql/public/typeDefs/AddCommentInput.graphql +++ b/packages/server/graphql/public/typeDefs/AddCommentInput.graphql @@ -13,6 +13,6 @@ input AddCommentInput { foreign key for the discussion this was created in """ discussionId: ID! - threadSortOrder: Float! + threadSortOrder: Int! threadParentId: ID } diff --git a/packages/server/graphql/public/typeDefs/Comment.graphql b/packages/server/graphql/public/typeDefs/Comment.graphql index eadc0fe6fd3..782cb1d5885 100644 --- a/packages/server/graphql/public/typeDefs/Comment.graphql +++ b/packages/server/graphql/public/typeDefs/Comment.graphql @@ -45,7 +45,7 @@ type Comment implements Reactable & Threadable { """ the order of this threadable, relative to threadParentId """ - threadSortOrder: Float + threadSortOrder: Int """ The timestamp the item was updated diff --git a/packages/server/graphql/public/typeDefs/CreatePollInput.graphql b/packages/server/graphql/public/typeDefs/CreatePollInput.graphql index d73109d5924..e12e37221f0 100644 --- a/packages/server/graphql/public/typeDefs/CreatePollInput.graphql +++ b/packages/server/graphql/public/typeDefs/CreatePollInput.graphql @@ -7,7 +7,7 @@ input CreatePollInput { """ The order of this threadable """ - threadSortOrder: Float! + threadSortOrder: Int! """ Poll question diff --git a/packages/server/graphql/public/typeDefs/CreateTaskInput.graphql b/packages/server/graphql/public/typeDefs/CreateTaskInput.graphql index 374821407a8..9a14e61ab8a 100644 --- a/packages/server/graphql/public/typeDefs/CreateTaskInput.graphql +++ b/packages/server/graphql/public/typeDefs/CreateTaskInput.graphql @@ -11,7 +11,7 @@ input CreateTaskInput { foreign key for the thread this was created in """ discussionId: ID - threadSortOrder: Float + threadSortOrder: Int threadParentId: ID sortOrder: Float status: TaskStatusEnum! diff --git a/packages/server/graphql/public/typeDefs/Poll.graphql b/packages/server/graphql/public/typeDefs/Poll.graphql index 62d1a3325d7..0da37ebfb78 100644 --- a/packages/server/graphql/public/typeDefs/Poll.graphql +++ b/packages/server/graphql/public/typeDefs/Poll.graphql @@ -40,7 +40,7 @@ type Poll implements Threadable { """ the order of this threadable, relative to threadParentId """ - threadSortOrder: Float + threadSortOrder: Int """ The timestamp the item was updated diff --git a/packages/server/graphql/public/typeDefs/Task.graphql b/packages/server/graphql/public/typeDefs/Task.graphql index 4ea269155c0..ea3d30bd31f 100644 --- a/packages/server/graphql/public/typeDefs/Task.graphql +++ b/packages/server/graphql/public/typeDefs/Task.graphql @@ -45,7 +45,7 @@ type Task implements Threadable { """ the order of this threadable, relative to threadParentId """ - threadSortOrder: Float + threadSortOrder: Int """ The timestamp the item was updated diff --git a/packages/server/graphql/public/typeDefs/Threadable.graphql b/packages/server/graphql/public/typeDefs/Threadable.graphql index 4f713b5c208..96644234f9b 100644 --- a/packages/server/graphql/public/typeDefs/Threadable.graphql +++ b/packages/server/graphql/public/typeDefs/Threadable.graphql @@ -40,7 +40,7 @@ interface Threadable { """ the order of this threadable, relative to threadParentId """ - threadSortOrder: Float + threadSortOrder: Int """ The timestamp the item was updated diff --git a/packages/server/graphql/public/types/AddCommentSuccess.ts b/packages/server/graphql/public/types/AddCommentSuccess.ts new file mode 100644 index 00000000000..5f946d07872 --- /dev/null +++ b/packages/server/graphql/public/types/AddCommentSuccess.ts @@ -0,0 +1,14 @@ +import {AddCommentSuccessResolvers} from '../resolverTypes' + +export type AddCommentSuccessSource = { + commentId: string + meetingId: string +} + +const AddCommentSuccess: AddCommentSuccessResolvers = { + comment: async ({commentId}, _args, {dataLoader}) => { + return dataLoader.get('comments').load(commentId) + } +} + +export default AddCommentSuccess diff --git a/packages/server/graphql/public/types/TeamPromptMeeting.ts b/packages/server/graphql/public/types/TeamPromptMeeting.ts index 9ade4b0e494..adba25e12fc 100644 --- a/packages/server/graphql/public/types/TeamPromptMeeting.ts +++ b/packages/server/graphql/public/types/TeamPromptMeeting.ts @@ -5,6 +5,7 @@ import {getTeamPromptResponsesByMeetingId} from '../../../postgres/queries/getTe import {getUserId} from '../../../utils/authorization' import filterTasksByMeeting from '../../../utils/filterTasksByMeeting' import getPhase from '../../../utils/getPhase' +import isValid from '../../isValid' import {TeamPromptMeetingResolvers} from '../resolverTypes' const TeamPromptMeeting: TeamPromptMeetingResolvers = { @@ -98,14 +99,11 @@ const TeamPromptMeeting: TeamPromptMeetingResolvers = { const discussPhase = getPhase(phases, 'RESPONSES') const {stages} = discussPhase const discussionIds = stages.map((stage) => stage.discussionId) - const r = await getRethink() - return r - .table('Comment') - .getAll(r.args(discussionIds), {index: 'discussionId'}) - .filter({isActive: true}) - .count() - .default(0) - .run() + const commentCounts = ( + await dataLoader.get('commentCountByDiscussionId').loadMany(discussionIds) + ).filter(isValid) + const commentCount = commentCounts.reduce((cumSum, count) => cumSum + count, 0) + return commentCount } } diff --git a/packages/server/graphql/public/types/Threadable.ts b/packages/server/graphql/public/types/Threadable.ts index 80fa584c253..c2520177b3a 100644 --- a/packages/server/graphql/public/types/Threadable.ts +++ b/packages/server/graphql/public/types/Threadable.ts @@ -15,7 +15,8 @@ const Threadable: ThreadableResolvers = { createdByUser: ({createdBy}, _args, {dataLoader}) => { return createdBy ? dataLoader.get('users').loadNonNull(createdBy) : null }, - replies: ({replies}) => replies || [] + replies: ({replies}) => replies || [], + threadSortOrder: ({threadSortOrder}) => (isNaN(threadSortOrder) ? 0 : Math.trunc(threadSortOrder)) } export default Threadable diff --git a/packages/server/graphql/rootMutation.ts b/packages/server/graphql/rootMutation.ts index 71fde7a3e06..3dd06c921fd 100644 --- a/packages/server/graphql/rootMutation.ts +++ b/packages/server/graphql/rootMutation.ts @@ -1,7 +1,6 @@ import {GraphQLObjectType} from 'graphql' import {GQLContext} from './graphql' import addAtlassianAuth from './mutations/addAtlassianAuth' -import addComment from './mutations/addComment' import addGitHubAuth from './mutations/addGitHubAuth' import addIntegrationProvider from './mutations/addIntegrationProvider' import addOrg from './mutations/addOrg' @@ -114,7 +113,6 @@ export default new GraphQLObjectType({ fields: () => ({ addAtlassianAuth, - addComment, addPokerTemplateDimension, addPokerTemplateScale, addPokerTemplateScaleValue, diff --git a/packages/server/graphql/types/AddCommentInput.ts b/packages/server/graphql/types/AddCommentInput.ts deleted file mode 100644 index ff86e2917bc..00000000000 --- a/packages/server/graphql/types/AddCommentInput.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - GraphQLBoolean, - GraphQLFloat, - GraphQLID, - GraphQLInputObjectType, - GraphQLNonNull, - GraphQLString -} from 'graphql' - -const AddCommentInput = new GraphQLInputObjectType({ - name: 'AddCommentInput', - fields: () => ({ - content: { - type: new GraphQLNonNull(GraphQLString), - description: 'A stringified draft-js document containing thoughts' - }, - isAnonymous: { - type: GraphQLBoolean, - description: 'true if the comment should be anonymous' - }, - discussionId: { - type: new GraphQLNonNull(GraphQLID), - description: 'foreign key for the discussion this was created in' - }, - threadSortOrder: { - type: new GraphQLNonNull(GraphQLFloat) - }, - threadParentId: { - type: GraphQLID - } - }) -}) - -export default AddCommentInput diff --git a/packages/server/graphql/types/AddCommentPayload.ts b/packages/server/graphql/types/AddCommentPayload.ts deleted file mode 100644 index c0535e1b9fe..00000000000 --- a/packages/server/graphql/types/AddCommentPayload.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {GraphQLID, GraphQLNonNull, GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import Comment from './Comment' -import makeMutationPayload from './makeMutationPayload' - -export const AddCommentSuccess = new GraphQLObjectType({ - name: 'AddCommentSuccess', - fields: () => ({ - comment: { - type: new GraphQLNonNull(Comment), - description: 'the comment just created', - resolve: async ({commentId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('comments').load(commentId) - } - }, - meetingId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The id of the meeting where the comment was added' - } - }) -}) - -const AddCommentPayload = makeMutationPayload('AddCommentPayload', AddCommentSuccess) - -export default AddCommentPayload diff --git a/packages/server/graphql/types/CreatePollInput.ts b/packages/server/graphql/types/CreatePollInput.ts index ddeb17bf122..70c0bff6deb 100644 --- a/packages/server/graphql/types/CreatePollInput.ts +++ b/packages/server/graphql/types/CreatePollInput.ts @@ -1,7 +1,7 @@ import { - GraphQLFloat, GraphQLID, GraphQLInputObjectType, + GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLString @@ -16,7 +16,7 @@ const CreatePollInput = new GraphQLInputObjectType({ description: 'Foreign key for the discussion this was created in' }, threadSortOrder: { - type: new GraphQLNonNull(GraphQLFloat), + type: new GraphQLNonNull(GraphQLInt), description: 'The order of this threadable' }, title: { diff --git a/packages/server/graphql/types/CreateTaskInput.ts b/packages/server/graphql/types/CreateTaskInput.ts index dc6a7d1999a..6bad42ddbc7 100644 --- a/packages/server/graphql/types/CreateTaskInput.ts +++ b/packages/server/graphql/types/CreateTaskInput.ts @@ -2,6 +2,7 @@ import { GraphQLFloat, GraphQLID, GraphQLInputObjectType, + GraphQLInt, GraphQLNonNull, GraphQLString } from 'graphql' @@ -41,7 +42,7 @@ const CreateTaskInput = new GraphQLInputObjectType({ description: 'foreign key for the thread this was created in' }, threadSortOrder: { - type: GraphQLFloat + type: GraphQLInt }, threadParentId: { type: GraphQLID diff --git a/packages/server/graphql/types/Poll.ts b/packages/server/graphql/types/Poll.ts index 98de0976f31..0922b4fac7c 100644 --- a/packages/server/graphql/types/Poll.ts +++ b/packages/server/graphql/types/Poll.ts @@ -3,16 +3,13 @@ import PollId from '../../../client/shared/gqlIds/PollId' import {GQLContext} from './../graphql' import PollOption from './PollOption' import Team from './Team' -import Threadable, {threadableFields} from './Threadable' import User from './User' const Poll: GraphQLObjectType = new GraphQLObjectType({ name: 'Poll', description: 'A poll created during the meeting', - interfaces: () => [Threadable], isTypeOf: ({title}) => !!title, fields: () => ({ - ...(threadableFields() as any), createdByUser: { type: new GraphQLNonNull(User), description: 'The user that created the item', diff --git a/packages/server/graphql/types/Task.ts b/packages/server/graphql/types/Task.ts index fcf13730521..d2a8b1b7ad8 100644 --- a/packages/server/graphql/types/Task.ts +++ b/packages/server/graphql/types/Task.ts @@ -32,15 +32,12 @@ import TaskIntegration from './TaskIntegration' import TaskServiceEnum from './TaskServiceEnum' import TaskStatusEnum from './TaskStatusEnum' import Team from './Team' -import Threadable, {threadableFields} from './Threadable' const Task: GraphQLObjectType = new GraphQLObjectType({ name: 'Task', description: 'A long-term task shared across the team, assigned to a single user ', - interfaces: () => [Threadable], isTypeOf: ({status}) => !!status, fields: () => ({ - ...(threadableFields() as any), agendaItem: { type: AgendaItem, description: 'The agenda item that the task was created in, if any', diff --git a/packages/server/graphql/types/Threadable.ts b/packages/server/graphql/types/Threadable.ts deleted file mode 100644 index 3341b404ba9..00000000000 --- a/packages/server/graphql/types/Threadable.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - GraphQLFloat, - GraphQLID, - GraphQLInterfaceType, - GraphQLList, - GraphQLNonNull, - GraphQLString -} from 'graphql' -import connectionDefinitions from '../connectionDefinitions' -import {GQLContext} from '../graphql' -import {ThreadableSource as ThreadableDB} from '../public/types/Threadable' -import GraphQLISO8601Type from './GraphQLISO8601Type' -import PageInfo from './PageInfo' - -export const threadableFields = () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'shortid' - }, - createdAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The timestamp the item was created' - }, - createdBy: { - type: GraphQLID, - description: 'The userId that created the item' - }, - createdByUser: { - type: require('./User').default, - description: 'The user that created the item', - resolve: ({createdBy}: {createdBy: string}, _args: unknown, {dataLoader}: GQLContext) => { - return dataLoader.get('users').load(createdBy) - } - }, - replies: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Threadable))), - description: 'the replies to this threadable item', - resolve: ({replies}: {replies: ThreadableDB[]}) => replies || [] - }, - discussionId: { - type: GraphQLID, - description: - 'The FK of the discussion this task was created in. Null if task was not created in a discussion', - // can remove the threadId after 2021-07-01 - resolve: ({discussionId, threadId}: {discussionId: string; threadId: string}) => - discussionId || threadId - }, - threadParentId: { - type: GraphQLID, - description: 'the parent, if this threadable is a reply, else null' - }, - threadSortOrder: { - type: GraphQLFloat, - description: 'the order of this threadable, relative to threadParentId' - }, - updatedAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The timestamp the item was updated' - } -}) - -const Threadable: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: 'Threadable', - fields: {} -}) - -const {connectionType, edgeType} = connectionDefinitions({ - name: Threadable.name, - nodeType: Threadable, - edgeFields: () => ({ - cursor: { - type: GraphQLString - } - }), - connectionFields: () => ({ - error: { - type: GraphQLString, - description: 'Any errors that prevented the query from returning the full results' - }, - pageInfo: { - type: PageInfo, - description: 'Page info with strings (sortOrder) as cursors' - } - }) -}) - -export const ThreadableConnection = connectionType -export const ThreadableEdge = edgeType -export default Threadable diff --git a/packages/server/postgres/migrations/1724780116674_Comment-phase1.ts b/packages/server/postgres/migrations/1724780116674_Comment-phase1.ts new file mode 100644 index 00000000000..da38171f0e3 --- /dev/null +++ b/packages/server/postgres/migrations/1724780116674_Comment-phase1.ts @@ -0,0 +1,48 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DO $$ + BEGIN + CREATE TABLE IF NOT EXISTS "Comment" ( + "id" VARCHAR(100) PRIMARY KEY, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "isActive" BOOLEAN NOT NULL DEFAULT TRUE, + "isAnonymous" BOOLEAN NOT NULL DEFAULT FALSE, + "threadParentId" VARCHAR(100), + "reactjis" "Reactji"[] NOT NULL DEFAULT array[]::"Reactji"[], + "content" JSONB NOT NULL, + "createdBy" VARCHAR(100) NOT NULL, + "plaintextContent" VARCHAR(2000) NOT NULL, + "discussionId" VARCHAR(100) NOT NULL, + "threadSortOrder" INTEGER NOT NULL, + CONSTRAINT "fk_createdBy" + FOREIGN KEY("createdBy") + REFERENCES "User"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_discussionId" + FOREIGN KEY("discussionId") + REFERENCES "Discussion"("id") + ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS "idx_Comment_threadParentId" ON "Comment"("threadParentId"); + CREATE INDEX IF NOT EXISTS "idx_Comment_createdBy" ON "Comment"("createdBy"); + CREATE INDEX IF NOT EXISTS "idx_Comment_discussionId" ON "Comment"("discussionId"); + END $$; +`) + // TODO add constraint threadParentId in phase 2 + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE "Comment"; + ` /* Do undo magic */) + await client.end() +} diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index 481164d8916..9a610b1de19 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -210,3 +210,21 @@ export const selectSlackAuths = () => getKysely().selectFrom('SlackAuth').select export const selectSlackNotifications = () => getKysely().selectFrom('SlackNotification').selectAll() + +export const selectComments = () => + getKysely() + .selectFrom('Comment') + .select([ + 'id', + 'createdAt', + 'isActive', + 'isAnonymous', + 'threadParentId', + 'updatedAt', + 'content', + 'createdBy', + 'plaintextContent', + 'discussionId', + 'threadSortOrder' + ]) + .select(({fn}) => [fn('to_json', ['reactjis']).as('reactjis')]) From 217606e11875666e512097078974d1aa062f9e3f Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 27 Aug 2024 13:01:42 -0700 Subject: [PATCH 02/16] fix: refactor comment to SDL Signed-off-by: Matt Krick --- codegen.json | 2 + .../server/graphql/mutations/deleteComment.ts | 77 --------------- .../graphql/mutations/updateCommentContent.ts | 93 ------------------- .../graphql/public/mutations/deleteComment.ts | 58 ++++++++++++ .../public/mutations/updateCommentContent.ts | 71 ++++++++++++++ .../public/types/DeleteCommentSuccess.ts | 13 +++ .../types/UpdateCommentContentSuccess.ts | 13 +++ packages/server/graphql/rootMutation.ts | 4 - packages/server/graphql/rootTypes.ts | 2 - packages/server/graphql/types/Comment.ts | 29 ------ .../graphql/types/DeleteCommentPayload.ts | 24 ----- .../types/UpdateCommentContentPayload.ts | 24 ----- 12 files changed, 157 insertions(+), 253 deletions(-) delete mode 100644 packages/server/graphql/mutations/deleteComment.ts delete mode 100644 packages/server/graphql/mutations/updateCommentContent.ts create mode 100644 packages/server/graphql/public/mutations/deleteComment.ts create mode 100644 packages/server/graphql/public/mutations/updateCommentContent.ts create mode 100644 packages/server/graphql/public/types/DeleteCommentSuccess.ts create mode 100644 packages/server/graphql/public/types/UpdateCommentContentSuccess.ts delete mode 100644 packages/server/graphql/types/Comment.ts delete mode 100644 packages/server/graphql/types/DeleteCommentPayload.ts delete mode 100644 packages/server/graphql/types/UpdateCommentContentPayload.ts diff --git a/codegen.json b/codegen.json index 8ea9beb81b1..50e40fa3bbe 100644 --- a/codegen.json +++ b/codegen.json @@ -49,6 +49,8 @@ "SetSlackNotificationPayload": "./types/SetSlackNotificationPayload#SetSlackNotificationPayloadSource", "SetDefaultSlackChannelSuccess": "./types/SetDefaultSlackChannelSuccess#SetDefaultSlackChannelSuccessSource", "AddCommentSuccess": "./types/AddCommentSuccess#AddCommentSuccessSource", + "DeleteCommentSuccess": "./types/DeleteCommentSuccess#DeleteCommentSuccessSource", + "UpdateCommentContentSuccess": "./types/UpdateCommentContentSuccess#UpdateCommentContentSuccessSource", "AddSlackAuthPayload": "./types/AddSlackAuthPayload#AddSlackAuthPayloadSource", "RemoveAgendaItemPayload": "./types/RemoveAgendaItemPayload#RemoveAgendaItemPayloadSource", "AddAgendaItemPayload": "./types/AddAgendaItemPayload#AddAgendaItemPayloadSource", diff --git a/packages/server/graphql/mutations/deleteComment.ts b/packages/server/graphql/mutations/deleteComment.ts deleted file mode 100644 index e2a3ee08276..00000000000 --- a/packages/server/graphql/mutations/deleteComment.ts +++ /dev/null @@ -1,77 +0,0 @@ -import {GraphQLID, GraphQLNonNull} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' -import {PARABOL_AI_USER_ID} from '../../../client/utils/constants' -import getRethink from '../../database/rethinkDriver' -import getKysely from '../../postgres/getKysely' -import {getUserId} from '../../utils/authorization' -import publish from '../../utils/publish' -import {GQLContext} from '../graphql' -import DeleteCommentPayload from '../types/DeleteCommentPayload' - -type DeleteCommentMutationVariables = { - commentId: string - meetingId: string -} - -const deleteComment = { - type: new GraphQLNonNull(DeleteCommentPayload), - description: `Delete a comment from a discussion`, - args: { - commentId: { - type: new GraphQLNonNull(GraphQLID) - }, - meetingId: { - type: new GraphQLNonNull(GraphQLID) - } - }, - resolve: async ( - _source: unknown, - {commentId, meetingId}: DeleteCommentMutationVariables, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) => { - const r = await getRethink() - const viewerId = getUserId(authToken) - const operationId = dataLoader.share() - const subOptions = {mutatorId, operationId} - const now = new Date() - - //AUTH - const meetingMemberId = toTeamMemberId(meetingId, viewerId) - const [comment, viewerMeetingMember] = await Promise.all([ - dataLoader.get('comments').load(commentId), - dataLoader.get('meetingMembers').load(meetingMemberId), - dataLoader.get('newMeetings').load(meetingId) - ]) - if (!comment || !comment.isActive) { - return {error: {message: 'Comment does not exist'}} - } - if (!viewerMeetingMember) { - return {error: {message: `Not a member of the meeting`}} - } - const {createdBy, discussionId} = comment - const discussion = await dataLoader.get('discussions').loadNonNull(discussionId) - if (discussion.meetingId !== meetingId) { - return {error: {message: `Comment is not from this meeting`}} - } - if (createdBy !== viewerId && createdBy !== PARABOL_AI_USER_ID) { - return {error: {message: 'Can only delete your own comment or Parabol AI comments'}} - } - - await r.table('Comment').get(commentId).update({isActive: false, updatedAt: now}).run() - await getKysely() - .updateTable('Comment') - .set({updatedAt: now}) - .where('id', '=', commentId) - .execute() - dataLoader.clearAll('comments') - const data = {commentId} - - if (meetingId) { - publish(SubscriptionChannel.MEETING, meetingId, 'DeleteCommentSuccess', data, subOptions) - } - return data - } -} - -export default deleteComment diff --git a/packages/server/graphql/mutations/updateCommentContent.ts b/packages/server/graphql/mutations/updateCommentContent.ts deleted file mode 100644 index 307a82044af..00000000000 --- a/packages/server/graphql/mutations/updateCommentContent.ts +++ /dev/null @@ -1,93 +0,0 @@ -import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractTextFromDraftString' -import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' -import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' -import {PARABOL_AI_USER_ID} from '../../../client/utils/constants' -import getRethink from '../../database/rethinkDriver' -import getKysely from '../../postgres/getKysely' -import {getUserId} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import UpdateCommentContentPayload from '../types/UpdateCommentContentPayload' - -export default { - type: UpdateCommentContentPayload, - description: 'Update the content of a comment', - args: { - commentId: { - type: new GraphQLNonNull(GraphQLID) - }, - content: { - type: new GraphQLNonNull(GraphQLString), - description: 'A stringified draft-js document containing thoughts' - }, - meetingId: { - type: new GraphQLNonNull(GraphQLID) - } - }, - async resolve( - _source: unknown, - {commentId, content, meetingId}: {commentId: string; content: string; meetingId: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const operationId = dataLoader.share() - const now = new Date() - const subOptions = {operationId, mutatorId} - - // AUTH - const viewerId = getUserId(authToken) - const meetingMemberId = toTeamMemberId(meetingId, viewerId) - const [comment, viewerMeetingMember] = await Promise.all([ - dataLoader.get('comments').load(commentId), - dataLoader.get('meetingMembers').load(meetingMemberId) - ]) - if (!comment || !comment.isActive) { - return standardError(new Error('comment not found'), {userId: viewerId}) - } - if (!viewerMeetingMember) { - return {error: {message: `Not a member of the meeting`}} - } - const {createdBy, discussionId} = comment - const discussion = await dataLoader.get('discussions').loadNonNull(discussionId) - if (discussion.meetingId !== meetingId) { - return {error: {message: `Comment is not from this meeting`}} - } - if (createdBy !== viewerId && createdBy !== PARABOL_AI_USER_ID) { - return {error: {message: 'Can only update your own comment or Parabol AI comments'}} - } - - // VALIDATION - const normalizedContent = normalizeRawDraftJS(content) - - // RESOLUTION - const plaintextContent = extractTextFromDraftString(normalizedContent) - await r - .table('Comment') - .get(commentId) - .update({content: normalizedContent, plaintextContent, updatedAt: now}) - .run() - await getKysely() - .updateTable('Comment') - .set({content: normalizedContent, plaintextContent}) - .where('id', '=', commentId) - .execute() - dataLoader.clearAll('comments') - // :TODO: (jmtaber129): diff new and old comment content for mentions and handle notifications - // appropriately. - - const data = {commentId} - if (meetingId) { - publish( - SubscriptionChannel.MEETING, - meetingId, - 'UpdateCommentContentSuccess', - data, - subOptions - ) - } - return data - } -} diff --git a/packages/server/graphql/public/mutations/deleteComment.ts b/packages/server/graphql/public/mutations/deleteComment.ts new file mode 100644 index 00000000000..957cbda541a --- /dev/null +++ b/packages/server/graphql/public/mutations/deleteComment.ts @@ -0,0 +1,58 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' +import {PARABOL_AI_USER_ID} from '../../../../client/utils/constants' +import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' +import {getUserId} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import {MutationResolvers} from '../resolverTypes' + +const deleteComment: MutationResolvers['deleteComment'] = async ( + _source, + {commentId, meetingId}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const r = await getRethink() + const viewerId = getUserId(authToken) + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + const now = new Date() + + //AUTH + const meetingMemberId = toTeamMemberId(meetingId, viewerId) + const [comment, viewerMeetingMember] = await Promise.all([ + dataLoader.get('comments').load(commentId), + dataLoader.get('meetingMembers').load(meetingMemberId), + dataLoader.get('newMeetings').load(meetingId) + ]) + if (!comment || !comment.isActive) { + return {error: {message: 'Comment does not exist'}} + } + if (!viewerMeetingMember) { + return {error: {message: `Not a member of the meeting`}} + } + const {createdBy, discussionId} = comment + const discussion = await dataLoader.get('discussions').loadNonNull(discussionId) + if (discussion.meetingId !== meetingId) { + return {error: {message: `Comment is not from this meeting`}} + } + if (createdBy !== viewerId && createdBy !== PARABOL_AI_USER_ID) { + return {error: {message: 'Can only delete your own comment or Parabol AI comments'}} + } + + await r.table('Comment').get(commentId).update({isActive: false, updatedAt: now}).run() + await getKysely() + .updateTable('Comment') + .set({updatedAt: now}) + .where('id', '=', commentId) + .execute() + dataLoader.clearAll('comments') + const data = {commentId} + + if (meetingId) { + publish(SubscriptionChannel.MEETING, meetingId, 'DeleteCommentSuccess', data, subOptions) + } + return data +} + +export default deleteComment diff --git a/packages/server/graphql/public/mutations/updateCommentContent.ts b/packages/server/graphql/public/mutations/updateCommentContent.ts new file mode 100644 index 00000000000..3f11febcc72 --- /dev/null +++ b/packages/server/graphql/public/mutations/updateCommentContent.ts @@ -0,0 +1,71 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractTextFromDraftString' +import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' +import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' +import {PARABOL_AI_USER_ID} from '../../../../client/utils/constants' +import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' +import {getUserId} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const updateCommentContent: MutationResolvers['updateCommentContent'] = async ( + _source, + {commentId, content, meetingId}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const r = await getRethink() + const operationId = dataLoader.share() + const now = new Date() + const subOptions = {operationId, mutatorId} + + // AUTH + const viewerId = getUserId(authToken) + const meetingMemberId = toTeamMemberId(meetingId, viewerId) + const [comment, viewerMeetingMember] = await Promise.all([ + dataLoader.get('comments').load(commentId), + dataLoader.get('meetingMembers').load(meetingMemberId) + ]) + if (!comment || !comment.isActive) { + return standardError(new Error('comment not found'), {userId: viewerId}) + } + if (!viewerMeetingMember) { + return {error: {message: `Not a member of the meeting`}} + } + const {createdBy, discussionId} = comment + const discussion = await dataLoader.get('discussions').loadNonNull(discussionId) + if (discussion.meetingId !== meetingId) { + return {error: {message: `Comment is not from this meeting`}} + } + if (createdBy !== viewerId && createdBy !== PARABOL_AI_USER_ID) { + return {error: {message: 'Can only update your own comment or Parabol AI comments'}} + } + + // VALIDATION + const normalizedContent = normalizeRawDraftJS(content) + + // RESOLUTION + const plaintextContent = extractTextFromDraftString(normalizedContent) + await r + .table('Comment') + .get(commentId) + .update({content: normalizedContent, plaintextContent, updatedAt: now}) + .run() + await getKysely() + .updateTable('Comment') + .set({content: normalizedContent, plaintextContent}) + .where('id', '=', commentId) + .execute() + dataLoader.clearAll('comments') + // :TODO: (jmtaber129): diff new and old comment content for mentions and handle notifications + // appropriately. + + const data = {commentId} + if (meetingId) { + publish(SubscriptionChannel.MEETING, meetingId, 'UpdateCommentContentSuccess', data, subOptions) + } + return data +} + +export default updateCommentContent diff --git a/packages/server/graphql/public/types/DeleteCommentSuccess.ts b/packages/server/graphql/public/types/DeleteCommentSuccess.ts new file mode 100644 index 00000000000..b9f2c86f76d --- /dev/null +++ b/packages/server/graphql/public/types/DeleteCommentSuccess.ts @@ -0,0 +1,13 @@ +import {DeleteCommentSuccessResolvers} from '../resolverTypes' + +export type DeleteCommentSuccessSource = { + commentId: string +} + +const DeleteCommentSuccess: DeleteCommentSuccessResolvers = { + comment: async ({commentId}, _args, {dataLoader}) => { + return dataLoader.get('comments').load(commentId) + } +} + +export default DeleteCommentSuccess diff --git a/packages/server/graphql/public/types/UpdateCommentContentSuccess.ts b/packages/server/graphql/public/types/UpdateCommentContentSuccess.ts new file mode 100644 index 00000000000..62c7382325b --- /dev/null +++ b/packages/server/graphql/public/types/UpdateCommentContentSuccess.ts @@ -0,0 +1,13 @@ +import {UpdateCommentContentSuccessResolvers} from '../resolverTypes' + +export type UpdateCommentContentSuccessSource = { + commentId: string +} + +const UpdateCommentContentSuccess: UpdateCommentContentSuccessResolvers = { + comment: async ({commentId}, _args, {dataLoader}) => { + return dataLoader.get('comments').load(commentId) + } +} + +export default UpdateCommentContentSuccess diff --git a/packages/server/graphql/rootMutation.ts b/packages/server/graphql/rootMutation.ts index 3dd06c921fd..ab88c2e03ba 100644 --- a/packages/server/graphql/rootMutation.ts +++ b/packages/server/graphql/rootMutation.ts @@ -19,7 +19,6 @@ import createPoll from './mutations/createPoll' import createReflection from './mutations/createReflection' import createTask from './mutations/createTask' import createTaskIntegration from './mutations/createTaskIntegration' -import deleteComment from './mutations/deleteComment' import deleteTask from './mutations/deleteTask' import deleteUser from './mutations/deleteUser' import denyPushInvitation from './mutations/denyPushInvitation' @@ -91,7 +90,6 @@ import startDraggingReflection from './mutations/startDraggingReflection' import startSprintPoker from './mutations/startSprintPoker' import toggleTeamDrawer from './mutations/toggleTeamDrawer' import updateAzureDevOpsDimensionField from './mutations/updateAzureDevOpsDimensionField' -import updateCommentContent from './mutations/updateCommentContent' import updateDragLocation from './mutations/updateDragLocation' import updateGitHubDimensionField from './mutations/updateGitHubDimensionField' import updateNewCheckInQuestion from './mutations/updateNewCheckInQuestion' @@ -130,7 +128,6 @@ export default new GraphQLObjectType({ createOAuth1AuthorizeUrl, createReflection, createTask, - deleteComment, deleteTask, deleteUser, denyPushInvitation, @@ -188,7 +185,6 @@ export default new GraphQLObjectType({ startDraggingReflection, startSprintPoker, setTaskHighlight, - updateCommentContent, oldUpdateCreditCard, updatePokerTemplateDimensionScale, updatePokerTemplateScaleValue, diff --git a/packages/server/graphql/rootTypes.ts b/packages/server/graphql/rootTypes.ts index d2c9f7cfe3e..6ef6af5d374 100644 --- a/packages/server/graphql/rootTypes.ts +++ b/packages/server/graphql/rootTypes.ts @@ -1,4 +1,3 @@ -import Comment from './types/Comment' import IntegrationProviderOAuth1 from './types/IntegrationProviderOAuth1' import IntegrationProviderOAuth2 from './types/IntegrationProviderOAuth2' import IntegrationProviderWebhook from './types/IntegrationProviderWebhook' @@ -22,7 +21,6 @@ const rootTypes = [ TimelineEventCompletedRetroMeeting, TimelineEventCompletedActionMeeting, TimelineEventPokerComplete, - Comment, JiraDimensionField, RenamePokerTemplatePayload, UserTiersCount diff --git a/packages/server/graphql/types/Comment.ts b/packages/server/graphql/types/Comment.ts deleted file mode 100644 index 93b25e00d4f..00000000000 --- a/packages/server/graphql/types/Comment.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import connectionDefinitions from '../connectionDefinitions' -import GraphQLISO8601Type from './GraphQLISO8601Type' -import PageInfoDateCursor from './PageInfoDateCursor' - -const Comment = new GraphQLObjectType({ - name: 'Comment', - fields: {} -}) - -const {connectionType, edgeType} = connectionDefinitions({ - name: Comment.name, - nodeType: Comment, - edgeFields: () => ({ - cursor: { - type: GraphQLISO8601Type - } - }), - connectionFields: () => ({ - pageInfo: { - type: PageInfoDateCursor, - description: 'Page info with cursors coerced to ISO8601 dates' - } - }) -}) - -export const TaskConnection = connectionType -export const TaskEdge = edgeType -export default Comment diff --git a/packages/server/graphql/types/DeleteCommentPayload.ts b/packages/server/graphql/types/DeleteCommentPayload.ts deleted file mode 100644 index 361812d4cb4..00000000000 --- a/packages/server/graphql/types/DeleteCommentPayload.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {GraphQLID, GraphQLNonNull, GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import Comment from './Comment' -import makeMutationPayload from './makeMutationPayload' - -export const DeleteCommentSuccess = new GraphQLObjectType({ - name: 'DeleteCommentSuccess', - fields: () => ({ - commentId: { - type: new GraphQLNonNull(GraphQLID) - }, - comment: { - type: new GraphQLNonNull(Comment), - description: 'the comment just deleted', - resolve: async ({commentId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('comments').load(commentId) - } - } - }) -}) - -const DeleteCommentPayload = makeMutationPayload('DeleteCommentPayload', DeleteCommentSuccess) - -export default DeleteCommentPayload diff --git a/packages/server/graphql/types/UpdateCommentContentPayload.ts b/packages/server/graphql/types/UpdateCommentContentPayload.ts deleted file mode 100644 index 9d028791b90..00000000000 --- a/packages/server/graphql/types/UpdateCommentContentPayload.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {GraphQLNonNull, GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import Comment from './Comment' -import makeMutationPayload from './makeMutationPayload' - -export const UpdateCommentContentSuccess = new GraphQLObjectType({ - name: 'UpdateCommentContentSuccess', - fields: () => ({ - comment: { - type: new GraphQLNonNull(Comment), - description: 'the comment with updated content', - resolve: async ({commentId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('comments').load(commentId) - } - } - }) -}) - -const UpdateCommentContentPayload = makeMutationPayload( - 'UpdateCommentContentPayload', - UpdateCommentContentSuccess -) - -export default UpdateCommentContentPayload From fd13993d4c6219caa0aafee5a3220cef0126f7ba Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 27 Aug 2024 13:11:44 -0700 Subject: [PATCH 03/16] add comment Signed-off-by: Matt Krick --- packages/server/graphql/public/types/Threadable.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/server/graphql/public/types/Threadable.ts b/packages/server/graphql/public/types/Threadable.ts index c2520177b3a..0964be02ce2 100644 --- a/packages/server/graphql/public/types/Threadable.ts +++ b/packages/server/graphql/public/types/Threadable.ts @@ -16,7 +16,10 @@ const Threadable: ThreadableResolvers = { return createdBy ? dataLoader.get('users').loadNonNull(createdBy) : null }, replies: ({replies}) => replies || [], - threadSortOrder: ({threadSortOrder}) => (isNaN(threadSortOrder) ? 0 : Math.trunc(threadSortOrder)) + // Can remove after Comment is in PG + threadSortOrder: ({threadSortOrder}) => { + return isNaN(threadSortOrder) ? 0 : Math.trunc(threadSortOrder) + } } export default Threadable From 424a54b253dcc43eb889df917a3a0f8167a844bd Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 28 Aug 2024 17:42:15 -0700 Subject: [PATCH 04/16] migrate existing Comments Signed-off-by: Matt Krick --- .../1724780116674_Comment-phase1.ts | 4 +- .../1724884922936_Comment-phase2.ts | 136 ++++++++++++++++++ 2 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 packages/server/postgres/migrations/1724884922936_Comment-phase2.ts diff --git a/packages/server/postgres/migrations/1724780116674_Comment-phase1.ts b/packages/server/postgres/migrations/1724780116674_Comment-phase1.ts index da38171f0e3..5090679072f 100644 --- a/packages/server/postgres/migrations/1724780116674_Comment-phase1.ts +++ b/packages/server/postgres/migrations/1724780116674_Comment-phase1.ts @@ -16,14 +16,14 @@ export async function up() { "threadParentId" VARCHAR(100), "reactjis" "Reactji"[] NOT NULL DEFAULT array[]::"Reactji"[], "content" JSONB NOT NULL, - "createdBy" VARCHAR(100) NOT NULL, + "createdBy" VARCHAR(100), "plaintextContent" VARCHAR(2000) NOT NULL, "discussionId" VARCHAR(100) NOT NULL, "threadSortOrder" INTEGER NOT NULL, CONSTRAINT "fk_createdBy" FOREIGN KEY("createdBy") REFERENCES "User"("id") - ON DELETE CASCADE, + ON DELETE SET NULL, CONSTRAINT "fk_discussionId" FOREIGN KEY("discussionId") REFERENCES "Discussion"("id") diff --git a/packages/server/postgres/migrations/1724884922936_Comment-phase2.ts b/packages/server/postgres/migrations/1724884922936_Comment-phase2.ts new file mode 100644 index 00000000000..9e5f6d3e893 --- /dev/null +++ b/packages/server/postgres/migrations/1724884922936_Comment-phase2.ts @@ -0,0 +1,136 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {r} from 'rethinkdb-ts' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + + try { + console.log('Adding index') + await r + .table('Comment') + .indexCreate('updatedAtId', (row: any) => [row('updatedAt'), row('id')]) + .run() + await r.table('Comment').indexWait().run() + } catch { + // index already exists + } + + console.log('Adding index complete') + const MAX_PG_PARAMS = 65545 + const PG_COLS = [ + 'id', + 'createdAt', + 'updatedAt', + 'isActive', + 'isAnonymous', + 'threadParentId', + 'reactjis', + 'content', + 'createdBy', + 'plaintextContent', + 'discussionId', + 'threadSortOrder' + ] as const + type Comment = { + [K in (typeof PG_COLS)[number]]: any + } + const BATCH_SIZE = Math.trunc(MAX_PG_PARAMS / PG_COLS.length) + + let curUpdatedAt = r.minval + let curId = r.minval + for (let i = 0; i < 1e6; i++) { + console.log('inserting row', i * BATCH_SIZE, String(curUpdatedAt), String(curId)) + const rawRowsToInsert = (await r + .table('Comment') + .between([curUpdatedAt, curId], [r.maxval, r.maxval], { + index: 'updatedAtId', + leftBound: 'open', + rightBound: 'closed' + }) + .orderBy({index: 'updatedAtId'}) + .limit(BATCH_SIZE) + .pluck(...PG_COLS) + .run()) as Comment[] + + const rowsToInsert = rawRowsToInsert + .map((row) => { + const {plaintextContent, threadSortOrder, reactjis, ...rest} = row as any + return { + ...rest, + plaintextContent: plaintextContent.slice(0, 2000), + threadSortOrder: threadSortOrder ? Math.trunc(threadSortOrder) : 0, + reactjis: reactjis?.map((r: any) => `(${r.id},${r.userId})`) ?? [] + } + }) + .filter((row) => row.discussionId) + + if (rowsToInsert.length === 0) break + const lastRow = rowsToInsert[rowsToInsert.length - 1] + curUpdatedAt = lastRow.updatedAt + curId = lastRow.id + await Promise.all( + rowsToInsert.map(async (row) => { + try { + await pg + .insertInto('Comment') + .values(row) + .onConflict((oc) => oc.doNothing()) + .execute() + } catch (e) { + if (e.constraint === 'fk_createdBy') { + await pg + .insertInto('Comment') + .values({...row, createdBy: null}) + .onConflict((oc) => oc.doNothing()) + .execute() + return + } + if (e.constraint === 'fk_discussionId') { + console.log(`Skipping ${row.id} because it has no discussion`) + return + } + console.log(e, row) + } + }) + ) + } + + // if the threadParentId references an id that does not exist, set it to null + console.log('adding threadParentId constraint') + await pg + .updateTable('Comment') + .set({threadParentId: null}) + .where(({eb, selectFrom}) => + eb( + 'id', + 'in', + selectFrom('Comment as child') + .select('child.id') + .leftJoin('Comment as parent', 'child.threadParentId', 'parent.id') + .where('parent.id', 'is', null) + .where('child.threadParentId', 'is not', null) + ) + ) + .execute() + await pg.schema + .alterTable('Comment') + .addForeignKeyConstraint('fk_threadParentId', ['threadParentId'], 'Comment', ['id']) + .onDelete('set null') + .execute() +} + +export async function down() { + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql`TRUNCATE TABLE "Comment" CASCADE`.execute(pg) +} From c0278ae341df2a616fa68fb3ed2b4edb3e305b91 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 28 Aug 2024 18:40:37 -0700 Subject: [PATCH 05/16] switch reads to PG Signed-off-by: Matt Krick --- codegen.json | 2 +- .../indexing/retrospectiveDiscussionTopic.ts | 4 +- .../helpers/publishSimilarRetroTopics.ts | 3 - packages/server/database/rethinkDriver.ts | 5 -- packages/server/database/types/Comment.ts | 63 ------------------- packages/server/database/types/Reactji.ts | 15 ----- .../dataloader/foreignKeyLoaderMakers.ts | 4 +- .../dataloader/primaryKeyLoaderMakers.ts | 2 +- .../rethinkForeignKeyLoaderMakers.ts | 17 ----- .../rethinkPrimaryKeyLoaderMakers.ts | 1 - .../helpers/addAIGeneratedContentToThreads.ts | 39 +++++------- .../mutations/helpers/calculateEngagement.ts | 2 +- .../helpers/notifications/SlackNotifier.ts | 2 +- .../resetRetroMeetingToGroupStage.ts | 5 -- .../mutations/generateMeetingSummary.ts | 12 ++-- .../private/mutations/hardDeleteUser.ts | 12 +--- .../graphql/public/mutations/addComment.ts | 24 +++---- .../public/mutations/addReactjiToReactable.ts | 36 +---------- .../graphql/public/mutations/deleteComment.ts | 3 - .../public/mutations/helpers/getTopics.ts | 10 +-- .../public/mutations/updateCommentContent.ts | 8 --- .../graphql/public/types/AddCommentSuccess.ts | 2 +- .../server/graphql/public/types/Comment.ts | 6 +- .../public/types/DeleteCommentSuccess.ts | 2 +- .../public/types/NotifyDiscussionMentioned.ts | 4 +- .../public/types/NotifyResponseReplied.ts | 4 +- .../types/UpdateCommentContentSuccess.ts | 2 +- .../graphql/resolvers/resolveReactjis.ts | 4 +- .../resolvers/resolveThreadableConnection.ts | 2 +- packages/server/postgres/select.ts | 2 +- packages/server/postgres/types/index.d.ts | 5 +- packages/server/utils/analytics/analytics.ts | 2 +- packages/server/utils/getGroupedReactjis.ts | 4 +- 33 files changed, 72 insertions(+), 236 deletions(-) delete mode 100644 packages/server/database/types/Comment.ts delete mode 100644 packages/server/database/types/Reactji.ts diff --git a/codegen.json b/codegen.json index 50e40fa3bbe..bb7e83c6aa9 100644 --- a/codegen.json +++ b/codegen.json @@ -84,7 +84,7 @@ "AzureDevOpsRemoteProject": "./types/AzureDevOpsRemoteProject#AzureDevOpsRemoteProjectSource", "AzureDevOpsWorkItem": "../../dataloader/azureDevOpsLoaders#AzureDevOpsWorkItem", "BatchArchiveTasksSuccess": "./types/BatchArchiveTasksSuccess#BatchArchiveTasksSuccessSource", - "Comment": "../../database/types/Comment#default as CommentDB", + "Comment": "../../postgres/types/index#Comment as CommentDB", "Company": "./types/Company#CompanySource", "CreateGcalEventInput": "./types/CreateGcalEventInput#default", "CreateImposterTokenPayload": "./types/CreateImposterTokenPayload#CreateImposterTokenPayloadSource", diff --git a/packages/embedder/indexing/retrospectiveDiscussionTopic.ts b/packages/embedder/indexing/retrospectiveDiscussionTopic.ts index ac49248abfd..36eca506754 100644 --- a/packages/embedder/indexing/retrospectiveDiscussionTopic.ts +++ b/packages/embedder/indexing/retrospectiveDiscussionTopic.ts @@ -1,7 +1,7 @@ -import Comment from 'parabol-server/database/types/Comment' import {isMeetingRetrospective} from 'parabol-server/database/types/MeetingRetrospective' import {DataLoaderInstance} from 'parabol-server/dataloader/RootDataLoader' import prettier from 'prettier' +import {Comment} from '../../server/postgres/types' import {inferLanguage} from '../inferLanguage' import {ISO6391} from '../iso6393To1' @@ -154,7 +154,7 @@ export const createTextFromRetrospectiveDiscussionTopic = async ( }) as Comment[] const filteredComments = sortedComments.filter( - (c) => !IGNORE_COMMENT_USER_IDS.includes(c.createdBy) + (c) => !IGNORE_COMMENT_USER_IDS.includes(c.createdBy!) ) if (filteredComments.length) { markdown += `Further discussion was made:\n` diff --git a/packages/embedder/workflows/helpers/publishSimilarRetroTopics.ts b/packages/embedder/workflows/helpers/publishSimilarRetroTopics.ts index 8490667c5eb..217ee63073b 100644 --- a/packages/embedder/workflows/helpers/publishSimilarRetroTopics.ts +++ b/packages/embedder/workflows/helpers/publishSimilarRetroTopics.ts @@ -1,7 +1,6 @@ import {SubscriptionChannel} from '../../../client/types/constEnums' import makeAppURL from '../../../client/utils/makeAppURL' import appOrigin from '../../../server/appOrigin' -import getRethink from '../../../server/database/rethinkDriver' import {DataLoaderInstance} from '../../../server/dataloader/RootDataLoader' import {isRetroMeeting} from '../../../server/graphql/meetingTypePredicates' import { @@ -54,7 +53,6 @@ export const publishSimilarRetroTopics = async ( similarEmbeddings: {embeddingsMetadataId: number; similarity: number}[], dataLoader: DataLoaderInstance ) => { - const r = await getRethink() const pg = getKysely() const links = await Promise.all( similarEmbeddings.map((se) => makeSimilarDiscussionLink(se, dataLoader)) @@ -69,7 +67,6 @@ export const publishSimilarRetroTopics = async ( buildCommentContentBlock('🤖 Related Discussions', `
    ${listItems}
`), 2 ) - await r.table('Comment').insert(relatedDiscussionsComment).run() await pg .insertInto('Comment') .values({ diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index b45f322d350..055b5800142 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -3,7 +3,6 @@ import TeamInvitation from '../database/types/TeamInvitation' import {AnyMeeting, AnyMeetingTeamMember} from '../postgres/types/Meeting' import getRethinkConfig from './getRethinkConfig' import {R} from './stricterR' -import Comment from './types/Comment' import MassInvitation from './types/MassInvitation' import NotificationKickedOut from './types/NotificationKickedOut' import NotificationMeetingStageTimeLimitEnd from './types/NotificationMeetingStageTimeLimitEnd' @@ -21,10 +20,6 @@ import RetrospectivePrompt from './types/RetrospectivePrompt' import Task from './types/Task' export type RethinkSchema = { - Comment: { - type: Comment - index: 'discussionId' - } ReflectPrompt: { type: RetrospectivePrompt index: 'teamId' | 'templateId' diff --git a/packages/server/database/types/Comment.ts b/packages/server/database/types/Comment.ts deleted file mode 100644 index 5fee86fdade..00000000000 --- a/packages/server/database/types/Comment.ts +++ /dev/null @@ -1,63 +0,0 @@ -import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractTextFromDraftString' -import generateUID from '../../generateUID' -import Reactji from './Reactji' - -interface CommentInput { - id?: string - createdAt?: Date | null - isActive?: boolean | null - isAnonymous?: boolean | null - threadParentId?: string | null - reactjis?: Reactji[] | null - updatedAt?: Date | null - content: string - createdBy: string - plaintextContent?: string // the plaintext version of content - discussionId: string - threadSortOrder: number -} - -export default class Comment { - id: string - createdAt: Date - isActive: boolean - isAnonymous: boolean - threadParentId?: string - reactjis: Reactji[] - updatedAt: Date - content: string - // userId of the creator - createdBy: string - plaintextContent: string - discussionId: string - threadSortOrder: number - - constructor(input: CommentInput) { - const { - id, - content, - createdAt, - createdBy, - plaintextContent, - threadParentId, - threadSortOrder, - updatedAt, - discussionId, - isActive, - isAnonymous, - reactjis - } = input - this.id = id || generateUID() - this.content = content - this.createdAt = createdAt || new Date() - this.createdBy = createdBy - this.plaintextContent = plaintextContent || extractTextFromDraftString(content) - this.threadSortOrder = threadSortOrder - this.threadParentId = threadParentId || undefined - this.updatedAt = updatedAt || new Date() - this.discussionId = discussionId - this.isActive = isActive ?? true - this.isAnonymous = isAnonymous ?? false - this.reactjis = reactjis ?? [] - } -} diff --git a/packages/server/database/types/Reactji.ts b/packages/server/database/types/Reactji.ts deleted file mode 100644 index fa36d325351..00000000000 --- a/packages/server/database/types/Reactji.ts +++ /dev/null @@ -1,15 +0,0 @@ -interface Input { - userId: string - id: string -} - -export default class Reactji { - userId: string - id: string - constructor(input: Input) { - const {id, userId} = input - this.userId = userId - // not a GUID, the client will need a GUID - this.id = id - } -} diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index d65ab6b9385..0f08e21d5a6 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -207,8 +207,8 @@ export const slackNotificationsByTeamId = foreignKeyLoaderMaker( } ) -export const _pgcommentsByDiscussionId = foreignKeyLoaderMaker( - '_pgcomments', +export const commentsByDiscussionId = foreignKeyLoaderMaker( + 'comments', 'discussionId', async (discussionIds) => { // include deleted comments so we can replace them with tombstones diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index c57d6b9adab..adeee70820a 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -107,6 +107,6 @@ export const slackNotifications = primaryKeyLoaderMaker((ids: readonly string[]) return selectSlackNotifications().where('id', 'in', ids).execute() }) -export const _pgcomments = primaryKeyLoaderMaker((ids: readonly string[]) => { +export const comments = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectComments().where('id', 'in', ids).execute() }) diff --git a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts index 3f9392197a6..b0bf83cf10a 100644 --- a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts @@ -15,23 +15,6 @@ export const activeMeetingsByTeamId = new RethinkForeignKeyLoaderMaker( .run() } ) - -export const commentsByDiscussionId = new RethinkForeignKeyLoaderMaker( - 'comments', - 'discussionId', - async (discussionIds) => { - const r = await getRethink() - return ( - r - .table('Comment') - .getAll(r.args(discussionIds), {index: 'discussionId'}) - // include deleted comments so we can replace them with tombstones - // .filter({isActive: true}) - .run() - ) - } -) - export const completedMeetingsByTeamId = new RethinkForeignKeyLoaderMaker( 'newMeetings', 'teamId', diff --git a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts index 48019c4fcd3..742b1b9ea39 100644 --- a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts @@ -3,7 +3,6 @@ import RethinkPrimaryKeyLoaderMaker from './RethinkPrimaryKeyLoaderMaker' /** * all rethink dataloader types which also must exist in {@link rethinkDriver/RethinkSchema} */ -export const comments = new RethinkPrimaryKeyLoaderMaker('Comment') export const reflectPrompts = new RethinkPrimaryKeyLoaderMaker('ReflectPrompt') export const massInvitations = new RethinkPrimaryKeyLoaderMaker('MassInvitation') export const meetingMembers = new RethinkPrimaryKeyLoaderMaker('MeetingMember') diff --git a/packages/server/graphql/mutations/helpers/addAIGeneratedContentToThreads.ts b/packages/server/graphql/mutations/helpers/addAIGeneratedContentToThreads.ts index 5eae10abdb7..6b18a33ff8b 100644 --- a/packages/server/graphql/mutations/helpers/addAIGeneratedContentToThreads.ts +++ b/packages/server/graphql/mutations/helpers/addAIGeneratedContentToThreads.ts @@ -1,8 +1,10 @@ +import {Insertable} from 'kysely' import {PARABOL_AI_USER_ID} from '../../../../client/utils/constants' -import getRethink from '../../../database/rethinkDriver' -import Comment from '../../../database/types/Comment' +import extractTextFromDraftString from '../../../../client/utils/draftjs/extractTextFromDraftString' import DiscussStage from '../../../database/types/DiscussStage' +import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' +import {Comment} from '../../../postgres/pg' import {convertHtmlToTaskContent} from '../../../utils/draftjs/convertHtmlToTaskContent' import {DataLoaderWorker} from '../../graphql' @@ -16,27 +18,25 @@ export const buildCommentContentBlock = ( return convertHtmlToTaskContent(html) } -export const createAIComment = (discussionId: string, content: string, order: number) => - new Comment({ - discussionId, - content, - threadSortOrder: order, - createdBy: PARABOL_AI_USER_ID - }) +export const createAIComment = (discussionId: string, content: string, order: number) => ({ + id: generateUID(), + discussionId, + content, + plaintextContent: extractTextFromDraftString(content), + threadSortOrder: order, + createdBy: PARABOL_AI_USER_ID +}) const addAIGeneratedContentToThreads = async ( stages: DiscussStage[], meetingId: string, dataLoader: DataLoaderWorker ) => { - const [r, groups] = await Promise.all([ - getRethink(), - dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId) - ]) + const groups = await dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId) const commentPromises = stages.map(async ({discussionId, reflectionGroupId}) => { const group = groups.find((group) => group.id === reflectionGroupId) if (!group?.discussionPromptQuestion) return - const comments: Comment[] = [] + const comments: Insertable[] = [] if (group.discussionPromptQuestion) { const topicSummaryComment = createAIComment( @@ -46,16 +46,7 @@ const addAIGeneratedContentToThreads = async ( ) comments.push(topicSummaryComment) } - const pgComments = comments.map((comment) => ({ - id: comment.id, - content: comment.content, - plaintextContent: comment.plaintextContent, - createdBy: comment.createdBy, - threadSortOrder: comment.threadSortOrder, - discussionId: comment.discussionId - })) - await getKysely().insertInto('Comment').values(pgComments).execute() - return r.table('Comment').insert(comments).run() + await getKysely().insertInto('Comment').values(comments).execute() }) await Promise.all(commentPromises) } diff --git a/packages/server/graphql/mutations/helpers/calculateEngagement.ts b/packages/server/graphql/mutations/helpers/calculateEngagement.ts index e4faf3d4d25..bed551a8130 100644 --- a/packages/server/graphql/mutations/helpers/calculateEngagement.ts +++ b/packages/server/graphql/mutations/helpers/calculateEngagement.ts @@ -88,7 +88,7 @@ const calculateEngagement = async (meeting: Meeting, dataLoader: DataLoaderWorke ]) const threadables = [...discussions.flat(), ...tasks.flat()] threadables.forEach(({createdBy}) => { - passiveMembers.delete(createdBy) + createdBy && passiveMembers.delete(createdBy) }) discussions.forEach((comments) => { diff --git a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts index 0e73db46043..457220ef98a 100644 --- a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts @@ -257,7 +257,7 @@ const getSlackMessageForNotification = async ( return null } const author = await dataLoader.get('users').loadNonNull(notification.authorId) - const comment = await dataLoader.get('comments').load(notification.commentId) + const comment = await dataLoader.get('comments').loadNonNull(notification.commentId) const authorName = comment.isAnonymous ? 'Anonymous' : author.preferredName diff --git a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts index 0e775bc2dc2..259a838413a 100644 --- a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts +++ b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts @@ -104,11 +104,6 @@ const resetRetroMeetingToGroupStage = { reflectionGroups.forEach((rg) => (rg.voterIds = [])) await Promise.all([ - r - .table('Comment') - .getAll(r.args(discussionIdsToDelete), {index: 'discussionId'}) - .delete() - .run(), pg.deleteFrom('Comment').where('discussionId', 'in', discussionIdsToDelete).execute(), r.table('Task').getAll(r.args(discussionIdsToDelete), {index: 'discussionId'}).delete().run(), pg diff --git a/packages/server/graphql/private/mutations/generateMeetingSummary.ts b/packages/server/graphql/private/mutations/generateMeetingSummary.ts index 6c82273ef1a..7be80c0d07b 100644 --- a/packages/server/graphql/private/mutations/generateMeetingSummary.ts +++ b/packages/server/graphql/private/mutations/generateMeetingSummary.ts @@ -45,7 +45,7 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn if (!discussion) return null const {id: discussionId} = discussion const rawComments = await dataLoader.get('commentsByDiscussionId').load(discussionId) - const humanComments = rawComments.filter((c) => !IGNORE_COMMENT_USER_IDS.includes(c.createdBy)) + const humanComments = rawComments.filter((c) => !IGNORE_COMMENT_USER_IDS.includes(c.createdBy!)) const rootComments = humanComments.filter((c) => !c.threadParentId) rootComments.sort((a, b) => { return a.createdAt.getTime() < b.createdAt.getTime() ? -1 : 1 @@ -53,8 +53,8 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn const comments = await Promise.all( rootComments.map(async (comment) => { const {createdBy, isAnonymous, plaintextContent} = comment - const creator = await dataLoader.get('users').loadNonNull(createdBy) - const commentAuthor = isAnonymous ? 'Anonymous' : creator.preferredName + const creator = createdBy ? await dataLoader.get('users').loadNonNull(createdBy) : null + const commentAuthor = isAnonymous || !creator ? 'Anonymous' : creator.preferredName const commentReplies = await Promise.all( humanComments .filter((c) => c.threadParentId === comment.id) @@ -63,8 +63,10 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn }) .map(async (reply) => { const {createdBy, isAnonymous, plaintextContent} = reply - const creator = await dataLoader.get('users').loadNonNull(createdBy) - const replyAuthor = isAnonymous ? 'Anonymous' : creator.preferredName + const creator = createdBy + ? await dataLoader.get('users').loadNonNull(createdBy) + : null + const replyAuthor = isAnonymous || !creator ? 'Anonymous' : creator.preferredName return { text: plaintextContent, author: replyAuthor diff --git a/packages/server/graphql/private/mutations/hardDeleteUser.ts b/packages/server/graphql/private/mutations/hardDeleteUser.ts index 0771f18b95c..6c19edc395b 100644 --- a/packages/server/graphql/private/mutations/hardDeleteUser.ts +++ b/packages/server/graphql/private/mutations/hardDeleteUser.ts @@ -75,7 +75,7 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( const teamDiscussionIds = discussions.rows.map(({id}) => id) // soft delete first for side effects - const tombstoneId = await softDeleteUser(userIdToDelete, dataLoader) + await softDeleteUser(userIdToDelete, dataLoader) // all other writes await setFacilitatedUserIdOrDelete(userIdToDelete, teamIds, dataLoader) @@ -103,15 +103,7 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( .table('TeamInvitation') .getAll(r.args(teamIds), {index: 'teamId'}) .filter((row: RValue) => row('acceptedBy').eq(userIdToDelete)) - .update({acceptedBy: ''}), - comment: r - .table('Comment') - .getAll(r.args(teamDiscussionIds), {index: 'discussionId'}) - .filter((row: RValue) => row('createdBy').eq(userIdToDelete)) - .update({ - createdBy: tombstoneId, - isAnonymous: true - }) + .update({acceptedBy: ''}) }).run() // now postgres, after FKs are added then triggers should take care of children diff --git a/packages/server/graphql/public/mutations/addComment.ts b/packages/server/graphql/public/mutations/addComment.ts index 2e0392c062e..d86e31f46e3 100644 --- a/packages/server/graphql/public/mutations/addComment.ts +++ b/packages/server/graphql/public/mutations/addComment.ts @@ -2,15 +2,16 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' import MeetingMemberId from '../../../../client/shared/gqlIds/MeetingMemberId' import TeamMemberId from '../../../../client/shared/gqlIds/TeamMemberId' +import extractTextFromDraftString from '../../../../client/utils/draftjs/extractTextFromDraftString' import getTypeFromEntityMap from '../../../../client/utils/draftjs/getTypeFromEntityMap' import getRethink from '../../../database/rethinkDriver' -import Comment from '../../../database/types/Comment' import GenericMeetingPhase, { NewMeetingPhaseTypeEnum } from '../../../database/types/GenericMeetingPhase' import GenericMeetingStage from '../../../database/types/GenericMeetingStage' import NotificationDiscussionMentioned from '../../../database/types/NotificationDiscussionMentioned' import NotificationResponseReplied from '../../../database/types/NotificationResponseReplied' +import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' import {IGetDiscussionsByIdsQueryResult} from '../../../postgres/queries/generated/getDiscussionsByIdsQuery' import {analytics} from '../../../utils/analytics/analytics' @@ -78,7 +79,7 @@ const addComment: MutationResolvers['addComment'] = async ( const subOptions = {mutatorId, operationId} //AUTH - const {discussionId} = comment + const {discussionId, threadSortOrder, isAnonymous, threadParentId} = comment const discussion = await dataLoader.get('discussions').load(discussionId) if (!discussion) { return {error: {message: 'Invalid discussion thread'}} @@ -101,19 +102,18 @@ const addComment: MutationResolvers['addComment'] = async ( // VALIDATION const content = normalizeRawDraftJS(comment.content) - const dbComment = new Comment({...comment, content, createdBy: viewerId}) - const {id: commentId, isAnonymous, threadParentId} = dbComment - await r.table('Comment').insert(dbComment).run() + const commentId = generateUID() await getKysely() .insertInto('Comment') .values({ - id: dbComment.id, - content: dbComment.content, - plaintextContent: dbComment.plaintextContent, - createdBy: dbComment.createdBy, - threadSortOrder: dbComment.threadSortOrder, - discussionId: dbComment.discussionId + id: commentId, + content, + plaintextContent: extractTextFromDraftString(content), + createdBy: viewerId, + threadSortOrder, + discussionId }) + .returning('id') .execute() if (discussion.discussionTopicType === 'teamPromptResponse') { @@ -162,7 +162,7 @@ const addComment: MutationResolvers['addComment'] = async ( )! const {stages} = containsThreadablePhase const isAsync = stages.some((stage: GenericMeetingStage) => stage.isAsync) - analytics.commentAdded(viewer, meeting, isAnonymous, isAsync, !!threadParentId) + analytics.commentAdded(viewer, meeting, !!isAnonymous, isAsync, !!threadParentId) publish(SubscriptionChannel.MEETING, meetingId, 'AddCommentSuccess', data, subOptions) return data } diff --git a/packages/server/graphql/public/mutations/addReactjiToReactable.ts b/packages/server/graphql/public/mutations/addReactjiToReactable.ts index 93225e8837e..2debd57a3ac 100644 --- a/packages/server/graphql/public/mutations/addReactjiToReactable.ts +++ b/packages/server/graphql/public/mutations/addReactjiToReactable.ts @@ -3,9 +3,6 @@ import TeamPromptResponseId from 'parabol-client/shared/gqlIds/TeamPromptRespons import {SubscriptionChannel, Threshold} from 'parabol-client/types/constEnums' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' import {ValueOf} from '../../../../client/types/generics' -import getRethink from '../../../database/rethinkDriver' -import {RDatum} from '../../../database/stricterR' -import Comment from '../../../database/types/Comment' import {DataLoaderInstance} from '../../../dataloader/RootDataLoader' import getKysely from '../../../postgres/getKysely' import {analytics} from '../../../utils/analytics/analytics' @@ -42,10 +39,8 @@ const addReactjiToReactable: MutationResolvers['addReactjiToReactable'] = async {reactableId, reactableType, reactji, isRemove, meetingId}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { - const r = await getRethink() const pg = getKysely() const viewerId = getUserId(authToken) - const now = new Date() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -87,7 +82,6 @@ const addReactjiToReactable: MutationResolvers['addReactjiToReactable'] = async } // RESOLUTION - const subDoc = {id: reactji, userId: viewerId} const tableName = tableLookup[reactableType] const dbId = tableName === 'TeamPromptResponse' ? TeamPromptResponseId.split(reactableId) : reactableId @@ -110,37 +104,9 @@ const addReactjiToReactable: MutationResolvers['addReactjiToReactable'] = async } } - const updateRethink = async (rethinkDbTable: ValueOf) => { - if (rethinkDbTable === 'TeamPromptResponse' || rethinkDbTable === 'RetroReflection') return - if (isRemove) { - await r - .table(rethinkDbTable) - .get(dbId) - .update((row: RDatum) => ({ - reactjis: row('reactjis').difference([subDoc]), - updatedAt: now - })) - .run() - } else { - await r - .table(rethinkDbTable) - .get(dbId) - .update((row: RDatum) => ({ - reactjis: r.branch( - row('reactjis').contains(subDoc), - row('reactjis'), - // don't use distinct, it sorts the fields - row('reactjis').append(subDoc) - ), - updatedAt: now - })) - .run() - } - } const [meeting] = await Promise.all([ dataLoader.get('newMeetings').load(meetingId), - updatePG(tableName), - updateRethink(tableName) + updatePG(tableName) ]) dataLoader.clearAll(['comments', 'teamPromptResponses', 'retroReflections']) diff --git a/packages/server/graphql/public/mutations/deleteComment.ts b/packages/server/graphql/public/mutations/deleteComment.ts index 957cbda541a..a63b4623bdc 100644 --- a/packages/server/graphql/public/mutations/deleteComment.ts +++ b/packages/server/graphql/public/mutations/deleteComment.ts @@ -1,7 +1,6 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' import {PARABOL_AI_USER_ID} from '../../../../client/utils/constants' -import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' import {getUserId} from '../../../utils/authorization' import publish from '../../../utils/publish' @@ -12,7 +11,6 @@ const deleteComment: MutationResolvers['deleteComment'] = async ( {commentId, meetingId}, {authToken, dataLoader, socketId: mutatorId} ) => { - const r = await getRethink() const viewerId = getUserId(authToken) const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -40,7 +38,6 @@ const deleteComment: MutationResolvers['deleteComment'] = async ( return {error: {message: 'Can only delete your own comment or Parabol AI comments'}} } - await r.table('Comment').get(commentId).update({isActive: false, updatedAt: now}).run() await getKysely() .updateTable('Comment') .set({updatedAt: now}) diff --git a/packages/server/graphql/public/mutations/helpers/getTopics.ts b/packages/server/graphql/public/mutations/helpers/getTopics.ts index bca0f2a4cec..94d23e228d9 100644 --- a/packages/server/graphql/public/mutations/helpers/getTopics.ts +++ b/packages/server/graphql/public/mutations/helpers/getTopics.ts @@ -19,7 +19,7 @@ const getComments = async (reflectionGroupId: string, dataLoader: DataLoaderWork if (!discussion) return null const {id: discussionId} = discussion const rawComments = await dataLoader.get('commentsByDiscussionId').load(discussionId) - const humanComments = rawComments.filter((c) => !IGNORE_COMMENT_USER_IDS.includes(c.createdBy)) + const humanComments = rawComments.filter((c) => !IGNORE_COMMENT_USER_IDS.includes(c.createdBy!)) const rootComments = humanComments.filter((c) => !c.threadParentId) rootComments.sort((a, b) => { return a.createdAt.getTime() < b.createdAt.getTime() ? -1 : 1 @@ -27,8 +27,8 @@ const getComments = async (reflectionGroupId: string, dataLoader: DataLoaderWork const comments = await Promise.all( rootComments.map(async (comment) => { const {createdBy, isAnonymous, plaintextContent} = comment - const creator = await dataLoader.get('users').loadNonNull(createdBy) - const commentAuthor = isAnonymous ? 'Anonymous' : creator.preferredName + const creator = createdBy ? await dataLoader.get('users').loadNonNull(createdBy) : null + const commentAuthor = isAnonymous || !creator ? 'Anonymous' : creator.preferredName const commentReplies = await Promise.all( humanComments .filter((c) => c.threadParentId === comment.id) @@ -37,8 +37,8 @@ const getComments = async (reflectionGroupId: string, dataLoader: DataLoaderWork }) .map(async (reply) => { const {createdBy, isAnonymous, plaintextContent} = reply - const creator = await dataLoader.get('users').loadNonNull(createdBy) - const replyAuthor = isAnonymous ? 'Anonymous' : creator.preferredName + const creator = createdBy ? await dataLoader.get('users').loadNonNull(createdBy) : null + const replyAuthor = isAnonymous || !creator ? 'Anonymous' : creator.preferredName return { text: plaintextContent, author: replyAuthor diff --git a/packages/server/graphql/public/mutations/updateCommentContent.ts b/packages/server/graphql/public/mutations/updateCommentContent.ts index 3f11febcc72..9ab6cb85b45 100644 --- a/packages/server/graphql/public/mutations/updateCommentContent.ts +++ b/packages/server/graphql/public/mutations/updateCommentContent.ts @@ -3,7 +3,6 @@ import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractText import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' import {PARABOL_AI_USER_ID} from '../../../../client/utils/constants' -import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' import {getUserId} from '../../../utils/authorization' import publish from '../../../utils/publish' @@ -15,9 +14,7 @@ const updateCommentContent: MutationResolvers['updateCommentContent'] = async ( {commentId, content, meetingId}, {authToken, dataLoader, socketId: mutatorId} ) => { - const r = await getRethink() const operationId = dataLoader.share() - const now = new Date() const subOptions = {operationId, mutatorId} // AUTH @@ -47,11 +44,6 @@ const updateCommentContent: MutationResolvers['updateCommentContent'] = async ( // RESOLUTION const plaintextContent = extractTextFromDraftString(normalizedContent) - await r - .table('Comment') - .get(commentId) - .update({content: normalizedContent, plaintextContent, updatedAt: now}) - .run() await getKysely() .updateTable('Comment') .set({content: normalizedContent, plaintextContent}) diff --git a/packages/server/graphql/public/types/AddCommentSuccess.ts b/packages/server/graphql/public/types/AddCommentSuccess.ts index 5f946d07872..eb620afe185 100644 --- a/packages/server/graphql/public/types/AddCommentSuccess.ts +++ b/packages/server/graphql/public/types/AddCommentSuccess.ts @@ -7,7 +7,7 @@ export type AddCommentSuccessSource = { const AddCommentSuccess: AddCommentSuccessResolvers = { comment: async ({commentId}, _args, {dataLoader}) => { - return dataLoader.get('comments').load(commentId) + return dataLoader.get('comments').loadNonNull(commentId) } } diff --git a/packages/server/graphql/public/types/Comment.ts b/packages/server/graphql/public/types/Comment.ts index 252fd124215..bede0ac3a81 100644 --- a/packages/server/graphql/public/types/Comment.ts +++ b/packages/server/graphql/public/types/Comment.ts @@ -7,7 +7,7 @@ const TOMBSTONE = convertToTaskContent('[deleted]') const Comment: CommentResolvers = { content: ({isActive, content}) => { - return isActive ? content : TOMBSTONE + return isActive ? JSON.stringify(content) : TOMBSTONE }, createdBy: ({createdBy, isAnonymous}) => { @@ -15,7 +15,9 @@ const Comment: CommentResolvers = { }, createdByUser: ({createdBy, isActive, isAnonymous}, _args, {dataLoader}) => { - return isAnonymous || !isActive ? null : dataLoader.get('users').loadNonNull(createdBy) + return isAnonymous || !isActive || !createdBy + ? null + : dataLoader.get('users').loadNonNull(createdBy) }, isActive: ({isActive}) => !!isActive, diff --git a/packages/server/graphql/public/types/DeleteCommentSuccess.ts b/packages/server/graphql/public/types/DeleteCommentSuccess.ts index b9f2c86f76d..68f28c5117c 100644 --- a/packages/server/graphql/public/types/DeleteCommentSuccess.ts +++ b/packages/server/graphql/public/types/DeleteCommentSuccess.ts @@ -6,7 +6,7 @@ export type DeleteCommentSuccessSource = { const DeleteCommentSuccess: DeleteCommentSuccessResolvers = { comment: async ({commentId}, _args, {dataLoader}) => { - return dataLoader.get('comments').load(commentId) + return dataLoader.get('comments').loadNonNull(commentId) } } diff --git a/packages/server/graphql/public/types/NotifyDiscussionMentioned.ts b/packages/server/graphql/public/types/NotifyDiscussionMentioned.ts index 1ab5dfd001d..d87b5bfefda 100644 --- a/packages/server/graphql/public/types/NotifyDiscussionMentioned.ts +++ b/packages/server/graphql/public/types/NotifyDiscussionMentioned.ts @@ -7,13 +7,13 @@ const NotifyDiscussionMentioned: NotifyDiscussionMentionedResolvers = { return meeting }, author: async ({authorId, commentId}, _args: unknown, {dataLoader}) => { - const comment = await dataLoader.get('comments').load(commentId) + const comment = await dataLoader.get('comments').loadNonNull(commentId) if (comment.isAnonymous) return null return dataLoader.get('users').loadNonNull(authorId) }, comment: ({commentId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('comments').load(commentId) + return dataLoader.get('comments').loadNonNull(commentId) }, discussion: ({discussionId}, _args: unknown, {dataLoader}) => { return dataLoader.get('discussions').loadNonNull(discussionId) diff --git a/packages/server/graphql/public/types/NotifyResponseReplied.ts b/packages/server/graphql/public/types/NotifyResponseReplied.ts index 53dd275becf..883a71d93bb 100644 --- a/packages/server/graphql/public/types/NotifyResponseReplied.ts +++ b/packages/server/graphql/public/types/NotifyResponseReplied.ts @@ -14,13 +14,13 @@ const NotifyResponseReplied: NotifyResponseRepliedResolvers = { return responses.find(({userId: responseUserId}) => responseUserId === userId)! }, author: async ({authorId, commentId}, _args: unknown, {dataLoader}) => { - const comment = await dataLoader.get('comments').load(commentId) + const comment = await dataLoader.get('comments').loadNonNull(commentId) if (comment.isAnonymous) return null return dataLoader.get('users').loadNonNull(authorId) }, comment: ({commentId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('comments').load(commentId) + return dataLoader.get('comments').loadNonNull(commentId) } } diff --git a/packages/server/graphql/public/types/UpdateCommentContentSuccess.ts b/packages/server/graphql/public/types/UpdateCommentContentSuccess.ts index 62c7382325b..53a55fea1cc 100644 --- a/packages/server/graphql/public/types/UpdateCommentContentSuccess.ts +++ b/packages/server/graphql/public/types/UpdateCommentContentSuccess.ts @@ -6,7 +6,7 @@ export type UpdateCommentContentSuccessSource = { const UpdateCommentContentSuccess: UpdateCommentContentSuccessResolvers = { comment: async ({commentId}, _args, {dataLoader}) => { - return dataLoader.get('comments').load(commentId) + return dataLoader.get('comments').loadNonNull(commentId) } } diff --git a/packages/server/graphql/resolvers/resolveReactjis.ts b/packages/server/graphql/resolvers/resolveReactjis.ts index d29cdedafea..9695a0e5487 100644 --- a/packages/server/graphql/resolvers/resolveReactjis.ts +++ b/packages/server/graphql/resolvers/resolveReactjis.ts @@ -1,10 +1,10 @@ -import Reactji from '../../database/types/Reactji' +import {ReactjiDB} from '../../postgres/types' import {getUserId} from '../../utils/authorization' import getGroupedReactjis from '../../utils/getGroupedReactjis' import {GQLContext} from './../graphql' const resolveReactjis = ( - {reactjis, id}: {reactjis: Reactji[]; id: string}, + {reactjis, id}: {reactjis: ReactjiDB[]; id: string}, _args: unknown, {authToken}: GQLContext ) => { diff --git a/packages/server/graphql/resolvers/resolveThreadableConnection.ts b/packages/server/graphql/resolvers/resolveThreadableConnection.ts index 180bcae6e2c..f553886f470 100644 --- a/packages/server/graphql/resolvers/resolveThreadableConnection.ts +++ b/packages/server/graphql/resolvers/resolveThreadableConnection.ts @@ -1,5 +1,5 @@ -import Comment from '../../database/types/Comment' import TaskDB from '../../database/types/Task' +import {Comment} from '../../postgres/types' import {ThreadableSource} from '../public/types/Threadable' import {DataLoaderWorker} from './../graphql' diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index 9a610b1de19..f97275eb80b 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -2,6 +2,7 @@ import type {JSONContent} from '@tiptap/core' import {NotNull, sql} from 'kysely' import {NewMeetingPhaseTypeEnum} from '../graphql/public/resolverTypes' import getKysely from './getKysely' +import {ReactjiDB} from './types' export const selectTimelineEvent = () => { return getKysely().selectFrom('TimelineEvent').selectAll().$narrowType< @@ -101,7 +102,6 @@ export const selectTeams = () => >('to_json', ['jiraDimensionFields']).as('jiraDimensionFields') ]) -export type ReactjiDB = {id: string; userId: string} export const selectRetroReflections = () => getKysely() .selectFrom('RetroReflection') diff --git a/packages/server/postgres/types/index.d.ts b/packages/server/postgres/types/index.d.ts index a3abdf24161..48fb2419996 100644 --- a/packages/server/postgres/types/index.d.ts +++ b/packages/server/postgres/types/index.d.ts @@ -1,5 +1,4 @@ import {SelectQueryBuilder, Selectable} from 'kysely' -import type Comment from '../../database/types/Comment' import { Discussion as DiscussionPG, OrganizationUser as OrganizationUserPG, @@ -7,6 +6,7 @@ import { } from '../pg.d' import { selectAgendaItems, + selectComments, selectMeetingSettings, selectOrganizations, selectRetroReflections, @@ -23,6 +23,7 @@ type ExtractTypeFromQueryBuilderSelect any> = ReturnType extends SelectQueryBuilder<_, _, infer X> ? X : never export type Discussion = Selectable +export type ReactjiDB = {id: string; userId: string} export interface Organization extends ExtractTypeFromQueryBuilderSelect {} @@ -53,3 +54,5 @@ export type AgendaItem = ExtractTypeFromQueryBuilderSelect export type SlackNotification = ExtractTypeFromQueryBuilderSelect + +export type Comment = ExtractTypeFromQueryBuilderSelect diff --git a/packages/server/utils/analytics/analytics.ts b/packages/server/utils/analytics/analytics.ts index 0476981a555..00b37a3fbc3 100644 --- a/packages/server/utils/analytics/analytics.ts +++ b/packages/server/utils/analytics/analytics.ts @@ -358,7 +358,7 @@ class Analytics { user: AnalyticsUser, meetingId: string, meetingType: MeetingTypeEnum, - reactable: {createdBy?: string; id: string}, + reactable: {createdBy?: string | null; id: string}, reactableType: ReactableEnum, reactji: string, isRemove: boolean diff --git a/packages/server/utils/getGroupedReactjis.ts b/packages/server/utils/getGroupedReactjis.ts index 6eda683da36..06448b744e1 100644 --- a/packages/server/utils/getGroupedReactjis.ts +++ b/packages/server/utils/getGroupedReactjis.ts @@ -1,8 +1,8 @@ import ReactjiId from 'parabol-client/shared/gqlIds/ReactjiId' -import Reactji from '../database/types/Reactji' import {ReactjiSource} from '../graphql/public/types/Reactji' +import {ReactjiDB} from '../postgres/types' -const getGroupedReactjis = (reactjis: Reactji[], viewerId: string, idPrefix: string) => { +const getGroupedReactjis = (reactjis: ReactjiDB[], viewerId: string, idPrefix: string) => { const agg = {} as {[key: string]: ReactjiSource} reactjis.forEach((reactji) => { const {id, userId} = reactji From 4cf9c8feb68ad3d51ecab02861f2416d4a2eff78 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 28 Aug 2024 18:50:23 -0700 Subject: [PATCH 06/16] fix replies Signed-off-by: Matt Krick --- packages/server/graphql/public/mutations/addComment.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/graphql/public/mutations/addComment.ts b/packages/server/graphql/public/mutations/addComment.ts index d86e31f46e3..6ccf9128c89 100644 --- a/packages/server/graphql/public/mutations/addComment.ts +++ b/packages/server/graphql/public/mutations/addComment.ts @@ -111,6 +111,7 @@ const addComment: MutationResolvers['addComment'] = async ( plaintextContent: extractTextFromDraftString(content), createdBy: viewerId, threadSortOrder, + threadParentId, discussionId }) .returning('id') From da853e75832dc8f8d01de5d1d3bfe5ec548a8ef5 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 6 Sep 2024 14:43:35 -0700 Subject: [PATCH 07/16] init migration --- .../dataloader/foreignKeyLoaderMakers.ts | 9 ++++ .../dataloader/primaryKeyLoaderMakers.ts | 5 ++ .../1725655687704_ReflectPrompt-phase1.ts | 52 +++++++++++++++++++ packages/server/postgres/select.ts | 2 + 4 files changed, 68 insertions(+) create mode 100644 packages/server/postgres/migrations/1725655687704_ReflectPrompt-phase1.ts diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 0f08e21d5a6..22ff29005f2 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -4,6 +4,7 @@ import { selectAgendaItems, selectComments, selectOrganizations, + selectReflectPrompts, selectRetroReflections, selectSlackAuths, selectSlackNotifications, @@ -215,3 +216,11 @@ export const commentsByDiscussionId = foreignKeyLoaderMaker( return selectComments().where('discussionId', 'in', discussionIds).execute() } ) + +export const _pgreflectPromptsByTemplateId = foreignKeyLoaderMaker( + '_pgreflectPrompts', + 'templateId', + async (templateIds) => { + return selectReflectPrompts().where('templateId', 'in', templateIds).execute() + } +) diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index adeee70820a..e80d3785659 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -10,6 +10,7 @@ import { selectComments, selectMeetingSettings, selectOrganizations, + selectReflectPrompts, selectRetroReflections, selectSlackAuths, selectSlackNotifications, @@ -110,3 +111,7 @@ export const slackNotifications = primaryKeyLoaderMaker((ids: readonly string[]) export const comments = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectComments().where('id', 'in', ids).execute() }) + +export const _pgreflectPrompts = primaryKeyLoaderMaker((ids: readonly string[]) => { + return selectReflectPrompts().where('id', 'in', ids).execute() +}) diff --git a/packages/server/postgres/migrations/1725655687704_ReflectPrompt-phase1.ts b/packages/server/postgres/migrations/1725655687704_ReflectPrompt-phase1.ts new file mode 100644 index 00000000000..099372d92ce --- /dev/null +++ b/packages/server/postgres/migrations/1725655687704_ReflectPrompt-phase1.ts @@ -0,0 +1,52 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DO $$ + BEGIN + CREATE TABLE IF NOT EXISTS "ReflectPrompt" ( + "id" VARCHAR(100) PRIMARY KEY, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "removedAt" TIMESTAMP WITH TIME ZONE, + "description" VARCHAR(256) NOT NULL, + "groupColor" VARCHAR(9) NOT NULL, + "sortOrder" VARCHAR(64) NOT NULL COLLATE "C", + "question" VARCHAR(100) NOT NULL, + "teamId" VARCHAR(100) NOT NULL, + "templateId" VARCHAR(100) NOT NULL, + "parentPromptId" VARCHAR(100), + CONSTRAINT "fk_teamId" + FOREIGN KEY("teamId") + REFERENCES "Team"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_templateId" + FOREIGN KEY("templateId") + REFERENCES "MeetingTemplate"("id") + ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS "idx_ReflectPrompt_teamId" ON "ReflectPrompt"("teamId"); + CREATE INDEX IF NOT EXISTS "idx_ReflectPrompt_templateId" ON "ReflectPrompt"("templateId"); + CREATE INDEX IF NOT EXISTS "idx_ReflectPrompt_parentPromptId" ON "ReflectPrompt"("templateId"); + END $$; +`) + // TODO add constraint parentPromptId constraint + // CONSTRAINT "fk_parentPromptId" + // FOREIGN KEY("parentPromptId") + // REFERENCES "MeetingTemplate"("id") + // ON DELETE CASCADE + + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE "Comment"; + ` /* Do undo magic */) + await client.end() +} diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index f97275eb80b..affc2e06e00 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -228,3 +228,5 @@ export const selectComments = () => 'threadSortOrder' ]) .select(({fn}) => [fn('to_json', ['reactjis']).as('reactjis')]) + +export const selectReflectPrompts = () => getKysely().selectFrom('ReflectPrompt').selectAll() From fb941f6ef8d91abd784a7db9f03db53602c9a5c7 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Mon, 9 Sep 2024 11:33:20 -0700 Subject: [PATCH 08/16] add writes to PG --- .../dataloader/foreignKeyLoaderMakers.ts | 5 ++- .../mutations/addReflectTemplatePrompt.ts | 34 ++++++++-------- .../mutations/moveReflectTemplatePrompt.ts | 22 ++++++++--- .../reflectTemplatePromptUpdateDescription.ts | 12 ++++-- .../reflectTemplatePromptUpdateGroupColor.ts | 8 ++-- .../mutations/removeReflectTemplate.ts | 15 +++++-- .../mutations/removeReflectTemplatePrompt.ts | 19 +++------ .../mutations/renameReflectTemplatePrompt.ts | 20 +++++----- .../graphql/mutations/updateTemplateScope.ts | 8 +++- .../public/mutations/addReflectTemplate.ts | 27 ++++++++----- .../1725655687704_ReflectPrompt-phase1.ts | 4 ++ .../postgres/queries/insertMeetingTemplate.ts | 39 ------------------- .../postgres/queries/removeMeetingTemplate.ts | 8 ---- 13 files changed, 104 insertions(+), 117 deletions(-) delete mode 100644 packages/server/postgres/queries/insertMeetingTemplate.ts delete mode 100644 packages/server/postgres/queries/removeMeetingTemplate.ts diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 22ff29005f2..2c2381b23a9 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -221,6 +221,9 @@ export const _pgreflectPromptsByTemplateId = foreignKeyLoaderMaker( '_pgreflectPrompts', 'templateId', async (templateIds) => { - return selectReflectPrompts().where('templateId', 'in', templateIds).execute() + return selectReflectPrompts() + .where('templateId', 'in', templateIds) + .orderBy('sortOrder') + .execute() } ) diff --git a/packages/server/graphql/mutations/addReflectTemplatePrompt.ts b/packages/server/graphql/mutations/addReflectTemplatePrompt.ts index e0867254b79..97e6028409f 100644 --- a/packages/server/graphql/mutations/addReflectTemplatePrompt.ts +++ b/packages/server/graphql/mutations/addReflectTemplatePrompt.ts @@ -1,10 +1,11 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel, Threshold} from 'parabol-client/types/constEnums' import dndNoise from 'parabol-client/utils/dndNoise' +import {positionAfter} from '../../../client/shared/sortOrder' import palettePickerOptions from '../../../client/styles/palettePickerOptions' import {PALETTE} from '../../../client/styles/paletteV3' import getRethink from '../../database/rethinkDriver' -import RetrospectivePrompt from '../../database/types/RetrospectivePrompt' +import generateUID from '../../generateUID' import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' @@ -42,26 +43,24 @@ const addReflectTemplatePrompt = { // VALIDATION const {teamId} = template - const activePrompts = await r - .table('ReflectPrompt') - .getAll(teamId, {index: 'teamId'}) - .filter({ - templateId, - removedAt: null - }) - .run() + const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(templateId) + const activePrompts = prompts.filter(({removedAt}) => !removedAt) + if (activePrompts.length >= Threshold.MAX_REFLECTION_PROMPTS) { return standardError(new Error('Too many prompts'), {userId: viewerId}) } // RESOLUTION - const sortOrder = - Math.max(0, ...activePrompts.map((prompt) => prompt.sortOrder)) + 1 + dndNoise() + const lastPrompt = activePrompts.at(-1)! + const sortOrder = lastPrompt.sortOrder + 1 + dndNoise() + // can remove String coercion after ReflectPrompt is in PG + const pgSortOrder = positionAfter(String(lastPrompt.sortOrder)) const pickedColors = activePrompts.map((prompt) => prompt.groupColor) const availableNewColor = palettePickerOptions.find( (color) => !pickedColors.includes(color.hex) ) - const reflectPrompt = new RetrospectivePrompt({ + const reflectPrompt = { + id: generateUID(), templateId: template.id, teamId: template.teamId, sortOrder, @@ -69,17 +68,16 @@ const addReflectTemplatePrompt = { description: '', groupColor: availableNewColor?.hex ?? PALETTE.JADE_400, removedAt: null - }) + } await Promise.all([ - await r.table('ReflectPrompt').insert(reflectPrompt).run(), + r.table('ReflectPrompt').insert(reflectPrompt).run(), pg - .updateTable('MeetingTemplate') - .set({updatedAt: new Date()}) - .where('id', '=', templateId) + .insertInto('ReflectPrompt') + .values({...reflectPrompt, sortOrder: pgSortOrder}) .execute() ]) - + dataLoader.clearAll('reflectPrompts') const promptId = reflectPrompt.id const data = {promptId} publish(SubscriptionChannel.TEAM, teamId, 'AddReflectTemplatePromptPayload', data, subOptions) diff --git a/packages/server/graphql/mutations/moveReflectTemplatePrompt.ts b/packages/server/graphql/mutations/moveReflectTemplatePrompt.ts index 2a02c13f9d7..e80bdd5576b 100644 --- a/packages/server/graphql/mutations/moveReflectTemplatePrompt.ts +++ b/packages/server/graphql/mutations/moveReflectTemplatePrompt.ts @@ -1,5 +1,6 @@ import {GraphQLFloat, GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import {getSortOrder} from '../../../client/shared/sortOrder' import getRethink from '../../database/rethinkDriver' import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' @@ -29,7 +30,7 @@ const moveReflectTemplate = { const now = new Date() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} - const prompt = await r.table('ReflectPrompt').get(promptId).run() + const prompt = await dataLoader.get('reflectPrompts').load(promptId) const viewerId = getUserId(authToken) // AUTH @@ -41,7 +42,10 @@ const moveReflectTemplate = { } // RESOLUTION - const {teamId, templateId} = prompt + const {teamId} = prompt + + const oldPrompts = await dataLoader.get('reflectPromptsByTemplateId').load(prompt.templateId) + const fromIdx = oldPrompts.findIndex((p) => p.id === promptId) await Promise.all([ r @@ -51,10 +55,18 @@ const moveReflectTemplate = { sortOrder, updatedAt: now }) - .run(), - pg.updateTable('MeetingTemplate').set({updatedAt: now}).where('id', '=', templateId).execute() + .run() ]) - + dataLoader.clearAll('reflectPrompts') + const newPrompts = await dataLoader.get('reflectPromptsByTemplateId').load(prompt.templateId) + const pgPrompts = await dataLoader.get('_pgreflectPromptsByTemplateId').load(prompt.templateId) + const toIdx = newPrompts.findIndex((p) => p.id === promptId) + const pgSortOrder = getSortOrder(pgPrompts, fromIdx, toIdx) + await pg + .updateTable('ReflectPrompt') + .set({sortOrder: pgSortOrder}) + .where('id', '=', promptId) + .execute() const data = {promptId} publish(SubscriptionChannel.TEAM, teamId, 'MoveReflectTemplatePromptPayload', data, subOptions) return data diff --git a/packages/server/graphql/mutations/reflectTemplatePromptUpdateDescription.ts b/packages/server/graphql/mutations/reflectTemplatePromptUpdateDescription.ts index 73254ea5522..aec89e34584 100644 --- a/packages/server/graphql/mutations/reflectTemplatePromptUpdateDescription.ts +++ b/packages/server/graphql/mutations/reflectTemplatePromptUpdateDescription.ts @@ -29,7 +29,7 @@ const reflectTemplatePromptUpdateDescription = { const now = new Date() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} - const prompt = await r.table('ReflectPrompt').get(promptId).run() + const prompt = await dataLoader.get('reflectPrompts').load(promptId) const viewerId = getUserId(authToken) // AUTH @@ -41,7 +41,7 @@ const reflectTemplatePromptUpdateDescription = { } // VALIDATION - const {teamId, templateId} = prompt + const {teamId} = prompt const normalizedDescription = description.trim().slice(0, 256) || '' // RESOLUTION @@ -54,9 +54,13 @@ const reflectTemplatePromptUpdateDescription = { updatedAt: now }) .run(), - pg.updateTable('MeetingTemplate').set({updatedAt: now}).where('id', '=', templateId).execute() + pg + .updateTable('ReflectPrompt') + .set({description: normalizedDescription}) + .where('id', '=', promptId) + .execute() ]) - + dataLoader.clearAll('reflectPrompts') const data = {promptId} publish( SubscriptionChannel.TEAM, diff --git a/packages/server/graphql/mutations/reflectTemplatePromptUpdateGroupColor.ts b/packages/server/graphql/mutations/reflectTemplatePromptUpdateGroupColor.ts index c7c397d1a99..46ecacd0994 100644 --- a/packages/server/graphql/mutations/reflectTemplatePromptUpdateGroupColor.ts +++ b/packages/server/graphql/mutations/reflectTemplatePromptUpdateGroupColor.ts @@ -31,7 +31,7 @@ const reflectTemplatePromptUpdateGroupColor = { const subOptions = {operationId, mutatorId} const viewerId = getUserId(authToken) - const prompt = await r.table('ReflectPrompt').get(promptId).run() + const prompt = await dataLoader.get('reflectPrompts').load(promptId) // AUTH if (!prompt || prompt.removedAt) { @@ -42,7 +42,7 @@ const reflectTemplatePromptUpdateGroupColor = { } // VALIDATION - const {teamId, templateId} = prompt + const {teamId} = prompt // RESOLUTION await Promise.all([ @@ -54,9 +54,9 @@ const reflectTemplatePromptUpdateGroupColor = { updatedAt: now }) .run(), - pg.updateTable('MeetingTemplate').set({updatedAt: now}).where('id', '=', templateId).execute() + pg.updateTable('ReflectPrompt').set({groupColor}).where('id', '=', promptId).execute() ]) - + dataLoader.clearAll('reflectPrompts') const data = {promptId} publish( SubscriptionChannel.TEAM, diff --git a/packages/server/graphql/mutations/removeReflectTemplate.ts b/packages/server/graphql/mutations/removeReflectTemplate.ts index 3357551a51a..14224dac4da 100644 --- a/packages/server/graphql/mutations/removeReflectTemplate.ts +++ b/packages/server/graphql/mutations/removeReflectTemplate.ts @@ -2,7 +2,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../database/rethinkDriver' import getKysely from '../../postgres/getKysely' -import removeMeetingTemplate from '../../postgres/queries/removeMeetingTemplate' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -23,6 +22,7 @@ const removeReflectTemplate = { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { const r = await getRethink() + const pg = getKysely() const now = new Date() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} @@ -47,7 +47,6 @@ const removeReflectTemplate = { // RESOLUTION const {id: settingsId} = settings await Promise.all([ - removeMeetingTemplate(templateId), r .table('ReflectPrompt') .getAll(teamId, {index: 'teamId'}) @@ -58,9 +57,17 @@ const removeReflectTemplate = { removedAt: now, updatedAt: now }) - .run() + .run(), + pg + .with('RemoveTemplate', (qb) => + qb.updateTable('MeetingTemplate').set({isActive: false}).where('id', '=', templateId) + ) + .updateTable('ReflectPrompt') + .set({removedAt: now}) + .where('templateId', '=', templateId) + .execute() ]) - + dataLoader.clearAll('reflectPrompts') if (settings.selectedTemplateId === templateId) { const nextTemplate = templates.find((template) => template.id !== templateId) const nextTemplateId = nextTemplate?.id ?? 'workingStuckTemplate' diff --git a/packages/server/graphql/mutations/removeReflectTemplatePrompt.ts b/packages/server/graphql/mutations/removeReflectTemplatePrompt.ts index b0f2f1eebaf..4fab8201d04 100644 --- a/packages/server/graphql/mutations/removeReflectTemplatePrompt.ts +++ b/packages/server/graphql/mutations/removeReflectTemplatePrompt.ts @@ -26,7 +26,7 @@ const removeReflectTemplatePrompt = { const now = new Date() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} - const prompt = await r.table('ReflectPrompt').get(promptId).run() + const prompt = await dataLoader.get('reflectPrompts').load(promptId) const viewerId = getUserId(authToken) // AUTH @@ -39,16 +39,9 @@ const removeReflectTemplatePrompt = { // VALIDATION const {teamId, templateId} = prompt - const promptCount = await r - .table('ReflectPrompt') - .getAll(teamId, {index: 'teamId'}) - .filter({ - removedAt: null, - templateId: templateId - }) - .count() - .default(0) - .run() + const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(templateId) + const activePrompts = prompts.filter((p) => !p.removedAt) + const promptCount = activePrompts.length if (promptCount <= 1) { return standardError(new Error('No prompts remain'), {userId: viewerId}) @@ -64,9 +57,9 @@ const removeReflectTemplatePrompt = { updatedAt: now }) .run(), - pg.updateTable('MeetingTemplate').set({updatedAt: now}).where('id', '=', templateId).execute() + pg.updateTable('ReflectPrompt').set({removedAt: now}).where('id', '=', promptId).execute() ]) - + dataLoader.clearAll('reflectPrompts') const data = {promptId, templateId} publish( SubscriptionChannel.TEAM, diff --git a/packages/server/graphql/mutations/renameReflectTemplatePrompt.ts b/packages/server/graphql/mutations/renameReflectTemplatePrompt.ts index b1a74470ebc..46ec142e06f 100644 --- a/packages/server/graphql/mutations/renameReflectTemplatePrompt.ts +++ b/packages/server/graphql/mutations/renameReflectTemplatePrompt.ts @@ -29,7 +29,7 @@ const renameReflectTemplatePrompt = { const now = new Date() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} - const prompt = await r.table('ReflectPrompt').get(promptId).run() + const prompt = await dataLoader.get('reflectPrompts').load(promptId) const viewerId = getUserId(authToken) // AUTH @@ -45,14 +45,8 @@ const renameReflectTemplatePrompt = { const trimmedQuestion = question.trim().slice(0, 100) const normalizedQuestion = trimmedQuestion || 'Unnamed Prompt' - const allPrompts = await r - .table('ReflectPrompt') - .getAll(teamId, {index: 'teamId'}) - .filter({ - removedAt: null, - templateId - }) - .run() + const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(templateId) + const allPrompts = prompts.filter(({removedAt}) => !removedAt) if (allPrompts.find((prompt) => prompt.question === normalizedQuestion)) { return standardError(new Error('Duplicate question template'), {userId: viewerId}) } @@ -67,9 +61,13 @@ const renameReflectTemplatePrompt = { updatedAt: now }) .run(), - pg.updateTable('MeetingTemplate').set({updatedAt: now}).where('id', '=', templateId).execute() + pg + .updateTable('ReflectPrompt') + .set({question: normalizedQuestion}) + .where('id', '=', promptId) + .execute() ]) - + dataLoader.clearAll('reflectPrompts') const data = {promptId} publish( SubscriptionChannel.TEAM, diff --git a/packages/server/graphql/mutations/updateTemplateScope.ts b/packages/server/graphql/mutations/updateTemplateScope.ts index b11f778856d..86c93b911f2 100644 --- a/packages/server/graphql/mutations/updateTemplateScope.ts +++ b/packages/server/graphql/mutations/updateTemplateScope.ts @@ -103,7 +103,13 @@ const updateTemplateScope = { ) .with('MeetingTemplateDeactivate', (qc) => qc.updateTable('MeetingTemplate').set({isActive: false}).where('id', '=', templateId) - ), + ) + .with('RemovePrompts', (qc) => + qc.updateTable('ReflectPrompt').set({removedAt: now}).where('id', 'in', promptIds) + ) + .insertInto('ReflectPrompt') + .values(clonedPrompts.map((p) => ({...p, sortOrder: String(p.sortOrder)}))) + .execute(), r.table('ReflectPrompt').insert(clonedPrompts).run(), r.table('ReflectPrompt').getAll(r.args(promptIds)).update({removedAt: now}).run() ]) diff --git a/packages/server/graphql/public/mutations/addReflectTemplate.ts b/packages/server/graphql/public/mutations/addReflectTemplate.ts index 586a6e57f05..958d938fcc6 100644 --- a/packages/server/graphql/public/mutations/addReflectTemplate.ts +++ b/packages/server/graphql/public/mutations/addReflectTemplate.ts @@ -2,9 +2,9 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {PALETTE} from '../../../../client/styles/paletteV3' import getRethink from '../../../database/rethinkDriver' import ReflectTemplate from '../../../database/types/ReflectTemplate' -import RetrospectivePrompt from '../../../database/types/RetrospectivePrompt' +import generateUID from '../../../generateUID' +import getKysely from '../../../postgres/getKysely' import decrementFreeTemplatesRemaining from '../../../postgres/queries/decrementFreeTemplatesRemaining' -import insertMeetingTemplate from '../../../postgres/queries/insertMeetingTemplate' import {analytics} from '../../../utils/analytics/analytics' import {getUserId, isTeamMember, isUserInOrg} from '../../../utils/authorization' import publish from '../../../utils/publish' @@ -18,6 +18,7 @@ const addPokerTemplate: MutationResolvers['addPokerTemplate'] = async ( {teamId, parentTemplateId}, {authToken, dataLoader, socketId: mutatorId} ) => { + const pg = getKysely() const r = await getRethink() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} @@ -76,20 +77,24 @@ const addPokerTemplate: MutationResolvers['addPokerTemplate'] = async ( mainCategory: parentTemplate.mainCategory }) const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(parentTemplate.id) - const activePrompts = prompts.filter(({removedAt}: RetrospectivePrompt) => !removedAt) - const newTemplatePrompts = activePrompts.map((prompt: RetrospectivePrompt) => { - return new RetrospectivePrompt({ + const activePrompts = prompts.filter(({removedAt}) => !removedAt) + const newTemplatePrompts = activePrompts.map((prompt) => { + return { ...prompt, + id: generateUID(), teamId, templateId: newTemplate.id, parentPromptId: prompt.id, removedAt: null - }) + } }) - await Promise.all([ r.table('ReflectPrompt').insert(newTemplatePrompts).run(), - insertMeetingTemplate(newTemplate), + pg + .with('MeetingTemplateInsert', (qc) => qc.insertInto('MeetingTemplate').values(newTemplate)) + .insertInto('ReflectPrompt') + .values(newTemplatePrompts.map((p) => ({...p, sortOrder: String(p.sortOrder)}))) + .execute(), decrementFreeTemplatesRemaining(viewerId, 'retro') ]) viewer.freeCustomRetroTemplatesRemaining = viewer.freeCustomRetroTemplatesRemaining - 1 @@ -114,7 +119,11 @@ const addPokerTemplate: MutationResolvers['addPokerTemplate'] = async ( const {id: templateId} = newTemplate await Promise.all([ r.table('ReflectPrompt').insert(newTemplatePrompts).run(), - insertMeetingTemplate(newTemplate), + pg + .with('MeetingTemplateInsert', (qc) => qc.insertInto('MeetingTemplate').values(newTemplate)) + .insertInto('ReflectPrompt') + .values(newTemplatePrompts.map((p) => ({...p, sortOrder: String(p.sortOrder)}))) + .execute(), decrementFreeTemplatesRemaining(viewerId, 'retro') ]) viewer.freeCustomRetroTemplatesRemaining = viewer.freeCustomRetroTemplatesRemaining - 1 diff --git a/packages/server/postgres/migrations/1725655687704_ReflectPrompt-phase1.ts b/packages/server/postgres/migrations/1725655687704_ReflectPrompt-phase1.ts index 099372d92ce..373ccc1f3b4 100644 --- a/packages/server/postgres/migrations/1725655687704_ReflectPrompt-phase1.ts +++ b/packages/server/postgres/migrations/1725655687704_ReflectPrompt-phase1.ts @@ -31,6 +31,10 @@ export async function up() { CREATE INDEX IF NOT EXISTS "idx_ReflectPrompt_teamId" ON "ReflectPrompt"("teamId"); CREATE INDEX IF NOT EXISTS "idx_ReflectPrompt_templateId" ON "ReflectPrompt"("templateId"); CREATE INDEX IF NOT EXISTS "idx_ReflectPrompt_parentPromptId" ON "ReflectPrompt"("templateId"); + CREATE OR REPLACE TRIGGER "update_MeetingTemplate_updatedAt_from_ReflectPrompt" + AFTER INSERT OR UPDATE OR DELETE ON "ReflectPrompt" + FOR EACH ROW + EXECUTE FUNCTION "set_MeetingTemplate_updatedAt"(); END $$; `) // TODO add constraint parentPromptId constraint diff --git a/packages/server/postgres/queries/insertMeetingTemplate.ts b/packages/server/postgres/queries/insertMeetingTemplate.ts deleted file mode 100644 index 78e06f2b53b..00000000000 --- a/packages/server/postgres/queries/insertMeetingTemplate.ts +++ /dev/null @@ -1,39 +0,0 @@ -import MeetingTemplate from '../../database/types/MeetingTemplate' -import getPg from '../getPg' - -const insertMeetingTemplate = async (meetingTemplate: MeetingTemplate) => { - const pg = getPg() - const { - id, - name, - teamId, - orgId, - parentTemplateId, - type, - scope, - lastUsedAt, - isStarter, - isFree, - mainCategory, - illustrationUrl - } = meetingTemplate - await pg.query( - `INSERT INTO "MeetingTemplate" (id, name, "teamId", "orgId", "parentTemplateId", type, scope, "lastUsedAt", "isStarter", "isFree", "mainCategory", "illustrationUrl") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, - [ - id, - name, - teamId, - orgId, - parentTemplateId, - type, - scope, - lastUsedAt, - isStarter, - isFree, - mainCategory, - illustrationUrl - ] - ) -} - -export default insertMeetingTemplate diff --git a/packages/server/postgres/queries/removeMeetingTemplate.ts b/packages/server/postgres/queries/removeMeetingTemplate.ts deleted file mode 100644 index 8793f726ff6..00000000000 --- a/packages/server/postgres/queries/removeMeetingTemplate.ts +++ /dev/null @@ -1,8 +0,0 @@ -import getPg from '../getPg' - -const removeMeetingTemplate = async (templateId: string) => { - const pg = getPg() - await pg.query(`UPDATE "MeetingTemplate" SET "isActive" = FALSE WHERE id = $1;`, [templateId]) -} - -export default removeMeetingTemplate From 814d9ef7ff0f0d92f8d92b826ba52c44294c0831 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Mon, 9 Sep 2024 11:55:25 -0700 Subject: [PATCH 09/16] refactor add/move reflect template prompt to sdl --- codegen.json | 2 + .../mutations/addReflectTemplatePrompt.ts | 88 ------------------- .../mutations/moveReflectTemplatePrompt.ts | 76 ---------------- .../mutations/addReflectTemplatePrompt.ts | 75 ++++++++++++++++ .../mutations/moveReflectTemplatePrompt.ts | 62 +++++++++++++ .../graphql/public/typeDefs/Mutation.graphql | 2 +- .../types/AddReflectTemplatePromptPayload.ts | 15 ++++ .../types/MoveReflectTemplatePromptPayload.ts | 15 ++++ packages/server/graphql/rootMutation.ts | 4 - .../types/AddReflectTemplatePromptPayload.ts | 22 ----- .../types/MoveReflectTemplatePromptPayload.ts | 22 ----- 11 files changed, 170 insertions(+), 213 deletions(-) delete mode 100644 packages/server/graphql/mutations/addReflectTemplatePrompt.ts delete mode 100644 packages/server/graphql/mutations/moveReflectTemplatePrompt.ts create mode 100644 packages/server/graphql/public/mutations/addReflectTemplatePrompt.ts create mode 100644 packages/server/graphql/public/mutations/moveReflectTemplatePrompt.ts create mode 100644 packages/server/graphql/public/types/AddReflectTemplatePromptPayload.ts create mode 100644 packages/server/graphql/public/types/MoveReflectTemplatePromptPayload.ts delete mode 100644 packages/server/graphql/types/AddReflectTemplatePromptPayload.ts delete mode 100644 packages/server/graphql/types/MoveReflectTemplatePromptPayload.ts diff --git a/codegen.json b/codegen.json index bb7e83c6aa9..c5b7445dbed 100644 --- a/codegen.json +++ b/codegen.json @@ -46,6 +46,8 @@ "config": { "contextType": "../graphql#GQLContext", "mappers": { + "MoveReflectTemplatePromptPayload": "./types/MoveReflectTemplatePromptPayload#MoveReflectTemplatePromptPayloadSource", + "AddReflectTemplatePromptPayload": "./types/AddReflectTemplatePromptPayload#AddReflectTemplatePromptPayloadSource", "SetSlackNotificationPayload": "./types/SetSlackNotificationPayload#SetSlackNotificationPayloadSource", "SetDefaultSlackChannelSuccess": "./types/SetDefaultSlackChannelSuccess#SetDefaultSlackChannelSuccessSource", "AddCommentSuccess": "./types/AddCommentSuccess#AddCommentSuccessSource", diff --git a/packages/server/graphql/mutations/addReflectTemplatePrompt.ts b/packages/server/graphql/mutations/addReflectTemplatePrompt.ts deleted file mode 100644 index 97e6028409f..00000000000 --- a/packages/server/graphql/mutations/addReflectTemplatePrompt.ts +++ /dev/null @@ -1,88 +0,0 @@ -import {GraphQLID, GraphQLNonNull} from 'graphql' -import {SubscriptionChannel, Threshold} from 'parabol-client/types/constEnums' -import dndNoise from 'parabol-client/utils/dndNoise' -import {positionAfter} from '../../../client/shared/sortOrder' -import palettePickerOptions from '../../../client/styles/palettePickerOptions' -import {PALETTE} from '../../../client/styles/paletteV3' -import getRethink from '../../database/rethinkDriver' -import generateUID from '../../generateUID' -import getKysely from '../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import AddReflectTemplatePromptPayload from '../types/AddReflectTemplatePromptPayload' - -const addReflectTemplatePrompt = { - description: 'Add a new template full of prompts', - type: AddReflectTemplatePromptPayload, - args: { - templateId: { - type: new GraphQLNonNull(GraphQLID) - } - }, - async resolve( - _source: unknown, - {templateId}: {templateId: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const pg = getKysely() - const operationId = dataLoader.share() - const subOptions = {operationId, mutatorId} - const template = await dataLoader.get('meetingTemplates').load(templateId) - const viewerId = getUserId(authToken) - - // AUTH - if (!template || !template.isActive) { - return standardError(new Error('Template not found'), {userId: viewerId}) - } - if (!isTeamMember(authToken, template.teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - - // VALIDATION - const {teamId} = template - const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(templateId) - const activePrompts = prompts.filter(({removedAt}) => !removedAt) - - if (activePrompts.length >= Threshold.MAX_REFLECTION_PROMPTS) { - return standardError(new Error('Too many prompts'), {userId: viewerId}) - } - - // RESOLUTION - const lastPrompt = activePrompts.at(-1)! - const sortOrder = lastPrompt.sortOrder + 1 + dndNoise() - // can remove String coercion after ReflectPrompt is in PG - const pgSortOrder = positionAfter(String(lastPrompt.sortOrder)) - const pickedColors = activePrompts.map((prompt) => prompt.groupColor) - const availableNewColor = palettePickerOptions.find( - (color) => !pickedColors.includes(color.hex) - ) - const reflectPrompt = { - id: generateUID(), - templateId: template.id, - teamId: template.teamId, - sortOrder, - question: `New prompt #${activePrompts.length + 1}`, - description: '', - groupColor: availableNewColor?.hex ?? PALETTE.JADE_400, - removedAt: null - } - - await Promise.all([ - r.table('ReflectPrompt').insert(reflectPrompt).run(), - pg - .insertInto('ReflectPrompt') - .values({...reflectPrompt, sortOrder: pgSortOrder}) - .execute() - ]) - dataLoader.clearAll('reflectPrompts') - const promptId = reflectPrompt.id - const data = {promptId} - publish(SubscriptionChannel.TEAM, teamId, 'AddReflectTemplatePromptPayload', data, subOptions) - return data - } -} - -export default addReflectTemplatePrompt diff --git a/packages/server/graphql/mutations/moveReflectTemplatePrompt.ts b/packages/server/graphql/mutations/moveReflectTemplatePrompt.ts deleted file mode 100644 index e80bdd5576b..00000000000 --- a/packages/server/graphql/mutations/moveReflectTemplatePrompt.ts +++ /dev/null @@ -1,76 +0,0 @@ -import {GraphQLFloat, GraphQLID, GraphQLNonNull} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import {getSortOrder} from '../../../client/shared/sortOrder' -import getRethink from '../../database/rethinkDriver' -import getKysely from '../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import MoveReflectTemplatePromptPayload from '../types/MoveReflectTemplatePromptPayload' - -const moveReflectTemplate = { - description: 'Move a reflect template', - type: MoveReflectTemplatePromptPayload, - args: { - promptId: { - type: new GraphQLNonNull(GraphQLID) - }, - sortOrder: { - type: new GraphQLNonNull(GraphQLFloat) - } - }, - async resolve( - _source: unknown, - {promptId, sortOrder}: {promptId: string; sortOrder: number}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const pg = getKysely() - const now = new Date() - const operationId = dataLoader.share() - const subOptions = {operationId, mutatorId} - const prompt = await dataLoader.get('reflectPrompts').load(promptId) - const viewerId = getUserId(authToken) - - // AUTH - if (!prompt || prompt.removedAt) { - return standardError(new Error('Prompt not found'), {userId: viewerId}) - } - if (!isTeamMember(authToken, prompt.teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - - // RESOLUTION - const {teamId} = prompt - - const oldPrompts = await dataLoader.get('reflectPromptsByTemplateId').load(prompt.templateId) - const fromIdx = oldPrompts.findIndex((p) => p.id === promptId) - - await Promise.all([ - r - .table('ReflectPrompt') - .get(promptId) - .update({ - sortOrder, - updatedAt: now - }) - .run() - ]) - dataLoader.clearAll('reflectPrompts') - const newPrompts = await dataLoader.get('reflectPromptsByTemplateId').load(prompt.templateId) - const pgPrompts = await dataLoader.get('_pgreflectPromptsByTemplateId').load(prompt.templateId) - const toIdx = newPrompts.findIndex((p) => p.id === promptId) - const pgSortOrder = getSortOrder(pgPrompts, fromIdx, toIdx) - await pg - .updateTable('ReflectPrompt') - .set({sortOrder: pgSortOrder}) - .where('id', '=', promptId) - .execute() - const data = {promptId} - publish(SubscriptionChannel.TEAM, teamId, 'MoveReflectTemplatePromptPayload', data, subOptions) - return data - } -} - -export default moveReflectTemplate diff --git a/packages/server/graphql/public/mutations/addReflectTemplatePrompt.ts b/packages/server/graphql/public/mutations/addReflectTemplatePrompt.ts new file mode 100644 index 00000000000..43de3440ea8 --- /dev/null +++ b/packages/server/graphql/public/mutations/addReflectTemplatePrompt.ts @@ -0,0 +1,75 @@ +import {SubscriptionChannel, Threshold} from 'parabol-client/types/constEnums' +import dndNoise from 'parabol-client/utils/dndNoise' +import {positionAfter} from '../../../../client/shared/sortOrder' +import palettePickerOptions from '../../../../client/styles/palettePickerOptions' +import {PALETTE} from '../../../../client/styles/paletteV3' +import getRethink from '../../../database/rethinkDriver' +import generateUID from '../../../generateUID' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const addReflectTemplatePrompt: MutationResolvers['addReflectTemplatePrompt'] = async ( + _source, + {templateId}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const r = await getRethink() + const pg = getKysely() + const operationId = dataLoader.share() + const subOptions = {operationId, mutatorId} + const template = await dataLoader.get('meetingTemplates').load(templateId) + const viewerId = getUserId(authToken) + + // AUTH + if (!template || !template.isActive) { + return standardError(new Error('Template not found'), {userId: viewerId}) + } + if (!isTeamMember(authToken, template.teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + // VALIDATION + const {teamId} = template + const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(templateId) + const activePrompts = prompts.filter(({removedAt}) => !removedAt) + + if (activePrompts.length >= Threshold.MAX_REFLECTION_PROMPTS) { + return standardError(new Error('Too many prompts'), {userId: viewerId}) + } + + // RESOLUTION + const lastPrompt = activePrompts.at(-1)! + const sortOrder = lastPrompt.sortOrder + 1 + dndNoise() + // can remove String coercion after ReflectPrompt is in PG + const pgSortOrder = positionAfter(String(lastPrompt.sortOrder)) + const pickedColors = activePrompts.map((prompt) => prompt.groupColor) + const availableNewColor = palettePickerOptions.find((color) => !pickedColors.includes(color.hex)) + const reflectPrompt = { + id: generateUID(), + templateId: template.id, + teamId: template.teamId, + sortOrder, + question: `New prompt #${activePrompts.length + 1}`, + description: '', + groupColor: availableNewColor?.hex ?? PALETTE.JADE_400, + removedAt: null + } + + await Promise.all([ + r.table('ReflectPrompt').insert(reflectPrompt).run(), + pg + .insertInto('ReflectPrompt') + .values({...reflectPrompt, sortOrder: pgSortOrder}) + .execute() + ]) + dataLoader.clearAll('reflectPrompts') + const promptId = reflectPrompt.id + const data = {promptId} + publish(SubscriptionChannel.TEAM, teamId, 'AddReflectTemplatePromptPayload', data, subOptions) + return data +} + +export default addReflectTemplatePrompt diff --git a/packages/server/graphql/public/mutations/moveReflectTemplatePrompt.ts b/packages/server/graphql/public/mutations/moveReflectTemplatePrompt.ts new file mode 100644 index 00000000000..f3a43d60055 --- /dev/null +++ b/packages/server/graphql/public/mutations/moveReflectTemplatePrompt.ts @@ -0,0 +1,62 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import {getSortOrder} from '../../../../client/shared/sortOrder' +import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const moveReflectTemplatePrompt: MutationResolvers['moveReflectTemplatePrompt'] = async ( + _source, + {promptId, sortOrder}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const r = await getRethink() + const pg = getKysely() + const now = new Date() + const operationId = dataLoader.share() + const subOptions = {operationId, mutatorId} + const prompt = await dataLoader.get('reflectPrompts').load(promptId) + const viewerId = getUserId(authToken) + + // AUTH + if (!prompt || prompt.removedAt) { + return standardError(new Error('Prompt not found'), {userId: viewerId}) + } + if (!isTeamMember(authToken, prompt.teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + // RESOLUTION + const {teamId} = prompt + + const oldPrompts = await dataLoader.get('reflectPromptsByTemplateId').load(prompt.templateId) + const fromIdx = oldPrompts.findIndex((p) => p.id === promptId) + + await Promise.all([ + r + .table('ReflectPrompt') + .get(promptId) + .update({ + sortOrder, + updatedAt: now + }) + .run() + ]) + dataLoader.clearAll('reflectPrompts') + const newPrompts = await dataLoader.get('reflectPromptsByTemplateId').load(prompt.templateId) + const pgPrompts = await dataLoader.get('_pgreflectPromptsByTemplateId').load(prompt.templateId) + const toIdx = newPrompts.findIndex((p) => p.id === promptId) + const pgSortOrder = getSortOrder(pgPrompts, fromIdx, toIdx) + await pg + .updateTable('ReflectPrompt') + .set({sortOrder: pgSortOrder}) + .where('id', '=', promptId) + .execute() + const data = {promptId} + publish(SubscriptionChannel.TEAM, teamId, 'MoveReflectTemplatePromptPayload', data, subOptions) + return data +} + +export default moveReflectTemplatePrompt diff --git a/packages/server/graphql/public/typeDefs/Mutation.graphql b/packages/server/graphql/public/typeDefs/Mutation.graphql index 0a58cdfde64..1ff66c7adb6 100644 --- a/packages/server/graphql/public/typeDefs/Mutation.graphql +++ b/packages/server/graphql/public/typeDefs/Mutation.graphql @@ -41,7 +41,7 @@ type Mutation { """ Add a new template full of prompts """ - addReflectTemplatePrompt(templateId: ID!): AddReflectTemplatePromptPayload + addReflectTemplatePrompt(templateId: ID!): AddReflectTemplatePromptPayload! addSlackAuth(code: ID!, teamId: ID!): AddSlackAuthPayload! addGitHubAuth(code: ID!, teamId: ID!): AddGitHubAuthPayload! diff --git a/packages/server/graphql/public/types/AddReflectTemplatePromptPayload.ts b/packages/server/graphql/public/types/AddReflectTemplatePromptPayload.ts new file mode 100644 index 00000000000..59482f4f743 --- /dev/null +++ b/packages/server/graphql/public/types/AddReflectTemplatePromptPayload.ts @@ -0,0 +1,15 @@ +import {AddReflectTemplatePromptPayloadResolvers} from '../resolverTypes' + +export type AddReflectTemplatePromptPayloadSource = + | { + promptId: string + } + | {error: {message: string}} + +const AddReflectTemplatePromptPayload: AddReflectTemplatePromptPayloadResolvers = { + prompt: (source, _args, {dataLoader}) => { + return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + } +} + +export default AddReflectTemplatePromptPayload diff --git a/packages/server/graphql/public/types/MoveReflectTemplatePromptPayload.ts b/packages/server/graphql/public/types/MoveReflectTemplatePromptPayload.ts new file mode 100644 index 00000000000..87096f79f55 --- /dev/null +++ b/packages/server/graphql/public/types/MoveReflectTemplatePromptPayload.ts @@ -0,0 +1,15 @@ +import {MoveReflectTemplatePromptPayloadResolvers} from '../resolverTypes' + +export type MoveReflectTemplatePromptPayloadSource = + | { + promptId: string + } + | {error: {message: string}} + +const MoveReflectTemplatePromptPayload: MoveReflectTemplatePromptPayloadResolvers = { + prompt: (source, _args, {dataLoader}) => { + return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + } +} + +export default MoveReflectTemplatePromptPayload diff --git a/packages/server/graphql/rootMutation.ts b/packages/server/graphql/rootMutation.ts index ab88c2e03ba..5ed771337f5 100644 --- a/packages/server/graphql/rootMutation.ts +++ b/packages/server/graphql/rootMutation.ts @@ -7,7 +7,6 @@ import addOrg from './mutations/addOrg' import addPokerTemplateDimension from './mutations/addPokerTemplateDimension' import addPokerTemplateScale from './mutations/addPokerTemplateScale' import addPokerTemplateScaleValue from './mutations/addPokerTemplateScaleValue' -import addReflectTemplatePrompt from './mutations/addReflectTemplatePrompt' import addTeam from './mutations/addTeam' import archiveOrganization from './mutations/archiveOrganization' import archiveTeam from './mutations/archiveTeam' @@ -41,7 +40,6 @@ import inviteToTeam from './mutations/inviteToTeam' import joinMeeting from './mutations/joinMeeting' import movePokerTemplateDimension from './mutations/movePokerTemplateDimension' import movePokerTemplateScaleValue from './mutations/movePokerTemplateScaleValue' -import moveReflectTemplatePrompt from './mutations/moveReflectTemplatePrompt' import moveTeamToOrg from './mutations/moveTeamToOrg' import navigateMeeting from './mutations/navigateMeeting' import newMeetingCheckIn from './mutations/newMeetingCheckIn' @@ -114,7 +112,6 @@ export default new GraphQLObjectType({ addPokerTemplateDimension, addPokerTemplateScale, addPokerTemplateScaleValue, - addReflectTemplatePrompt, addGitHubAuth, addOrg, addTeam, @@ -148,7 +145,6 @@ export default new GraphQLObjectType({ invalidateSessions, inviteToTeam, movePokerTemplateDimension, - moveReflectTemplatePrompt, moveTeamToOrg, navigateMeeting, newMeetingCheckIn, diff --git a/packages/server/graphql/types/AddReflectTemplatePromptPayload.ts b/packages/server/graphql/types/AddReflectTemplatePromptPayload.ts deleted file mode 100644 index 9061c731a8b..00000000000 --- a/packages/server/graphql/types/AddReflectTemplatePromptPayload.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import ReflectPrompt from './ReflectPrompt' -import StandardMutationError from './StandardMutationError' - -const AddReflectTemplatePromptPayload = new GraphQLObjectType({ - name: 'AddReflectTemplatePromptPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - prompt: { - type: ReflectPrompt, - resolve: ({promptId}, _args: unknown, {dataLoader}) => { - if (!promptId) return null - return dataLoader.get('reflectPrompts').load(promptId) - } - } - }) -}) - -export default AddReflectTemplatePromptPayload diff --git a/packages/server/graphql/types/MoveReflectTemplatePromptPayload.ts b/packages/server/graphql/types/MoveReflectTemplatePromptPayload.ts deleted file mode 100644 index c6d4ea310e7..00000000000 --- a/packages/server/graphql/types/MoveReflectTemplatePromptPayload.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import ReflectPrompt from './ReflectPrompt' -import StandardMutationError from './StandardMutationError' - -const MoveReflectTemplatePromptPayload = new GraphQLObjectType({ - name: 'MoveReflectTemplatePromptPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - prompt: { - type: ReflectPrompt, - resolve: ({promptId}, _args: unknown, {dataLoader}) => { - if (!promptId) return null - return dataLoader.get('reflectPrompts').load(promptId) - } - } - }) -}) - -export default MoveReflectTemplatePromptPayload From 134b3b622ec247d22d3ec46137d06bc4918eb580 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Mon, 9 Sep 2024 12:04:43 -0700 Subject: [PATCH 10/16] refactor 4 mutations to sdl --- codegen.json | 4 + .../mutations/removeReflectTemplatePrompt.ts | 75 ----------------- .../mutations/renameReflectTemplatePrompt.ts | 83 ------------------- .../reflectTemplatePromptUpdateDescription.ts | 33 ++------ .../reflectTemplatePromptUpdateGroupColor.ts | 33 ++------ .../mutations/removeReflectTemplatePrompt.ts | 58 +++++++++++++ .../mutations/renameReflectTemplatePrompt.ts | 63 ++++++++++++++ ...tTemplatePromptUpdateDescriptionPayload.ts | 16 ++++ ...ctTemplatePromptUpdateGroupColorPayload.ts | 16 ++++ .../RemoveReflectTemplatePromptPayload.ts | 20 +++++ .../RenameReflectTemplatePromptPayload.ts | 15 ++++ packages/server/graphql/rootMutation.ts | 8 -- ...tTemplatePromptUpdateDescriptionPayload.ts | 22 ----- ...ctTemplatePromptUpdateGroupColorPayload.ts | 22 ----- .../RemoveReflectTemplatePromptPayload.ts | 30 ------- .../RenameReflectTemplatePromptPayload.ts | 22 ----- 16 files changed, 208 insertions(+), 312 deletions(-) delete mode 100644 packages/server/graphql/mutations/removeReflectTemplatePrompt.ts delete mode 100644 packages/server/graphql/mutations/renameReflectTemplatePrompt.ts rename packages/server/graphql/{ => public}/mutations/reflectTemplatePromptUpdateDescription.ts (59%) rename packages/server/graphql/{ => public}/mutations/reflectTemplatePromptUpdateGroupColor.ts (56%) create mode 100644 packages/server/graphql/public/mutations/removeReflectTemplatePrompt.ts create mode 100644 packages/server/graphql/public/mutations/renameReflectTemplatePrompt.ts create mode 100644 packages/server/graphql/public/types/ReflectTemplatePromptUpdateDescriptionPayload.ts create mode 100644 packages/server/graphql/public/types/ReflectTemplatePromptUpdateGroupColorPayload.ts create mode 100644 packages/server/graphql/public/types/RemoveReflectTemplatePromptPayload.ts create mode 100644 packages/server/graphql/public/types/RenameReflectTemplatePromptPayload.ts delete mode 100644 packages/server/graphql/types/ReflectTemplatePromptUpdateDescriptionPayload.ts delete mode 100644 packages/server/graphql/types/ReflectTemplatePromptUpdateGroupColorPayload.ts delete mode 100644 packages/server/graphql/types/RemoveReflectTemplatePromptPayload.ts delete mode 100644 packages/server/graphql/types/RenameReflectTemplatePromptPayload.ts diff --git a/codegen.json b/codegen.json index c5b7445dbed..d7f135aec7c 100644 --- a/codegen.json +++ b/codegen.json @@ -46,6 +46,10 @@ "config": { "contextType": "../graphql#GQLContext", "mappers": { + "ReflectTemplatePromptUpdateDescriptionPayload": "./types/ReflectTemplatePromptUpdateDescriptionPayload#ReflectTemplatePromptUpdateDescriptionPayloadSource", + "ReflectTemplatePromptUpdateGroupColorPayload": "./types/ReflectTemplatePromptUpdateGroupColorPayload#ReflectTemplatePromptUpdateGroupColorPayloadSource", + "RemoveReflectTemplatePromptPayload": "./types/RemoveReflectTemplatePromptPayload#RemoveReflectTemplatePromptPayloadSource", + "RenameReflectTemplatePromptPayload": "./types/RenameReflectTemplatePromptPayload#RenameReflectTemplatePromptPayloadSource", "MoveReflectTemplatePromptPayload": "./types/MoveReflectTemplatePromptPayload#MoveReflectTemplatePromptPayloadSource", "AddReflectTemplatePromptPayload": "./types/AddReflectTemplatePromptPayload#AddReflectTemplatePromptPayloadSource", "SetSlackNotificationPayload": "./types/SetSlackNotificationPayload#SetSlackNotificationPayloadSource", diff --git a/packages/server/graphql/mutations/removeReflectTemplatePrompt.ts b/packages/server/graphql/mutations/removeReflectTemplatePrompt.ts deleted file mode 100644 index 4fab8201d04..00000000000 --- a/packages/server/graphql/mutations/removeReflectTemplatePrompt.ts +++ /dev/null @@ -1,75 +0,0 @@ -import {GraphQLID, GraphQLNonNull} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import getKysely from '../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import RemoveReflectTemplatePromptPayload from '../types/RemoveReflectTemplatePromptPayload' - -const removeReflectTemplatePrompt = { - description: 'Remove a prompt from a template', - type: RemoveReflectTemplatePromptPayload, - args: { - promptId: { - type: new GraphQLNonNull(GraphQLID) - } - }, - async resolve( - _source: unknown, - {promptId}: {promptId: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const pg = getKysely() - const now = new Date() - const operationId = dataLoader.share() - const subOptions = {operationId, mutatorId} - const prompt = await dataLoader.get('reflectPrompts').load(promptId) - const viewerId = getUserId(authToken) - - // AUTH - if (!prompt || prompt.removedAt) { - return standardError(new Error('Prompt not found'), {userId: viewerId}) - } - if (!isTeamMember(authToken, prompt.teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - - // VALIDATION - const {teamId, templateId} = prompt - const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(templateId) - const activePrompts = prompts.filter((p) => !p.removedAt) - const promptCount = activePrompts.length - - if (promptCount <= 1) { - return standardError(new Error('No prompts remain'), {userId: viewerId}) - } - - // RESOLUTION - await Promise.all([ - r - .table('ReflectPrompt') - .get(promptId) - .update({ - removedAt: now, - updatedAt: now - }) - .run(), - pg.updateTable('ReflectPrompt').set({removedAt: now}).where('id', '=', promptId).execute() - ]) - dataLoader.clearAll('reflectPrompts') - const data = {promptId, templateId} - publish( - SubscriptionChannel.TEAM, - teamId, - 'RemoveReflectTemplatePromptPayload', - data, - subOptions - ) - return data - } -} - -export default removeReflectTemplatePrompt diff --git a/packages/server/graphql/mutations/renameReflectTemplatePrompt.ts b/packages/server/graphql/mutations/renameReflectTemplatePrompt.ts deleted file mode 100644 index 46ec142e06f..00000000000 --- a/packages/server/graphql/mutations/renameReflectTemplatePrompt.ts +++ /dev/null @@ -1,83 +0,0 @@ -import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import getKysely from '../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import RenameReflectTemplatePromptPayload from '../types/RenameReflectTemplatePromptPayload' - -const renameReflectTemplatePrompt = { - description: 'Rename a reflect template prompt', - type: RenameReflectTemplatePromptPayload, - args: { - promptId: { - type: new GraphQLNonNull(GraphQLID) - }, - question: { - type: new GraphQLNonNull(GraphQLString) - } - }, - async resolve( - _source: unknown, - {promptId, question}: {promptId: string; question: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const pg = getKysely() - const now = new Date() - const operationId = dataLoader.share() - const subOptions = {operationId, mutatorId} - const prompt = await dataLoader.get('reflectPrompts').load(promptId) - const viewerId = getUserId(authToken) - - // AUTH - if (!prompt || prompt.removedAt) { - return standardError(new Error('Prompt not found'), {userId: viewerId}) - } - if (!isTeamMember(authToken, prompt.teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - - // VALIDATION - const {teamId, templateId} = prompt - const trimmedQuestion = question.trim().slice(0, 100) - const normalizedQuestion = trimmedQuestion || 'Unnamed Prompt' - - const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(templateId) - const allPrompts = prompts.filter(({removedAt}) => !removedAt) - if (allPrompts.find((prompt) => prompt.question === normalizedQuestion)) { - return standardError(new Error('Duplicate question template'), {userId: viewerId}) - } - - // RESOLUTION - await Promise.all([ - r - .table('ReflectPrompt') - .get(promptId) - .update({ - question: normalizedQuestion, - updatedAt: now - }) - .run(), - pg - .updateTable('ReflectPrompt') - .set({question: normalizedQuestion}) - .where('id', '=', promptId) - .execute() - ]) - dataLoader.clearAll('reflectPrompts') - const data = {promptId} - publish( - SubscriptionChannel.TEAM, - teamId, - 'RenameReflectTemplatePromptPayload', - data, - subOptions - ) - return data - } -} - -export default renameReflectTemplatePrompt diff --git a/packages/server/graphql/mutations/reflectTemplatePromptUpdateDescription.ts b/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateDescription.ts similarity index 59% rename from packages/server/graphql/mutations/reflectTemplatePromptUpdateDescription.ts rename to packages/server/graphql/public/mutations/reflectTemplatePromptUpdateDescription.ts index aec89e34584..5fc36d0fb48 100644 --- a/packages/server/graphql/mutations/reflectTemplatePromptUpdateDescription.ts +++ b/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateDescription.ts @@ -1,29 +1,13 @@ -import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import getKysely from '../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import ReflectTemplatePromptUpdateDescriptionPayload from '../types/ReflectTemplatePromptUpdateDescriptionPayload' +import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' -const reflectTemplatePromptUpdateDescription = { - description: 'Update the description of a reflection prompt', - type: ReflectTemplatePromptUpdateDescriptionPayload, - args: { - promptId: { - type: new GraphQLNonNull(GraphQLID) - }, - description: { - type: new GraphQLNonNull(GraphQLString) - } - }, - async resolve( - _source: unknown, - {promptId, description}: {promptId: string; description: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { +const reflectTemplatePromptUpdateDescription: MutationResolvers['reflectTemplatePromptUpdateDescription'] = + async (_source, {promptId, description}, {authToken, dataLoader, socketId: mutatorId}) => { const r = await getRethink() const pg = getKysely() const now = new Date() @@ -71,6 +55,5 @@ const reflectTemplatePromptUpdateDescription = { ) return data } -} export default reflectTemplatePromptUpdateDescription diff --git a/packages/server/graphql/mutations/reflectTemplatePromptUpdateGroupColor.ts b/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateGroupColor.ts similarity index 56% rename from packages/server/graphql/mutations/reflectTemplatePromptUpdateGroupColor.ts rename to packages/server/graphql/public/mutations/reflectTemplatePromptUpdateGroupColor.ts index 46ecacd0994..f6de5ffc8f2 100644 --- a/packages/server/graphql/mutations/reflectTemplatePromptUpdateGroupColor.ts +++ b/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateGroupColor.ts @@ -1,29 +1,13 @@ -import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import getKysely from '../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import ReflectTemplatePromptUpdateGroupColorPayload from '../types/ReflectTemplatePromptUpdateGroupColorPayload' +import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' -const reflectTemplatePromptUpdateGroupColor = { - groupColor: 'Update the groupColor of a reflection prompt', - type: ReflectTemplatePromptUpdateGroupColorPayload, - args: { - promptId: { - type: new GraphQLNonNull(GraphQLID) - }, - groupColor: { - type: new GraphQLNonNull(GraphQLString) - } - }, - async resolve( - _source: unknown, - {promptId, groupColor}: {promptId: string; groupColor: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { +const reflectTemplatePromptUpdateGroupColor: MutationResolvers['reflectTemplatePromptUpdateGroupColor'] = + async (_source, {promptId, groupColor}, {authToken, dataLoader, socketId: mutatorId}) => { const r = await getRethink() const pg = getKysely() const now = new Date() @@ -67,6 +51,5 @@ const reflectTemplatePromptUpdateGroupColor = { ) return data } -} export default reflectTemplatePromptUpdateGroupColor diff --git a/packages/server/graphql/public/mutations/removeReflectTemplatePrompt.ts b/packages/server/graphql/public/mutations/removeReflectTemplatePrompt.ts new file mode 100644 index 00000000000..c74b0b48b0a --- /dev/null +++ b/packages/server/graphql/public/mutations/removeReflectTemplatePrompt.ts @@ -0,0 +1,58 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const removeReflectTemplatePrompt: MutationResolvers['removeReflectTemplatePrompt'] = async ( + _source, + {promptId}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const r = await getRethink() + const pg = getKysely() + const now = new Date() + const operationId = dataLoader.share() + const subOptions = {operationId, mutatorId} + const prompt = await dataLoader.get('reflectPrompts').load(promptId) + const viewerId = getUserId(authToken) + + // AUTH + if (!prompt || prompt.removedAt) { + return standardError(new Error('Prompt not found'), {userId: viewerId}) + } + if (!isTeamMember(authToken, prompt.teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + // VALIDATION + const {teamId, templateId} = prompt + const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(templateId) + const activePrompts = prompts.filter((p) => !p.removedAt) + const promptCount = activePrompts.length + + if (promptCount <= 1) { + return standardError(new Error('No prompts remain'), {userId: viewerId}) + } + + // RESOLUTION + await Promise.all([ + r + .table('ReflectPrompt') + .get(promptId) + .update({ + removedAt: now, + updatedAt: now + }) + .run(), + pg.updateTable('ReflectPrompt').set({removedAt: now}).where('id', '=', promptId).execute() + ]) + dataLoader.clearAll('reflectPrompts') + const data = {promptId, templateId} + publish(SubscriptionChannel.TEAM, teamId, 'RemoveReflectTemplatePromptPayload', data, subOptions) + return data +} + +export default removeReflectTemplatePrompt diff --git a/packages/server/graphql/public/mutations/renameReflectTemplatePrompt.ts b/packages/server/graphql/public/mutations/renameReflectTemplatePrompt.ts new file mode 100644 index 00000000000..426901b79e6 --- /dev/null +++ b/packages/server/graphql/public/mutations/renameReflectTemplatePrompt.ts @@ -0,0 +1,63 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const renameReflectTemplatePrompt: MutationResolvers['renameReflectTemplatePrompt'] = async ( + _source, + {promptId, question}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const r = await getRethink() + const pg = getKysely() + const now = new Date() + const operationId = dataLoader.share() + const subOptions = {operationId, mutatorId} + const prompt = await dataLoader.get('reflectPrompts').load(promptId) + const viewerId = getUserId(authToken) + + // AUTH + if (!prompt || prompt.removedAt) { + return standardError(new Error('Prompt not found'), {userId: viewerId}) + } + if (!isTeamMember(authToken, prompt.teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + // VALIDATION + const {teamId, templateId} = prompt + const trimmedQuestion = question.trim().slice(0, 100) + const normalizedQuestion = trimmedQuestion || 'Unnamed Prompt' + + const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(templateId) + const allPrompts = prompts.filter(({removedAt}) => !removedAt) + if (allPrompts.find((prompt) => prompt.question === normalizedQuestion)) { + return standardError(new Error('Duplicate question template'), {userId: viewerId}) + } + + // RESOLUTION + await Promise.all([ + r + .table('ReflectPrompt') + .get(promptId) + .update({ + question: normalizedQuestion, + updatedAt: now + }) + .run(), + pg + .updateTable('ReflectPrompt') + .set({question: normalizedQuestion}) + .where('id', '=', promptId) + .execute() + ]) + dataLoader.clearAll('reflectPrompts') + const data = {promptId} + publish(SubscriptionChannel.TEAM, teamId, 'RenameReflectTemplatePromptPayload', data, subOptions) + return data +} + +export default renameReflectTemplatePrompt diff --git a/packages/server/graphql/public/types/ReflectTemplatePromptUpdateDescriptionPayload.ts b/packages/server/graphql/public/types/ReflectTemplatePromptUpdateDescriptionPayload.ts new file mode 100644 index 00000000000..ab49737cd6e --- /dev/null +++ b/packages/server/graphql/public/types/ReflectTemplatePromptUpdateDescriptionPayload.ts @@ -0,0 +1,16 @@ +import {ReflectTemplatePromptUpdateDescriptionPayloadResolvers} from '../resolverTypes' + +export type ReflectTemplatePromptUpdateDescriptionPayloadSource = + | { + promptId: string + } + | {error: {message: string}} + +const ReflectTemplatePromptUpdateDescriptionPayload: ReflectTemplatePromptUpdateDescriptionPayloadResolvers = + { + prompt: (source, _args, {dataLoader}) => { + return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + } + } + +export default ReflectTemplatePromptUpdateDescriptionPayload diff --git a/packages/server/graphql/public/types/ReflectTemplatePromptUpdateGroupColorPayload.ts b/packages/server/graphql/public/types/ReflectTemplatePromptUpdateGroupColorPayload.ts new file mode 100644 index 00000000000..c964750561f --- /dev/null +++ b/packages/server/graphql/public/types/ReflectTemplatePromptUpdateGroupColorPayload.ts @@ -0,0 +1,16 @@ +import {ReflectTemplatePromptUpdateGroupColorPayloadResolvers} from '../resolverTypes' + +export type ReflectTemplatePromptUpdateGroupColorPayloadSource = + | { + promptId: string + } + | {error: {message: string}} + +const ReflectTemplatePromptUpdateGroupColorPayload: ReflectTemplatePromptUpdateGroupColorPayloadResolvers = + { + prompt: (source, _args, {dataLoader}) => { + return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + } + } + +export default ReflectTemplatePromptUpdateGroupColorPayload diff --git a/packages/server/graphql/public/types/RemoveReflectTemplatePromptPayload.ts b/packages/server/graphql/public/types/RemoveReflectTemplatePromptPayload.ts new file mode 100644 index 00000000000..9576753add4 --- /dev/null +++ b/packages/server/graphql/public/types/RemoveReflectTemplatePromptPayload.ts @@ -0,0 +1,20 @@ +import {RemoveReflectTemplatePromptPayloadResolvers} from '../resolverTypes' + +export type RemoveReflectTemplatePromptPayloadSource = + | { + promptId: string + templateId: string + } + | {error: {message: string}} + +const RemoveReflectTemplatePromptPayload: RemoveReflectTemplatePromptPayloadResolvers = { + reflectTemplate: (source, _args, {dataLoader}) => { + return 'templateId' in source ? dataLoader.get('meetingTemplates').load(templateId) : null + }, + + prompt: (source, _args, {dataLoader}) => { + return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + } +} + +export default RemoveReflectTemplatePromptPayload diff --git a/packages/server/graphql/public/types/RenameReflectTemplatePromptPayload.ts b/packages/server/graphql/public/types/RenameReflectTemplatePromptPayload.ts new file mode 100644 index 00000000000..980ad0ac390 --- /dev/null +++ b/packages/server/graphql/public/types/RenameReflectTemplatePromptPayload.ts @@ -0,0 +1,15 @@ +import {RenameReflectTemplatePromptPayloadResolvers} from '../resolverTypes' + +export type RenameReflectTemplatePromptPayloadSource = + | { + promptId: string + } + | {error: {message: string}} + +const RenameReflectTemplatePromptPayload: RenameReflectTemplatePromptPayloadResolvers = { + prompt: (source, _args, {dataLoader}) => { + return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + } +} + +export default RenameReflectTemplatePromptPayload diff --git a/packages/server/graphql/rootMutation.ts b/packages/server/graphql/rootMutation.ts index 5ed771337f5..918463c1dae 100644 --- a/packages/server/graphql/rootMutation.ts +++ b/packages/server/graphql/rootMutation.ts @@ -55,8 +55,6 @@ import pokerTemplateDimensionUpdateDescription from './mutations/pokerTemplateDi import promoteNewMeetingFacilitator from './mutations/promoteNewMeetingFacilitator' import promoteToTeamLead from './mutations/promoteToTeamLead' import pushInvitation from './mutations/pushInvitation' -import reflectTemplatePromptUpdateDescription from './mutations/reflectTemplatePromptUpdateDescription' -import reflectTemplatePromptUpdateGroupColor from './mutations/reflectTemplatePromptUpdateGroupColor' import removeAtlassianAuth from './mutations/removeAtlassianAuth' import removeGitHubAuth from './mutations/removeGitHubAuth' import removeIntegrationProvider from './mutations/removeIntegrationProvider' @@ -65,7 +63,6 @@ import removePokerTemplateDimension from './mutations/removePokerTemplateDimensi import removePokerTemplateScale from './mutations/removePokerTemplateScale' import removePokerTemplateScaleValue from './mutations/removePokerTemplateScaleValue' import removeReflectTemplate from './mutations/removeReflectTemplate' -import removeReflectTemplatePrompt from './mutations/removeReflectTemplatePrompt' import removeReflection from './mutations/removeReflection' import removeSlackAuth from './mutations/removeSlackAuth' import removeTeamMember from './mutations/removeTeamMember' @@ -73,7 +70,6 @@ import renameMeeting from './mutations/renameMeeting' import renameMeetingTemplate from './mutations/renameMeetingTemplate' import renamePokerTemplateDimension from './mutations/renamePokerTemplateDimension' import renamePokerTemplateScale from './mutations/renamePokerTemplateScale' -import renameReflectTemplatePrompt from './mutations/renameReflectTemplatePrompt' import resetPassword from './mutations/resetPassword' import resetRetroMeetingToGroupStage from './mutations/resetRetroMeetingToGroupStage' import selectTemplate from './mutations/selectTemplate' @@ -153,18 +149,14 @@ export default new GraphQLObjectType({ pushInvitation, promoteNewMeetingFacilitator, promoteToTeamLead, - reflectTemplatePromptUpdateDescription, pokerTemplateDimensionUpdateDescription, - reflectTemplatePromptUpdateGroupColor, removeAtlassianAuth, removeGitHubAuth, removeOrgUser, removeReflectTemplate, - removeReflectTemplatePrompt, removePokerTemplateDimension, renameMeeting, renameMeetingTemplate, - renameReflectTemplatePrompt, renamePokerTemplateDimension, renamePokerTemplateScale, removePokerTemplateScale, diff --git a/packages/server/graphql/types/ReflectTemplatePromptUpdateDescriptionPayload.ts b/packages/server/graphql/types/ReflectTemplatePromptUpdateDescriptionPayload.ts deleted file mode 100644 index 4e27cbaa23c..00000000000 --- a/packages/server/graphql/types/ReflectTemplatePromptUpdateDescriptionPayload.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import ReflectPrompt from './ReflectPrompt' -import StandardMutationError from './StandardMutationError' - -const ReflectTemplatePromptUpdateDescriptionPayload = new GraphQLObjectType({ - name: 'ReflectTemplatePromptUpdateDescriptionPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - prompt: { - type: ReflectPrompt, - resolve: ({promptId}, _args: unknown, {dataLoader}) => { - if (!promptId) return null - return dataLoader.get('reflectPrompts').load(promptId) - } - } - }) -}) - -export default ReflectTemplatePromptUpdateDescriptionPayload diff --git a/packages/server/graphql/types/ReflectTemplatePromptUpdateGroupColorPayload.ts b/packages/server/graphql/types/ReflectTemplatePromptUpdateGroupColorPayload.ts deleted file mode 100644 index 6efd27f0c8c..00000000000 --- a/packages/server/graphql/types/ReflectTemplatePromptUpdateGroupColorPayload.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import ReflectPrompt from './ReflectPrompt' -import StandardMutationError from './StandardMutationError' - -const ReflectTemplatePromptUpdateGroupColorPayload = new GraphQLObjectType({ - name: 'ReflectTemplatePromptUpdateGroupColorPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - prompt: { - type: ReflectPrompt, - resolve: ({promptId}, _args: unknown, {dataLoader}) => { - if (!promptId) return null - return dataLoader.get('reflectPrompts').load(promptId) - } - } - }) -}) - -export default ReflectTemplatePromptUpdateGroupColorPayload diff --git a/packages/server/graphql/types/RemoveReflectTemplatePromptPayload.ts b/packages/server/graphql/types/RemoveReflectTemplatePromptPayload.ts deleted file mode 100644 index 8c66ecd729e..00000000000 --- a/packages/server/graphql/types/RemoveReflectTemplatePromptPayload.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import ReflectPrompt from './ReflectPrompt' -import ReflectTemplate from './ReflectTemplate' -import StandardMutationError from './StandardMutationError' - -const RemoveReflectTemplatePromptPayload = new GraphQLObjectType({ - name: 'RemoveReflectTemplatePromptPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - reflectTemplate: { - type: ReflectTemplate, - resolve: ({templateId}, _args: unknown, {dataLoader}) => { - if (!templateId) return null - return dataLoader.get('meetingTemplates').load(templateId) - } - }, - prompt: { - type: ReflectPrompt, - resolve: ({promptId}, _args: unknown, {dataLoader}) => { - if (!promptId) return null - return dataLoader.get('reflectPrompts').load(promptId) - } - } - }) -}) - -export default RemoveReflectTemplatePromptPayload diff --git a/packages/server/graphql/types/RenameReflectTemplatePromptPayload.ts b/packages/server/graphql/types/RenameReflectTemplatePromptPayload.ts deleted file mode 100644 index 0fe41ac3764..00000000000 --- a/packages/server/graphql/types/RenameReflectTemplatePromptPayload.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import ReflectPrompt from './ReflectPrompt' -import StandardMutationError from './StandardMutationError' - -const RenameReflectTemplatePromptPayload = new GraphQLObjectType({ - name: 'RenameReflectTemplatePromptPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - prompt: { - type: ReflectPrompt, - resolve: ({promptId}, _args: unknown, {dataLoader}) => { - if (!promptId) return null - return dataLoader.get('reflectPrompts').load(promptId) - } - } - }) -}) - -export default RenameReflectTemplatePromptPayload From 2f8cf278fa1f7499ad8fbbd305aee3822d21b696 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Mon, 9 Sep 2024 12:56:54 -0700 Subject: [PATCH 11/16] fix downmigration Signed-off-by: Matt Krick --- .../public/types/RemoveReflectTemplatePromptPayload.ts | 4 +++- .../postgres/migrations/1725655687704_ReflectPrompt-phase1.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/server/graphql/public/types/RemoveReflectTemplatePromptPayload.ts b/packages/server/graphql/public/types/RemoveReflectTemplatePromptPayload.ts index 9576753add4..8e81e655db0 100644 --- a/packages/server/graphql/public/types/RemoveReflectTemplatePromptPayload.ts +++ b/packages/server/graphql/public/types/RemoveReflectTemplatePromptPayload.ts @@ -9,7 +9,9 @@ export type RemoveReflectTemplatePromptPayloadSource = const RemoveReflectTemplatePromptPayload: RemoveReflectTemplatePromptPayloadResolvers = { reflectTemplate: (source, _args, {dataLoader}) => { - return 'templateId' in source ? dataLoader.get('meetingTemplates').load(templateId) : null + return 'templateId' in source + ? dataLoader.get('meetingTemplates').loadNonNull(source.templateId) + : null }, prompt: (source, _args, {dataLoader}) => { diff --git a/packages/server/postgres/migrations/1725655687704_ReflectPrompt-phase1.ts b/packages/server/postgres/migrations/1725655687704_ReflectPrompt-phase1.ts index 373ccc1f3b4..425985da005 100644 --- a/packages/server/postgres/migrations/1725655687704_ReflectPrompt-phase1.ts +++ b/packages/server/postgres/migrations/1725655687704_ReflectPrompt-phase1.ts @@ -50,7 +50,7 @@ export async function down() { const client = new Client(getPgConfig()) await client.connect() await client.query(` - DROP TABLE "Comment"; + DROP TABLE IF EXISTS "ReflectPrompt"; ` /* Do undo magic */) await client.end() } From 171ded7387598a22a15b751d02e884174ffc174e Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Mon, 9 Sep 2024 13:20:54 -0700 Subject: [PATCH 12/16] use explicit columns Signed-off-by: Matt Krick --- .../mutations/helpers/makeRetroTemplates.ts | 36 +++++++++++-------- .../public/mutations/addReflectTemplate.ts | 12 +++++-- packages/server/postgres/types/index.d.ts | 2 ++ 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/packages/server/graphql/mutations/helpers/makeRetroTemplates.ts b/packages/server/graphql/mutations/helpers/makeRetroTemplates.ts index bd57b97b0c8..2c52d8eadff 100644 --- a/packages/server/graphql/mutations/helpers/makeRetroTemplates.ts +++ b/packages/server/graphql/mutations/helpers/makeRetroTemplates.ts @@ -1,5 +1,7 @@ +import {positionAfter} from '../../../../client/shared/sortOrder' import ReflectTemplate from '../../../database/types/ReflectTemplate' -import RetrospectivePrompt from '../../../database/types/RetrospectivePrompt' +import generateUID from '../../../generateUID' +import {ReflectPrompt} from '../../../postgres/types' import getTemplateIllustrationUrl from './getTemplateIllustrationUrl' interface TemplatePrompt { @@ -14,7 +16,7 @@ interface TemplateObject { } const makeRetroTemplates = (teamId: string, orgId: string, templateObj: TemplateObject) => { - const reflectPrompts: RetrospectivePrompt[] = [] + const reflectPrompts: ReflectPrompt[] = [] const templates: ReflectTemplate[] = [] Object.entries(templateObj).forEach(([templateName, promptBase]) => { const template = new ReflectTemplate({ @@ -25,18 +27,24 @@ const makeRetroTemplates = (teamId: string, orgId: string, templateObj: Template mainCategory: 'retrospective' }) - const prompts = promptBase.map( - (prompt, idx) => - new RetrospectivePrompt({ - teamId, - templateId: template.id, - sortOrder: idx, - question: prompt.question, - description: prompt.description, - groupColor: prompt.groupColor, - removedAt: null - }) - ) + let curSortOrder = positionAfter('') + const prompts = promptBase.map((prompt) => { + curSortOrder = positionAfter(curSortOrder) + return { + id: generateUID(), + teamId, + templateId: template.id, + sortOrder: curSortOrder, + question: prompt.question, + description: prompt.description, + groupColor: prompt.groupColor, + removedAt: null, + parentPromptId: null, + // can remove these after phase 3 + createdAt: new Date(), + updatedAt: new Date() + } + }) templates.push(template) reflectPrompts.push(...prompts) }) diff --git a/packages/server/graphql/public/mutations/addReflectTemplate.ts b/packages/server/graphql/public/mutations/addReflectTemplate.ts index 958d938fcc6..993abe9dc0f 100644 --- a/packages/server/graphql/public/mutations/addReflectTemplate.ts +++ b/packages/server/graphql/public/mutations/addReflectTemplate.ts @@ -80,11 +80,14 @@ const addPokerTemplate: MutationResolvers['addPokerTemplate'] = async ( const activePrompts = prompts.filter(({removedAt}) => !removedAt) const newTemplatePrompts = activePrompts.map((prompt) => { return { - ...prompt, id: generateUID(), teamId, templateId: newTemplate.id, parentPromptId: prompt.id, + sortOrder: prompt.sortOrder, + question: prompt.question, + description: prompt.description, + groupColor: prompt.groupColor, removedAt: null } }) @@ -118,11 +121,14 @@ const addPokerTemplate: MutationResolvers['addPokerTemplate'] = async ( const newTemplate = templates[0]! const {id: templateId} = newTemplate await Promise.all([ - r.table('ReflectPrompt').insert(newTemplatePrompts).run(), + r + .table('ReflectPrompt') + .insert(newTemplatePrompts.map((p, idx) => ({...p, sortOrder: idx}))) + .run(), pg .with('MeetingTemplateInsert', (qc) => qc.insertInto('MeetingTemplate').values(newTemplate)) .insertInto('ReflectPrompt') - .values(newTemplatePrompts.map((p) => ({...p, sortOrder: String(p.sortOrder)}))) + .values(newTemplatePrompts) .execute(), decrementFreeTemplatesRemaining(viewerId, 'retro') ]) diff --git a/packages/server/postgres/types/index.d.ts b/packages/server/postgres/types/index.d.ts index 48fb2419996..affb35215d4 100644 --- a/packages/server/postgres/types/index.d.ts +++ b/packages/server/postgres/types/index.d.ts @@ -9,6 +9,7 @@ import { selectComments, selectMeetingSettings, selectOrganizations, + selectReflectPrompts, selectRetroReflections, selectSlackAuths, selectSlackNotifications, @@ -56,3 +57,4 @@ export type SlackAuth = ExtractTypeFromQueryBuilderSelect export type Comment = ExtractTypeFromQueryBuilderSelect +export type ReflectPrompt = ExtractTypeFromQueryBuilderSelect From a38beb4d5ee19c8c203117ae6dd59294d90ab0fb Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Mon, 9 Sep 2024 14:38:43 -0700 Subject: [PATCH 13/16] migrate existing rows Signed-off-by: Matt Krick --- .../1725913333530_ReflectPrompt-phase2.ts | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 packages/server/postgres/migrations/1725913333530_ReflectPrompt-phase2.ts diff --git a/packages/server/postgres/migrations/1725913333530_ReflectPrompt-phase2.ts b/packages/server/postgres/migrations/1725913333530_ReflectPrompt-phase2.ts new file mode 100644 index 00000000000..5654c71dc22 --- /dev/null +++ b/packages/server/postgres/migrations/1725913333530_ReflectPrompt-phase2.ts @@ -0,0 +1,169 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {r} from 'rethinkdb-ts' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' + +const START_CHAR_CODE = 32 +const END_CHAR_CODE = 126 + +export function positionAfter(pos: string) { + for (let i = pos.length - 1; i >= 0; i--) { + const curCharCode = pos.charCodeAt(i) + if (curCharCode < END_CHAR_CODE) { + return pos.substr(0, i) + String.fromCharCode(curCharCode + 1) + } + } + return pos + String.fromCharCode(START_CHAR_CODE + 1) +} + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + + try { + console.log('Adding index') + await r + .table('ReflectPrompt') + .indexCreate('updatedAtId', (row: any) => [row('updatedAt'), row('id')]) + .run() + await r.table('ReflectPrompt').indexWait().run() + } catch { + // index already exists + } + + console.log('Adding index complete') + + const MAX_PG_PARAMS = 65545 + const PG_COLS = [ + 'id', + 'createdAt', + 'updatedAt', + 'removedAt', + 'description', + 'groupColor', + 'sortOrder', + 'question', + 'teamId', + 'templateId', + 'parentPromptId' + ] as const + type ReflectPrompt = { + [K in (typeof PG_COLS)[number]]: any + } + const BATCH_SIZE = Math.trunc(MAX_PG_PARAMS / PG_COLS.length) + + let curUpdatedAt = r.minval + let curId = r.minval + for (let i = 0; i < 1e6; i++) { + console.log('inserting row', i * BATCH_SIZE, String(curUpdatedAt), String(curId)) + const rawRowsToInsert = (await r + .table('ReflectPrompt') + .between([curUpdatedAt, curId], [r.maxval, r.maxval], { + index: 'updatedAtId', + leftBound: 'open', + rightBound: 'closed' + }) + .orderBy({index: 'updatedAtId'}) + .limit(BATCH_SIZE) + .pluck(...PG_COLS) + .run()) as ReflectPrompt[] + + const rowsToInsert = rawRowsToInsert.map((row) => { + const {description, groupColor, sortOrder, question, ...rest} = row as any + return { + ...rest, + description: description?.slice(0, 256) ?? '', + groupColor: groupColor?.slice(0, 9) ?? '#66BC8C', + sortOrder: String(sortOrder), + question: question?.slice(0, 100) ?? '' + } + }) + + if (rowsToInsert.length === 0) break + const lastRow = rowsToInsert[rowsToInsert.length - 1] + curUpdatedAt = lastRow.updatedAt + curId = lastRow.id + await Promise.all( + rowsToInsert.map(async (row) => { + try { + await pg + .insertInto('ReflectPrompt') + .values(row) + .onConflict((oc) => oc.doNothing()) + .execute() + } catch (e) { + if (e.constraint === 'fk_templateId' || e.constraint === 'fk_teamId') { + console.log('Missing templateId or teamId', row.id) + return + } + console.log(e, row) + } + }) + ) + } + + // remap the sortOrder in PG because rethinkdb is too slow to group + console.log('Correcting sortOrder') + const pgRows = await sql<{items: {sortOrder: string; id: string}[]}>` + select jsonb_agg(jsonb_build_object('sortOrder', "sortOrder", 'id', "id", 'templateId', "templateId") ORDER BY "sortOrder") items from "ReflectPrompt" +group by "templateId";`.execute(pg) + + const groups = pgRows.rows.map((row) => { + const {items} = row + let curSortOrder = '' + for (let i = 0; i < items.length; i++) { + const item = items[i] + curSortOrder = positionAfter(curSortOrder) + item.sortOrder = curSortOrder + } + return row + }) + for (let i = 0; i < groups.length; i++) { + const group = groups[i] + await Promise.all( + group.items.map((item) => { + return pg + .updateTable('ReflectPrompt') + .set({sortOrder: item.sortOrder}) + .where('id', '=', item.id) + .execute() + }) + ) + } + + // if the threadParentId references an id that does not exist, set it to null + console.log('adding parentPromptId constraint') + await pg + .updateTable('ReflectPrompt') + .set({parentPromptId: null}) + .where(({eb, selectFrom}) => + eb( + 'id', + 'in', + selectFrom('ReflectPrompt as child') + .select('child.id') + .leftJoin('ReflectPrompt as parent', 'child.parentPromptId', 'parent.id') + .where('parent.id', 'is', null) + .where('child.parentPromptId', 'is not', null) + ) + ) + .execute() + await pg.schema + .alterTable('ReflectPrompt') + .addForeignKeyConstraint('fk_parentPromptId', ['parentPromptId'], 'ReflectPrompt', ['id']) + .onDelete('set null') + .execute() +} + +export async function down() { + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql`TRUNCATE TABLE "ReflectPrompt" CASCADE`.execute(pg) +} From fcc66a9998f57f0b4c2fa44952de769da6e33792 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Mon, 9 Sep 2024 14:59:19 -0700 Subject: [PATCH 14/16] use explicit props --- .../server/graphql/mutations/updateTemplateScope.ts | 12 ++++++++---- packages/server/graphql/public/types/ReflectPhase.ts | 3 +-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/server/graphql/mutations/updateTemplateScope.ts b/packages/server/graphql/mutations/updateTemplateScope.ts index 86c93b911f2..1b7c7aeeca1 100644 --- a/packages/server/graphql/mutations/updateTemplateScope.ts +++ b/packages/server/graphql/mutations/updateTemplateScope.ts @@ -5,7 +5,6 @@ import {RDatum} from '../../database/stricterR' import {SharingScopeEnum as ESharingScope} from '../../database/types/MeetingTemplate' import PokerTemplate from '../../database/types/PokerTemplate' import ReflectTemplate from '../../database/types/ReflectTemplate' -import RetrospectivePrompt from '../../database/types/RetrospectivePrompt' import generateUID from '../../generateUID' import getKysely from '../../postgres/getKysely' import {analytics} from '../../utils/analytics/analytics' @@ -89,12 +88,17 @@ const updateTemplateScope = { const activePrompts = prompts.filter(({removedAt}) => !removedAt) const promptIds = activePrompts.map(({id}) => id) const clonedPrompts = activePrompts.map((prompt) => { - return new RetrospectivePrompt({ - ...prompt, + return { + id: generateUID(), + teamId: prompt.teamId, templateId: clonedTemplateId!, parentPromptId: prompt.id, + sortOrder: prompt.sortOrder, + question: prompt.question, + description: prompt.description, + groupColor: prompt.groupColor, removedAt: null - }) + } }) await Promise.all([ pg diff --git a/packages/server/graphql/public/types/ReflectPhase.ts b/packages/server/graphql/public/types/ReflectPhase.ts index 9943e8fac56..fb5bad8671e 100644 --- a/packages/server/graphql/public/types/ReflectPhase.ts +++ b/packages/server/graphql/public/types/ReflectPhase.ts @@ -1,5 +1,4 @@ import MeetingRetrospective from '../../../database/types/MeetingRetrospective' -import RetrospectivePrompt from '../../../database/types/RetrospectivePrompt' import {ReflectPhaseResolvers} from '../resolverTypes' const ReflectPhase: ReflectPhaseResolvers = { @@ -15,7 +14,7 @@ const ReflectPhase: ReflectPhaseResolvers = { // only show prompts that were created before the meeting and // either have not been removed or they were removed after the meeting was created return prompts.filter( - (prompt: RetrospectivePrompt) => + (prompt) => prompt.createdAt < meeting.createdAt && (!prompt.removedAt || meeting.createdAt < prompt.removedAt) ) From c44f72c128e57e21e8b7819b19c9a6e3d995f010 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Mon, 9 Sep 2024 14:59:37 -0700 Subject: [PATCH 15/16] use explicit props nullable --- packages/server/database/types/RetrospectivePrompt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/database/types/RetrospectivePrompt.ts b/packages/server/database/types/RetrospectivePrompt.ts index a278dee85ad..8e06704a71e 100644 --- a/packages/server/database/types/RetrospectivePrompt.ts +++ b/packages/server/database/types/RetrospectivePrompt.ts @@ -8,7 +8,7 @@ interface Input { description: string groupColor: string removedAt: Date | null - parentPromptId?: string + parentPromptId?: string | null } export default class RetrospectivePrompt { From 21fc97c85650ce2c8cc41600a973beb0be0aeecc Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 10 Sep 2024 12:06:27 -0700 Subject: [PATCH 16/16] fix tsc Signed-off-by: Matt Krick --- packages/server/database/types/RetrospectivePrompt.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/database/types/RetrospectivePrompt.ts b/packages/server/database/types/RetrospectivePrompt.ts index 8e06704a71e..23f8b6b5373 100644 --- a/packages/server/database/types/RetrospectivePrompt.ts +++ b/packages/server/database/types/RetrospectivePrompt.ts @@ -22,7 +22,7 @@ export default class RetrospectivePrompt { question: string removedAt: Date | null updatedAt = new Date() - parentPromptId?: string + parentPromptId?: string | null constructor(input: Input) { const { @@ -43,6 +43,6 @@ export default class RetrospectivePrompt { this.description = description || '' this.groupColor = groupColor this.removedAt = removedAt - this.parentPromptId = parentPromptId + this.parentPromptId = parentPromptId || null } }